Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f5c035f527 | |||
| 58e4072e60 | |||
| ced0533fc2 | |||
| 8732fce613 | |||
| b80afd1877 | |||
| a7b1ae7c7d | |||
| 111f397741 |
+259
-330
@@ -9156,61 +9156,6 @@ def _credential_label(name: str, entry: ProviderEntry) -> str:
|
||||
)
|
||||
|
||||
|
||||
def _harness_summary_lines(config: dict[str, Any], family: str) -> list[str]: # type: ignore[explicit-any]
|
||||
"""The styled sub-line(s) shown under a harness on the level-1 overview.
|
||||
|
||||
Returns a prominent default line — a bold-green ``✓`` + the default
|
||||
credential's label, with the model dimmed — and, when there are other
|
||||
credentials, a dim ``+N more`` line (the full list is one keystroke away on
|
||||
level 2). Mirrors how ``gh`` / ``gcloud`` summaries surface the active
|
||||
item: highlight it, don't enumerate the rest. The returned strings carry
|
||||
Rich markup; :func:`_render_menu` indents them without re-styling.
|
||||
|
||||
:param config: The parsed config mapping (``providers:`` block).
|
||||
:param family: The harness surface, ``"anthropic"``, ``"openai"``, or
|
||||
``"pi"``.
|
||||
:returns: One or two markup sub-lines, e.g. ``["[bold green]✓ Anthropic API
|
||||
Key[/][dim] · claude-opus-4-8[/]", "[dim]+1 more[/]"]``, or
|
||||
``["[dim]no credential yet — open to add one[/]"]``.
|
||||
"""
|
||||
from omnigent.onboarding.provider_config import (
|
||||
load_providers,
|
||||
provider_families,
|
||||
surface_default_model,
|
||||
surface_default_provider,
|
||||
)
|
||||
|
||||
serving = [
|
||||
(name, entry)
|
||||
for name, entry in load_providers(config).items()
|
||||
if family in provider_families(entry)
|
||||
]
|
||||
if not serving:
|
||||
return ["[dim]no credential yet — open to add one[/]"]
|
||||
# The surface's *effective* default: for the family surfaces this is the
|
||||
# explicit per-family default; for pi it is what the pi harness would
|
||||
# actually route through (explicit pi scope, else the fallback).
|
||||
default = surface_default_provider(config, family)
|
||||
default_label: str | None = None
|
||||
default_model: str | None = None
|
||||
others = 0
|
||||
for name, entry in serving:
|
||||
if default is not None and name == default.name:
|
||||
default_label = _family_credential_label(config, family, name, entry)
|
||||
default_model = surface_default_model(entry, family)
|
||||
else:
|
||||
others += 1
|
||||
if default_label is None:
|
||||
return ["[dim]no default set — open to choose one[/]"]
|
||||
default_line = f"[bold green]✓ {default_label}[/]" + (
|
||||
f"[dim] · {default_model}[/]" if default_model else ""
|
||||
)
|
||||
lines = [default_line]
|
||||
if others:
|
||||
lines.append(f"[dim]+{others} more[/]")
|
||||
return lines
|
||||
|
||||
|
||||
def _harness_credential_rows(config: dict[str, Any], family: str) -> list[_HarnessMenuRow]: # type: ignore[explicit-any]
|
||||
"""Build the level-2 rows: each credential serving *family*, then ``+ Add``.
|
||||
|
||||
@@ -10888,13 +10833,21 @@ def _run_configure_harnesses_interactive() -> None:
|
||||
Opening it backfills a legacy databricks ``auth:`` block into a real
|
||||
provider and adopts any ambient-detected credential — announcing the
|
||||
newly auto-configured machine credentials in a callout — then loops on
|
||||
the level-1 harness overview (Claude / Codex / Pi / Cursor / Antigravity /
|
||||
Qwen Code / Kimi Code / Quit) until the user quits or presses Esc.
|
||||
the level-1 harness overview. Every harness is shown on a single compact
|
||||
row — the harness name on the left, then an aligned ``✓``/``✗`` status
|
||||
column (the configured credential, or "Not installed" / "No
|
||||
credential") — in 0.3 priority order: Claude, Codex, Cursor, OpenCode,
|
||||
Hermes, Pi, then Antigravity, Qwen Code, Goose, Copilot, Kiro, Kimi Code.
|
||||
The actionable hint (install command / next step) renders only for the
|
||||
highlighted row, as the selector's description line, so the overview stays
|
||||
uncluttered.
|
||||
|
||||
:returns: None. Side effect: may write ``~/.omnigent/config.yaml`` via
|
||||
the backfill/adopt steps and any add/set-default/remove the user
|
||||
performs while navigating.
|
||||
"""
|
||||
from rich.markup import escape
|
||||
|
||||
from omnigent.onboarding.antigravity_auth import (
|
||||
ANTIGRAVITY_ENV_VARS,
|
||||
ANTIGRAVITY_EXTRA_INSTALL_COMMAND,
|
||||
@@ -10942,18 +10895,12 @@ def _run_configure_harnesses_interactive() -> None:
|
||||
|
||||
# Backfill a databricks provider from a legacy global auth: block FIRST (it
|
||||
# outranks ambient detection in routing), then adopt ambient detections.
|
||||
# The databricks backfill is silent (it just shows up in the harness summary
|
||||
# The databricks backfill is silent (it just shows up in the harness status
|
||||
# line); newly-adopted machine credentials get a one-time callout naming
|
||||
# what was auto-configured and from where. The detection scan can take a
|
||||
# beat (on macOS it shells out to ``claude auth status`` to read the
|
||||
# Keychain), so surface a spinner over just that step — it clears before the
|
||||
# callout (and the menu) paints, and is a no-op off a TTY.
|
||||
from omnigent._runner_startup import runner_startup_progress
|
||||
|
||||
with runner_startup_progress(
|
||||
initial_message="Searching for existing credentials…"
|
||||
) as progress:
|
||||
_adopt_ambient_credentials(progress=progress)
|
||||
# what was auto-configured and from where. No progress spinner here: a
|
||||
# transient spinner over the (fast) detection left a cleared-region gap and
|
||||
# a residual line directly above the menu on first paint.
|
||||
_adopt_ambient_credentials()
|
||||
|
||||
# Level 1: pick a harness. The cursor moves between Claude, Codex, Pi, and
|
||||
# Quit; each harness's status renders as a non-selectable sub-line beneath
|
||||
@@ -10991,299 +10938,281 @@ def _run_configure_harnesses_interactive() -> None:
|
||||
# own drill-in rather than ``_manage_harness_providers``.
|
||||
_KIMI = "\x00kimi"
|
||||
families = [ANTHROPIC_FAMILY, OPENAI_FAMILY, PI_SURFACE]
|
||||
while True:
|
||||
config = _load_global_config()
|
||||
options: list[str] = []
|
||||
selectable: list[bool] = []
|
||||
row_target: list[str | None] = []
|
||||
for fam in families:
|
||||
# A harness's readiness is a single descent: is the CLI installed? →
|
||||
# does it have a usable default credential? → show that credential.
|
||||
# Only a fully ready harness carries no name-level marker (its green
|
||||
# default line in the summary already says it's ready); any harness
|
||||
# that can't be used yet — not installed, or installed but with no
|
||||
# usable default — gets a red ✗, so it's clear at a glance which
|
||||
# harnesses still need attention. Pi's default is its *effective*
|
||||
# one (explicit pi scope, else the cross-family fallback).
|
||||
installed = harness_cli_installed(fam)
|
||||
ready = installed and surface_default_provider(config, fam) is not None
|
||||
marker = " " if ready else "[red]✗[/] "
|
||||
options.append(f"{marker}{family_label(fam)}")
|
||||
selectable.append(True)
|
||||
row_target.append(fam)
|
||||
# Sub-line text follows the same descent. An uninstalled harness
|
||||
# points at the install command (creds are moot until it exists);
|
||||
# otherwise the summary helper renders "no credential yet" / "no
|
||||
# default set" / the ✓ default line.
|
||||
if not installed:
|
||||
# Parallel to "no credential yet — open to add one": name the
|
||||
# state, point at the action. The exact ``npm install`` command
|
||||
# is shown on drill-in (``_prompt_install_harness``), so it stays
|
||||
# off the overview — keeping the line short enough not to wrap.
|
||||
sub_lines = ["[dim]not installed yet — open to install[/]"]
|
||||
else:
|
||||
sub_lines = _harness_summary_lines(config, fam)
|
||||
for sub_line in sub_lines:
|
||||
# Indent every status sub-line a touch more than the harness
|
||||
# name so it reads as hanging off the marker column — the
|
||||
# configured default's ✓ (and the "not installed" / "no
|
||||
# credential yet" hints) all start at the same column.
|
||||
options.append(f" {sub_line}")
|
||||
selectable.append(False) # a sub-line — cursor skips it
|
||||
row_target.append(None)
|
||||
# Cursor: runs via the ``cursor-sdk`` package and authenticates with a
|
||||
# ``CURSOR_API_KEY`` (the SDK requires one; it has no provider/gateway
|
||||
# family and a ``cursor-agent login`` does not apply). So readiness is
|
||||
# simply whether an API key is configured — one stored by setup (the
|
||||
# ``cursor:`` block) or inherited from the environment — and its
|
||||
# drill-in manages exactly that key.
|
||||
cursor_key_set = cursor_api_key_configured(config) or bool(
|
||||
os.environ.get("CURSOR_API_KEY")
|
||||
)
|
||||
options.append(f"{' ' if cursor_key_set else '[red]✗[/] '}Cursor")
|
||||
selectable.append(True)
|
||||
row_target.append(CURSOR_KEY)
|
||||
# ``cursor-sdk`` now ships in an OPTIONAL extra, so the key can be set
|
||||
# with no SDK present. When the extra is missing, lead with that gap and
|
||||
# the install command (parallel to Antigravity post-#322), then still
|
||||
# report key status. ``[cursor]`` is escaped — sub-lines render through
|
||||
# Rich markup, where bare brackets parse as a tag.
|
||||
cursor_sub_lines: list[str] = []
|
||||
if not cursor_sdk_installed():
|
||||
from rich.markup import escape as _rich_escape
|
||||
|
||||
cursor_sub_lines.append(
|
||||
f"[dim]not installed — open to install "
|
||||
f"({_rich_escape(CURSOR_EXTRA_INSTALL_COMMAND)})[/]"
|
||||
# Status glyph + Rich color per readiness kind: "ready" is a configured,
|
||||
# launchable harness (green ✓); "missing" is an absent CLI/SDK (red ✗);
|
||||
# "warn" is installed-but-unconfigured (yellow ✗ — present, not usable
|
||||
# yet). The glyph leads the status, which sits in a left-aligned column
|
||||
# right of the names, so every ✓/✗ lines up in a single column.
|
||||
status_styles = {"ready": ("✓", "green"), "missing": ("✗", "red"), "warn": ("✗", "yellow")}
|
||||
|
||||
def _install_hint(command: str) -> str:
|
||||
# Selection-only tooltip. The command is escaped so a bracketed extra
|
||||
# (e.g. ``pip install "omnigent[cursor]"``) renders literally instead of
|
||||
# parsing as Rich markup.
|
||||
return f"Install with `{escape(command)}`"
|
||||
|
||||
def _family_row(fam: str) -> tuple[str, str, str, str, str]:
|
||||
# Claude / Codex / Pi: a CLI binary plus a usable default credential.
|
||||
# Pi's default is its *effective* one (explicit pi scope, else the
|
||||
# cross-family fallback).
|
||||
name = family_label(fam)
|
||||
if not harness_cli_installed(fam):
|
||||
return (
|
||||
fam,
|
||||
name,
|
||||
"Not installed",
|
||||
"missing",
|
||||
_install_hint(" ".join(harness_install_command(fam))),
|
||||
)
|
||||
cursor_sub_lines.append(
|
||||
"[green]✓[/] API key configured"
|
||||
if cursor_key_set
|
||||
else "[dim]no API key yet — open to add one[/]"
|
||||
)
|
||||
for cursor_sub in cursor_sub_lines:
|
||||
options.append(f" {cursor_sub}")
|
||||
selectable.append(False)
|
||||
row_target.append(None)
|
||||
# Antigravity (Gemini-native, no provider family): like Cursor, readiness
|
||||
# is just whether a Gemini key is configured (``antigravity:`` block or
|
||||
# ambient env); its drill-in manages that key. Vertex specs need no key,
|
||||
# so a ✗ isn't a hard blocker for that path.
|
||||
ag_key_set = antigravity_api_key_configured(config) or any(
|
||||
os.environ.get(v) for v in ANTIGRAVITY_ENV_VARS
|
||||
)
|
||||
options.append(f"{' ' if ag_key_set else '[red]✗[/] '}Antigravity")
|
||||
selectable.append(True)
|
||||
row_target.append(_ANTIGRAVITY)
|
||||
# The antigravity SDK ships in an OPTIONAL extra (unlike Cursor's baseline
|
||||
# ``cursor-sdk``), so a user can have a key but no SDK. Lead with that gap when
|
||||
# the extra is missing — naming the install command inline — then still report
|
||||
# key status. ``[antigravity]`` is escaped since the sub-lines render as Rich
|
||||
# markup (bare brackets parse as a tag).
|
||||
ag_sub_lines: list[str] = []
|
||||
if not antigravity_sdk_installed():
|
||||
from rich.markup import escape as _rich_escape
|
||||
default = surface_default_provider(config, fam)
|
||||
if default is None:
|
||||
return (fam, name, "Not configured", "warn", "Open to add a credential.")
|
||||
label = _family_credential_label(config, fam, default.name, default)
|
||||
return (fam, name, label, "ready", "")
|
||||
|
||||
ag_sub_lines.append(
|
||||
f"[dim]not installed — open to install "
|
||||
f"({_rich_escape(ANTIGRAVITY_EXTRA_INSTALL_COMMAND)})[/]"
|
||||
)
|
||||
ag_sub_lines.append(
|
||||
"[green]✓[/] Gemini API key configured"
|
||||
if ag_key_set
|
||||
else "[dim]no Gemini API key yet — open to add one[/]"
|
||||
)
|
||||
for ag_sub in ag_sub_lines:
|
||||
options.append(f" {ag_sub}")
|
||||
selectable.append(False)
|
||||
row_target.append(None)
|
||||
# Qwen Code (OpenAI-compatible auth, no provider family — like Cursor /
|
||||
# Antigravity). Qwen has no CLI login (its ``auth`` subcommand was
|
||||
# removed); auth comes from OpenAI-compatible env vars or the interactive
|
||||
# ``/auth`` flow. "Ready" means the CLI is installed AND we can detect
|
||||
# auth — ``_qwen_auth_configured`` reads env vars / ~/.qwen creds, so the
|
||||
# overview never falsely shows "signed in" for a fresh, unauthed install.
|
||||
qwen_installed = harness_cli_installed(QWEN_KEY)
|
||||
qwen_authed = qwen_installed and _qwen_auth_configured()
|
||||
options.append(f"{' ' if qwen_authed else '[red]✗[/] '}Qwen Code")
|
||||
selectable.append(True)
|
||||
row_target.append(_QWEN)
|
||||
if not qwen_installed:
|
||||
from rich.markup import escape as _rich_escape
|
||||
|
||||
qwen_cmd = _rich_escape(" ".join(harness_install_command(QWEN_KEY)))
|
||||
qwen_sub = f"[dim]not installed — open to install ({qwen_cmd})[/]"
|
||||
elif qwen_authed:
|
||||
qwen_sub = "[green]✓[/] authentication detected"
|
||||
else:
|
||||
qwen_sub = "[dim]installed — open to set up auth (/auth or env vars)[/]"
|
||||
options.append(f" {qwen_sub}")
|
||||
selectable.append(False)
|
||||
row_target.append(None)
|
||||
# OpenCode (native-server harness): readiness is just whether the
|
||||
# ``opencode`` CLI is installed — it has no Omnigent-stored credential,
|
||||
# routing through the bound agent's Databricks gateway profile or
|
||||
# ambient provider env. Its drill-in installs the CLI and explains that.
|
||||
# OpenCode: ready = CLI installed AND a provider reachable (a stored
|
||||
# ``opencode auth login`` credential or a provider env key). Drill-in
|
||||
# manages its native login. (Gateway path uses the agent profile.)
|
||||
def build_harness_rows() -> list[tuple[str, str, str, str, str]]:
|
||||
# One visible row per harness, in 0.3 priority order. No folding — every
|
||||
# harness shows at once. Each row is (target, name, status, kind, hint),
|
||||
# where ``hint`` is the selection-only description (install command /
|
||||
# next step), empty for a ready harness.
|
||||
from omnigent.onboarding.opencode_auth import opencode_auth_summary
|
||||
|
||||
opencode_summary = opencode_auth_summary()
|
||||
opencode_ready = opencode_summary.ready
|
||||
options.append(f"{' ' if opencode_ready else '[red]✗[/] '}OpenCode")
|
||||
selectable.append(True)
|
||||
row_target.append(_OPENCODE)
|
||||
if not opencode_summary.installed:
|
||||
from rich.markup import escape as _rich_escape
|
||||
rows: list[tuple[str, str, str, str, str]] = []
|
||||
rows.append(_family_row(ANTHROPIC_FAMILY))
|
||||
rows.append(_family_row(OPENAI_FAMILY))
|
||||
|
||||
opencode_cmd = _rich_escape(" ".join(harness_install_command(OPENCODE_KEY)))
|
||||
opencode_sub = f"[dim]not installed — open to install ({opencode_cmd})[/]"
|
||||
elif opencode_ready:
|
||||
opencode_sub = f"[green]✓[/] {opencode_summary.describe()}"
|
||||
else:
|
||||
opencode_sub = "[dim]installed — open to sign in (opencode auth login)[/]"
|
||||
options.append(f" {opencode_sub}")
|
||||
selectable.append(False)
|
||||
row_target.append(None)
|
||||
# Goose (its own provider config — no provider family, like Cursor /
|
||||
# Antigravity / Qwen). Goose owns its auth via ``goose configure``
|
||||
# (keyring / ~/.config/goose/config.yaml); Omnigent stores no key, so
|
||||
# "ready" means the CLI is installed AND a provider is configured
|
||||
# (``goose_config_summary`` reads GOOSE_PROVIDER from env or the config
|
||||
# file, so a fresh, unconfigured install never falsely shows as ready).
|
||||
goose_installed = harness_cli_installed(GOOSE_KEY)
|
||||
goose_summary = goose_config_summary() if goose_installed else None
|
||||
goose_ready = goose_summary is not None and goose_summary.provider is not None
|
||||
options.append(f"{' ' if goose_ready else '[red]✗[/] '}Goose")
|
||||
selectable.append(True)
|
||||
row_target.append(_GOOSE)
|
||||
if not goose_installed:
|
||||
from rich.markup import escape as _rich_escape
|
||||
|
||||
goose_spec = harness_install_spec(GOOSE_KEY)
|
||||
goose_hint = _rich_escape(
|
||||
goose_spec.install_hint
|
||||
if goose_spec and goose_spec.install_hint
|
||||
else "brew install block-goose-cli"
|
||||
# Cursor — readiness is the CURSOR_API_KEY (the cursor-sdk extra is a
|
||||
# soft dependency; the key is independently storable, so a missing SDK
|
||||
# is surfaced as the install hint, not a hard block).
|
||||
if cursor_api_key_configured(config) or bool(os.environ.get("CURSOR_API_KEY")):
|
||||
rows.append((CURSOR_KEY, "Cursor", "API key", "ready", ""))
|
||||
elif not cursor_sdk_installed():
|
||||
rows.append(
|
||||
(
|
||||
CURSOR_KEY,
|
||||
"Cursor",
|
||||
"Not installed",
|
||||
"missing",
|
||||
_install_hint(CURSOR_EXTRA_INSTALL_COMMAND),
|
||||
),
|
||||
)
|
||||
goose_sub = f"[dim]not installed — open to install ({goose_hint})[/]"
|
||||
elif goose_ready:
|
||||
assert goose_summary is not None
|
||||
goose_model = f" · {goose_summary.model}" if goose_summary.model else ""
|
||||
goose_sub = f"[green]✓[/] {goose_summary.provider}{goose_model} configured"
|
||||
else:
|
||||
goose_sub = "[dim]installed — open to run goose configure[/]"
|
||||
options.append(f" {goose_sub}")
|
||||
selectable.append(False)
|
||||
row_target.append(None)
|
||||
# Copilot (GitHub Copilot SDK, no provider family): like Cursor, readiness
|
||||
# is just whether a GitHub token with Copilot access is configured (the
|
||||
# ``copilot:`` block or an ambient ``COPILOT_GITHUB_TOKEN``/``GH_TOKEN``/
|
||||
# ``GITHUB_TOKEN``); its drill-in manages that token.
|
||||
copilot_token_set = copilot_github_token_configured(config) or any(
|
||||
os.environ.get(v) for v in COPILOT_TOKEN_ENV_VARS
|
||||
)
|
||||
options.append(f"{' ' if copilot_token_set else '[red]✗[/] '}Copilot")
|
||||
selectable.append(True)
|
||||
row_target.append(COPILOT_KEY)
|
||||
# ``github-copilot-sdk`` ships in an OPTIONAL extra, so the token can be
|
||||
# set with no SDK present. When the extra is missing, lead with that gap
|
||||
# and the install command (parallel to Cursor / Antigravity), then still
|
||||
# report token status. ``[copilot]`` is escaped — sub-lines render through
|
||||
# Rich markup, where bare brackets parse as a tag.
|
||||
copilot_sub_lines: list[str] = []
|
||||
if not copilot_sdk_installed():
|
||||
from rich.markup import escape as _rich_escape
|
||||
|
||||
copilot_sub_lines.append(
|
||||
f"[dim]not installed — open to install "
|
||||
f"({_rich_escape(COPILOT_EXTRA_INSTALL_COMMAND)})[/]"
|
||||
rows.append(
|
||||
(
|
||||
CURSOR_KEY,
|
||||
"Cursor",
|
||||
"Not configured",
|
||||
"warn",
|
||||
"Open to add the Cursor API key.",
|
||||
),
|
||||
)
|
||||
copilot_sub_lines.append(
|
||||
"[green]✓[/] GitHub token configured"
|
||||
if copilot_token_set
|
||||
else "[dim]no GitHub token yet — open to add one[/]"
|
||||
)
|
||||
for copilot_sub in copilot_sub_lines:
|
||||
options.append(f" {copilot_sub}")
|
||||
selectable.append(False)
|
||||
row_target.append(None)
|
||||
# Hermes Agent (its own provider config via ``hermes model``, installed
|
||||
# via a curl installer from Nous Research — no npm package or Omnigent
|
||||
# credential).
|
||||
hermes_installed = harness_cli_installed(HERMES_KEY)
|
||||
options.append(f"{' ' if hermes_installed else '[red]✗[/] '}Hermes")
|
||||
selectable.append(True)
|
||||
row_target.append(_HERMES)
|
||||
if not hermes_installed:
|
||||
from rich.markup import escape as _rich_escape
|
||||
|
||||
# OpenCode — its own provider auth (login or env keys); the status is
|
||||
# what it can reach (e.g. "1 stored").
|
||||
opencode = opencode_auth_summary()
|
||||
if not opencode.installed:
|
||||
rows.append(
|
||||
(
|
||||
_OPENCODE,
|
||||
"OpenCode",
|
||||
"Not installed",
|
||||
"missing",
|
||||
_install_hint(" ".join(harness_install_command(OPENCODE_KEY))),
|
||||
),
|
||||
)
|
||||
elif opencode.ready:
|
||||
rows.append((_OPENCODE, "OpenCode", opencode.describe(), "ready", ""))
|
||||
else:
|
||||
rows.append(
|
||||
(
|
||||
_OPENCODE,
|
||||
"OpenCode",
|
||||
"Not configured",
|
||||
"warn",
|
||||
"Open to sign in (opencode auth login).",
|
||||
),
|
||||
)
|
||||
|
||||
# Hermes — curl-installed, no Omnigent credential, so readiness is just
|
||||
# the binary.
|
||||
if harness_cli_installed(HERMES_KEY):
|
||||
rows.append(
|
||||
(
|
||||
_HERMES,
|
||||
"Hermes",
|
||||
"Installed",
|
||||
"ready",
|
||||
"Open to configure with `hermes model`.",
|
||||
),
|
||||
)
|
||||
else:
|
||||
hermes_spec = harness_install_spec(HERMES_KEY)
|
||||
hermes_hint = _rich_escape(
|
||||
hermes_hint = (
|
||||
hermes_spec.install_hint
|
||||
if hermes_spec and hermes_spec.install_hint
|
||||
else "curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash"
|
||||
)
|
||||
hermes_sub = f"[dim]not installed — open to install ({hermes_hint})[/]"
|
||||
else:
|
||||
hermes_sub = "[green]✓[/] ready"
|
||||
options.append(f" {hermes_sub}")
|
||||
selectable.append(False)
|
||||
row_target.append(None)
|
||||
# Kiro — native kiro-cli TUI (own auth via `kiro-cli login`, installed via
|
||||
# Kiro's curl installer — no npm package or Omnigent credential).
|
||||
kiro_installed = harness_cli_installed(KIRO_KEY)
|
||||
options.append(f"{' ' if kiro_installed else '[red]✗[/] '}Kiro")
|
||||
selectable.append(True)
|
||||
row_target.append(_KIRO)
|
||||
if not kiro_installed:
|
||||
from rich.markup import escape as _rich_escape
|
||||
rows.append(
|
||||
(_HERMES, "Hermes", "Not installed", "missing", _install_hint(hermes_hint)),
|
||||
)
|
||||
|
||||
rows.append(_family_row(PI_SURFACE))
|
||||
|
||||
# Antigravity — Gemini key (antigravity-sdk extra is soft, like Cursor).
|
||||
if antigravity_api_key_configured(config) or any(
|
||||
os.environ.get(v) for v in ANTIGRAVITY_ENV_VARS
|
||||
):
|
||||
rows.append((_ANTIGRAVITY, "Antigravity", "Gemini API key", "ready", ""))
|
||||
elif not antigravity_sdk_installed():
|
||||
rows.append(
|
||||
(
|
||||
_ANTIGRAVITY,
|
||||
"Antigravity",
|
||||
"Not installed",
|
||||
"missing",
|
||||
_install_hint(ANTIGRAVITY_EXTRA_INSTALL_COMMAND),
|
||||
),
|
||||
)
|
||||
else:
|
||||
rows.append(
|
||||
(
|
||||
_ANTIGRAVITY,
|
||||
"Antigravity",
|
||||
"Not configured",
|
||||
"warn",
|
||||
"Open to add the Gemini API key.",
|
||||
),
|
||||
)
|
||||
|
||||
# Qwen Code — no CLI login; auth via OpenAI-compatible env vars or the
|
||||
# interactive /auth flow.
|
||||
if not harness_cli_installed(QWEN_KEY):
|
||||
rows.append(
|
||||
(
|
||||
_QWEN,
|
||||
"Qwen Code",
|
||||
"Not installed",
|
||||
"missing",
|
||||
_install_hint(" ".join(harness_install_command(QWEN_KEY))),
|
||||
),
|
||||
)
|
||||
elif _qwen_auth_configured():
|
||||
rows.append((_QWEN, "Qwen Code", "Authenticated", "ready", ""))
|
||||
else:
|
||||
rows.append(
|
||||
(
|
||||
_QWEN,
|
||||
"Qwen Code",
|
||||
"Not configured",
|
||||
"warn",
|
||||
"Open to set up auth (/auth or env vars).",
|
||||
),
|
||||
)
|
||||
|
||||
# Goose — its own provider config via `goose configure`.
|
||||
if not harness_cli_installed(GOOSE_KEY):
|
||||
goose_spec = harness_install_spec(GOOSE_KEY)
|
||||
goose_hint = (
|
||||
goose_spec.install_hint
|
||||
if goose_spec and goose_spec.install_hint
|
||||
else "brew install block-goose-cli"
|
||||
)
|
||||
rows.append((_GOOSE, "Goose", "Not installed", "missing", _install_hint(goose_hint)))
|
||||
else:
|
||||
goose_summary = goose_config_summary()
|
||||
if goose_summary.provider:
|
||||
rows.append((_GOOSE, "Goose", goose_summary.provider, "ready", ""))
|
||||
else:
|
||||
rows.append(
|
||||
(_GOOSE, "Goose", "Not configured", "warn", "Open to run `goose configure`."),
|
||||
)
|
||||
|
||||
# Copilot — GitHub token (github-copilot-sdk extra is soft).
|
||||
if copilot_github_token_configured(config) or any(
|
||||
os.environ.get(v) for v in COPILOT_TOKEN_ENV_VARS
|
||||
):
|
||||
rows.append((COPILOT_KEY, "Copilot", "GitHub token", "ready", ""))
|
||||
elif not copilot_sdk_installed():
|
||||
rows.append(
|
||||
(
|
||||
COPILOT_KEY,
|
||||
"Copilot",
|
||||
"Not installed",
|
||||
"missing",
|
||||
_install_hint(COPILOT_EXTRA_INSTALL_COMMAND),
|
||||
),
|
||||
)
|
||||
else:
|
||||
rows.append(
|
||||
(
|
||||
COPILOT_KEY,
|
||||
"Copilot",
|
||||
"Not configured",
|
||||
"warn",
|
||||
"Open to add the GitHub token.",
|
||||
),
|
||||
)
|
||||
|
||||
# Kiro — native CLI, own auth via `kiro-cli login`.
|
||||
if harness_cli_installed(KIRO_KEY):
|
||||
rows.append((_KIRO, "Kiro", "Installed", "ready", "Sign in with `kiro-cli login`."))
|
||||
else:
|
||||
kiro_spec = harness_install_spec(KIRO_KEY)
|
||||
kiro_hint = _rich_escape(
|
||||
kiro_hint = (
|
||||
kiro_spec.install_hint
|
||||
if kiro_spec and kiro_spec.install_hint
|
||||
else "curl -fsSL https://cli.kiro.dev/install | bash"
|
||||
)
|
||||
kiro_sub = f"[dim]not installed — open to install ({kiro_hint})[/]"
|
||||
else:
|
||||
kiro_sub = "[green]✓[/] installed — sign in with `kiro-cli login`"
|
||||
options.append(f" {kiro_sub}")
|
||||
selectable.append(False)
|
||||
row_target.append(None)
|
||||
# Kimi Code (Moonshot AI's multi-provider CLI, no provider family — like
|
||||
# Cursor / Antigravity / Qwen). Auth lives entirely in the kimi CLI and
|
||||
# Omnigent stores no kimi credential, so "ready" is just whether the
|
||||
# binary is installed; the drill-in runs install + ``kimi login``. Kimi
|
||||
# has no status probe, so the overview can't claim "signed in" — it only
|
||||
# distinguishes installed vs. not.
|
||||
kimi_installed = harness_cli_installed(KIMI_KEY)
|
||||
options.append(f"{' ' if kimi_installed else '[red]✗[/] '}Kimi Code")
|
||||
selectable.append(True)
|
||||
row_target.append(_KIMI)
|
||||
if not kimi_installed:
|
||||
from rich.markup import escape as _rich_escape
|
||||
rows.append((_KIRO, "Kiro", "Not installed", "missing", _install_hint(kiro_hint)))
|
||||
|
||||
# Kimi is curl-installed (package=None), so use its install_hint —
|
||||
# ``harness_install_command`` raises ValueError for non-npm specs.
|
||||
_kimi_spec = harness_install_spec(KIMI_KEY)
|
||||
kimi_hint = (_kimi_spec.install_hint if _kimi_spec else None) or "see Kimi Code docs"
|
||||
kimi_cmd = _rich_escape(kimi_hint)
|
||||
kimi_sub = f"[dim]not installed — open to install ({kimi_cmd})[/]"
|
||||
# Kimi Code — native CLI, own auth via `kimi login`. Curl-installed
|
||||
# (no npm package), so use its install_hint.
|
||||
if harness_cli_installed(KIMI_KEY):
|
||||
rows.append((_KIMI, "Kimi Code", "Installed", "ready", "Sign in with `kimi login`."))
|
||||
else:
|
||||
kimi_sub = "[dim]installed — open to sign in (kimi login)[/]"
|
||||
options.append(f" {kimi_sub}")
|
||||
selectable.append(False)
|
||||
row_target.append(None)
|
||||
kimi_spec = harness_install_spec(KIMI_KEY)
|
||||
kimi_hint = (kimi_spec.install_hint if kimi_spec else None) or "see Kimi Code docs"
|
||||
rows.append((_KIMI, "Kimi Code", "Not installed", "missing", _install_hint(kimi_hint)))
|
||||
return rows
|
||||
|
||||
# Cap the status text so one verbose row (e.g. an OpenCode summary listing
|
||||
# several providers) can't run off a narrow terminal.
|
||||
max_status_width = 30
|
||||
|
||||
while True:
|
||||
config = _load_global_config()
|
||||
harness_rows = build_harness_rows()
|
||||
# Left-align the status into a single column a fixed gutter right of the
|
||||
# names, so every ✓/✗ glyph lines up vertically (a ragged right-aligned
|
||||
# status scattered the glyphs and read as messy). The name column is the
|
||||
# widest harness name + a 4-space gutter; the status is escaped when
|
||||
# interpolated into markup so a credential label containing a ``[`` can't
|
||||
# parse as a Rich tag (descriptions are escaped the same way).
|
||||
name_col = max(len(name) for _t, name, *_rest in harness_rows) + 4
|
||||
options: list[str] = []
|
||||
selectable: list[bool] = []
|
||||
row_target: list[str | None] = []
|
||||
descriptions: list[str] = []
|
||||
for target, name, status_text, kind, desc in harness_rows:
|
||||
if len(status_text) > max_status_width:
|
||||
status_text = status_text[: max_status_width - 1] + "…"
|
||||
glyph, color = status_styles[kind]
|
||||
options.append(f"{name.ljust(name_col)}[{color}]{glyph} {escape(status_text)}[/]")
|
||||
selectable.append(True)
|
||||
row_target.append(target)
|
||||
descriptions.append(desc)
|
||||
options.append("Quit")
|
||||
selectable.append(True)
|
||||
row_target.append(_QUIT)
|
||||
descriptions.append("")
|
||||
idx = select(
|
||||
"Configure harnesses",
|
||||
options,
|
||||
descriptions=descriptions,
|
||||
selectable=selectable,
|
||||
clear_on_exit=True,
|
||||
compact=True,
|
||||
)
|
||||
if idx < 0: # Esc / q — exit
|
||||
return
|
||||
|
||||
@@ -83,6 +83,7 @@ def _render_menu(
|
||||
status: str | None = None,
|
||||
max_visible: int | None = None,
|
||||
window_start: int = 0,
|
||||
compact: bool = False,
|
||||
) -> str:
|
||||
"""Render the menu frame to an ANSI string for the termios redraw.
|
||||
|
||||
@@ -120,7 +121,10 @@ def _render_menu(
|
||||
render_console.print(Text.from_markup(f" [bold green]{status}[/]"))
|
||||
render_console.print()
|
||||
render_console.print(Text.from_markup(f" [bold {ACCENT}]{title}[/]"))
|
||||
render_console.print()
|
||||
if not compact:
|
||||
# The compact overview hugs the title to the list (Hermes-style); other
|
||||
# menus keep a blank line below the title for breathing room.
|
||||
render_console.print()
|
||||
|
||||
# Optional scrolling viewport: when *max_visible* is set and the list is
|
||||
# longer, render only ``options[window_start : window_start + max_visible]``
|
||||
@@ -174,7 +178,15 @@ def _render_menu(
|
||||
render_console.print(Text.from_markup(f" [dim italic]{descriptions[selected]}[/]"))
|
||||
|
||||
render_console.print()
|
||||
render_console.print(Text.from_markup(f" [{MUTED}]↑/↓ move · Enter select · Esc back[/]"))
|
||||
# The compact overview is a top-level menu (Esc exits setup), so it shows a
|
||||
# navigate/select/exit hint in the spirit of other modern CLIs; nested menus
|
||||
# keep the "Esc back" wording, where Esc returns rather than exits.
|
||||
hint = (
|
||||
"↑/↓ navigate · Enter select · Esc to exit"
|
||||
if compact
|
||||
else "↑/↓ move · Enter select · Esc back"
|
||||
)
|
||||
render_console.print(Text.from_markup(f" [{MUTED}]{hint}[/]"))
|
||||
|
||||
return buf.getvalue()
|
||||
|
||||
@@ -301,6 +313,7 @@ def select(
|
||||
clear_on_exit: bool = False,
|
||||
status: str | None = None,
|
||||
max_visible: int | None = None,
|
||||
compact: bool = False,
|
||||
) -> int:
|
||||
"""Show a theme-picker-styled arrow-key menu and return the choice.
|
||||
|
||||
@@ -346,6 +359,11 @@ def select(
|
||||
cursor (with "N more" markers) so a long flat list fits one screen
|
||||
instead of overflowing and flickering. ``None`` renders every row.
|
||||
No-op on the numbered fallback.
|
||||
:param compact: When ``True`` (TTY only), render the dense top-level
|
||||
overview layout: the title hugs the list (no blank line below it) and
|
||||
the footer reads ``navigate · select · Esc to exit`` (Esc exits rather
|
||||
than goes back). Intended for the setup harness overview. No-op on the
|
||||
numbered fallback.
|
||||
:returns: The chosen zero-based index into *options* (always a
|
||||
selectable row), or ``-1`` when the user aborts — Esc / Ctrl-C /
|
||||
Ctrl-D on the TTY, or ``q`` on the numbered fallback.
|
||||
@@ -397,6 +415,7 @@ def select(
|
||||
status=status,
|
||||
max_visible=max_visible,
|
||||
window_start=window_start[0],
|
||||
compact=compact,
|
||||
)
|
||||
if prev_lines[0] > 0:
|
||||
sys.stdout.write(f"\033[{prev_lines[0]}A")
|
||||
|
||||
+330
-114
@@ -8,14 +8,15 @@ and the secret store (forced to the file backend via
|
||||
shape, not just the command's exit code, so a regression in the
|
||||
add/set-default/remove write paths surfaces here rather than silently.
|
||||
|
||||
``configure harnesses`` is a **three-level** picker. Level 1 picks a harness —
|
||||
the cursor moves between ``1=Claude``, ``2=Codex``, ``3=Pi``, ``4=Cursor``,
|
||||
``5=Antigravity``, and ``6=Quit`` (each
|
||||
harness's prominent default + ``+N more`` summary renders as non-selectable
|
||||
sub-lines that are skipped; a harness with no usable default — uninstalled or
|
||||
unconfigured — shows a red ✗, while a configured one carries no name-level
|
||||
marker). Selecting a
|
||||
harness drills into level 2 — its configured credentials, then ``+ Add a
|
||||
``configure harnesses`` is a **three-level** picker. Level 1 shows every
|
||||
harness on a single compact row — the name on the left, then an aligned
|
||||
``✓``/``✗`` status column — in 0.3 priority order: ``1=Claude``,
|
||||
``2=Codex``, ``3=Cursor``, ``4=OpenCode``, ``5=Hermes``, ``6=Pi``,
|
||||
``7=Antigravity``, ``8=Qwen Code``, ``9=Goose``, ``10=Copilot``, ``11=Kiro``,
|
||||
``12=Kimi Code``, ``13=Quit``. There is no "More" folding — every harness is
|
||||
visible at once — and the actionable hint (install command / next step)
|
||||
renders only for the highlighted row, as the selector's description line.
|
||||
Selecting a harness drills into level 2 — its configured credentials, then ``+ Add a
|
||||
credential`` and ``← Back``. So an empty harness's level 2 is ``1=+Add 2=Back``;
|
||||
with one credential it's ``1=<credential> 2=+Add 3=Back``. Selecting a
|
||||
credential opens level 3 — ``Make default`` (only when not already the
|
||||
@@ -1494,14 +1495,13 @@ def test_configure_harnesses_add_databricks_under_codex_scopes_to_codex(
|
||||
|
||||
|
||||
def test_uninstalled_harness_shows_x_and_not_installed(isolated_config, monkeypatch) -> None:
|
||||
"""A harness whose CLI isn't installed renders ✗ + a 'not installed yet' line.
|
||||
"""A harness whose CLI isn't installed renders a red ✗ "Not installed" status.
|
||||
|
||||
Overrides the installed-by-default fixture. The level-1 overview must mark
|
||||
the harness with ✗ (not ✓) and a sub-line flagging it as not installed —
|
||||
the first rung of the installed? → credential? → show-credential descent.
|
||||
The exact ``npm install`` command lives on drill-in
|
||||
(``_prompt_install_harness``), not the overview, so the hint parallels "no
|
||||
credential yet — open to add one" and stays short enough not to wrap.
|
||||
Overrides the installed-by-default fixture. The level-1 overview folds the
|
||||
readiness into the row's right-aligned status: an absent CLI reads
|
||||
``✗ Not installed`` inline (the exact install command is the selection-only
|
||||
description, surfaced when the row is highlighted, not in the always-visible
|
||||
row).
|
||||
"""
|
||||
monkeypatch.setattr(
|
||||
"omnigent.onboarding.harness_install.harness_cli_installed", lambda family: False
|
||||
@@ -1510,7 +1510,7 @@ def test_uninstalled_harness_shows_x_and_not_installed(isolated_config, monkeypa
|
||||
assert result.exit_code == 0, result.output
|
||||
out = result.output
|
||||
assert "✗" in out
|
||||
assert "not installed yet — open to install" in out
|
||||
assert "Not installed" in out
|
||||
|
||||
|
||||
def test_overview_marks_unconfigured_with_x_and_configured_without_checkmark(
|
||||
@@ -1520,10 +1520,10 @@ def test_overview_marks_unconfigured_with_x_and_configured_without_checkmark(
|
||||
|
||||
Seeds only an Anthropic (Claude) default and leaves Codex with no
|
||||
credential. The overview must (1) drop the old green ✓ next to the
|
||||
configured Claude name — the green ✓ now lives only on the summary default
|
||||
line — and (2) mark the unconfigured Codex with a red ✗ plus the "no
|
||||
credential yet" hint. A regression that restores the name-level ✓ or fails
|
||||
to flag the empty harness surfaces here.
|
||||
configured Claude name — the green ✓ now lives only on the status column —
|
||||
and (2) mark the installed-but-unconfigured Codex with a ✗ "Not configured"
|
||||
status. A regression that restores the name-level ✓ or fails to flag the
|
||||
empty harness surfaces here.
|
||||
"""
|
||||
config_path = os.path.join(isolated_config, "config.yaml")
|
||||
with open(config_path, "w") as f:
|
||||
@@ -1547,41 +1547,210 @@ def test_overview_marks_unconfigured_with_x_and_configured_without_checkmark(
|
||||
result = CliRunner().invoke(cli, ["setup", "--no-internal-beta"], input="q\n")
|
||||
assert result.exit_code == 0, result.output
|
||||
out = result.output
|
||||
# Configured Claude: NO name-level checkmark (the old "✓ Claude" is gone)…
|
||||
# Configured Claude: the green ✓ rides the right-aligned status, not the
|
||||
# name — so "✓ Claude" never appears, but the credential's "✓ …" does.
|
||||
assert "✓ Claude" not in out
|
||||
# …but the summary default line still carries its green ✓ on the credential.
|
||||
assert "✓ Anthropic API Key" in out
|
||||
# Unconfigured Codex: red ✗ on the name + the "no credential yet" hint.
|
||||
assert "✗ Codex" in out
|
||||
assert "no credential yet" in out
|
||||
# Installed-but-unconfigured Codex: the row's status is a ✗ "Not configured".
|
||||
assert "Codex" in out
|
||||
assert "✗ Not configured" in out
|
||||
|
||||
|
||||
def test_overview_lists_kiro_native_row(isolated_config, monkeypatch) -> None:
|
||||
"""Level 1: Kiro appears as an installable native harness row.
|
||||
def _capture_setup_overview(
|
||||
monkeypatch,
|
||||
) -> tuple[list[str], list[bool], list[str], bool]:
|
||||
"""Render the level-1 setup overview once and capture the menu it builds.
|
||||
|
||||
Monkeypatches the shared ``select`` so the picker records the rows it would
|
||||
draw, then returns ``-1`` (Esc) so setup exits after one frame. Returns the
|
||||
captured ``(options, selectable, descriptions, compact)`` — enough to assert
|
||||
the row set, ordering, single-line compactness, and the selection-only
|
||||
install hints without driving a real TTY.
|
||||
"""
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
def _capture_select(
|
||||
title: str,
|
||||
options: list[str],
|
||||
*,
|
||||
selectable: list[bool] | None = None,
|
||||
descriptions: list[str] | None = None,
|
||||
compact: bool = False,
|
||||
clear_on_exit: bool = False,
|
||||
**_kwargs: object,
|
||||
) -> int:
|
||||
assert title == "Configure harnesses"
|
||||
assert selectable is not None
|
||||
assert descriptions is not None
|
||||
captured.update(
|
||||
options=options,
|
||||
selectable=selectable,
|
||||
descriptions=descriptions,
|
||||
compact=compact,
|
||||
)
|
||||
return -1
|
||||
|
||||
monkeypatch.setattr("omnigent.onboarding.interactive.select", _capture_select)
|
||||
result = CliRunner().invoke(cli, ["setup", "--no-internal-beta"])
|
||||
assert result.exit_code == 0, result.output
|
||||
return (
|
||||
captured["options"], # type: ignore[return-value]
|
||||
captured["selectable"], # type: ignore[return-value]
|
||||
captured["descriptions"], # type: ignore[return-value]
|
||||
captured["compact"], # type: ignore[return-value]
|
||||
)
|
||||
|
||||
|
||||
def _overview_row_names(options: list[str], selectable: list[bool]) -> list[str]:
|
||||
"""Extract the harness / Quit names from a captured overview frame.
|
||||
|
||||
Each row label is ``"<name><padding>[<color>]<glyph> <status>[/]"``; the
|
||||
name is the text before the 2-space status gutter, recovered after Rich
|
||||
markup is stripped.
|
||||
"""
|
||||
import re
|
||||
|
||||
from rich.text import Text
|
||||
|
||||
names: list[str] = []
|
||||
for option, is_selectable in zip(options, selectable, strict=True):
|
||||
if not is_selectable:
|
||||
continue
|
||||
plain = Text.from_markup(option).plain
|
||||
names.append(re.split(r"\s{2,}", plain, maxsplit=1)[0].strip())
|
||||
return names
|
||||
|
||||
|
||||
def test_overview_lists_all_harnesses_in_priority_order(isolated_config, monkeypatch) -> None:
|
||||
"""The overview shows every harness on one compact row, in 0.3 priority order.
|
||||
|
||||
No "More" folding: all twelve harnesses are visible at once, followed by
|
||||
Quit. A regression that hides a harness, reorders the core six, or
|
||||
reintroduces a collapse row fails here. The menu also opts into the compact
|
||||
(underline-highlight) rendering.
|
||||
"""
|
||||
options, selectable, _descriptions, compact = _capture_setup_overview(monkeypatch)
|
||||
assert _overview_row_names(options, selectable) == [
|
||||
"Claude",
|
||||
"Codex",
|
||||
"Cursor",
|
||||
"OpenCode",
|
||||
"Hermes",
|
||||
"Pi",
|
||||
"Antigravity",
|
||||
"Qwen Code",
|
||||
"Goose",
|
||||
"Copilot",
|
||||
"Kiro",
|
||||
"Kimi Code",
|
||||
"Quit",
|
||||
]
|
||||
assert compact is True
|
||||
|
||||
|
||||
def test_overview_rows_are_single_line(isolated_config, monkeypatch) -> None:
|
||||
"""Every overview row is a single selectable line — no skipped sub-lines.
|
||||
|
||||
The compact layout folds each harness's status into its row (right-aligned)
|
||||
instead of a dim sub-line beneath it, so the cursor lands on every rendered
|
||||
row and each row carries a (possibly empty) description. A regression that
|
||||
brings back non-selectable sub-lines fails here.
|
||||
"""
|
||||
options, selectable, descriptions, _compact = _capture_setup_overview(monkeypatch)
|
||||
assert all(selectable)
|
||||
assert len(descriptions) == len(options)
|
||||
|
||||
|
||||
def test_overview_lists_kiro_row(isolated_config, monkeypatch) -> None:
|
||||
"""Kiro is a first-class harness row with its own status + selection hint.
|
||||
|
||||
Kiro (``kiro-native``) is a native CLI harness with its own auth
|
||||
(``kiro-cli login``), so — like Goose/Hermes — it belongs in the setup
|
||||
overview. Absent the CLI it renders a red ✗ plus its curl install hint;
|
||||
installed, it renders ready with the sign-in reminder. A regression that
|
||||
drops the Kiro row (the gap this PR closes) fails here.
|
||||
(``kiro-cli login``). Absent the CLI the row reads ``✗ Not installed`` and
|
||||
its selection-only description names the curl installer; installed, it reads
|
||||
``✓ Installed`` with the sign-in reminder. A regression that drops the Kiro
|
||||
row fails here.
|
||||
"""
|
||||
# CLI absent → Kiro row + the curl install hint. (The red ✗ marker carries an
|
||||
# ANSI reset between the glyph and the name, so assert on the stable text.)
|
||||
monkeypatch.setattr(
|
||||
"omnigent.onboarding.harness_install.harness_cli_installed",
|
||||
lambda family: False,
|
||||
)
|
||||
out = CliRunner().invoke(cli, ["setup", "--no-internal-beta"], input="q\n").output
|
||||
assert "Kiro" in out
|
||||
assert "cli.kiro.dev/install" in out
|
||||
from rich.text import Text
|
||||
|
||||
# CLI present → ready row naming the sign-in step (no Omnigent credential).
|
||||
monkeypatch.setattr(
|
||||
"omnigent.onboarding.harness_install.harness_cli_installed",
|
||||
lambda family: True,
|
||||
"omnigent.onboarding.harness_install.harness_cli_installed", lambda family: False
|
||||
)
|
||||
out = CliRunner().invoke(cli, ["setup", "--no-internal-beta"], input="q\n").output
|
||||
assert "kiro-cli login" in out
|
||||
options, selectable, descriptions, _ = _capture_setup_overview(monkeypatch)
|
||||
names = _overview_row_names(options, selectable)
|
||||
kiro = names.index("Kiro")
|
||||
assert "Not installed" in Text.from_markup(options[kiro]).plain
|
||||
assert "cli.kiro.dev/install" in Text.from_markup(descriptions[kiro]).plain
|
||||
|
||||
monkeypatch.setattr(
|
||||
"omnigent.onboarding.harness_install.harness_cli_installed", lambda family: True
|
||||
)
|
||||
options, selectable, descriptions, _ = _capture_setup_overview(monkeypatch)
|
||||
names = _overview_row_names(options, selectable)
|
||||
kiro = names.index("Kiro")
|
||||
assert "Installed" in Text.from_markup(options[kiro]).plain
|
||||
assert "kiro-cli login" in Text.from_markup(descriptions[kiro]).plain
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"choice,manager_attr",
|
||||
[
|
||||
("4", "_manage_opencode_harness"),
|
||||
("5", "_manage_hermes_harness"),
|
||||
("8", "_manage_qwen_harness"),
|
||||
("9", "_manage_goose_harness"),
|
||||
("10", "_manage_copilot_harness"),
|
||||
("11", "_manage_kiro_harness"),
|
||||
("12", "_manage_kimi_harness"),
|
||||
],
|
||||
)
|
||||
def test_overview_dispatches_to_correct_manager(
|
||||
isolated_config, monkeypatch, choice: str, manager_attr: str
|
||||
) -> None:
|
||||
"""Selecting a harness routes to its drill-in, pinning position→sentinel→manager.
|
||||
|
||||
The ordering test asserts row *names* only, so a copy-paste slip that paired
|
||||
the wrong sentinel with a name (e.g. ``(_QWEN, "Goose", …)``) would route
|
||||
"Goose" to ``_manage_qwen_harness`` yet still pass the name check. This drives
|
||||
the real numbered-fallback dispatch end-to-end for the seven harnesses whose
|
||||
positions no other scripted-stdin test exercises (Claude/Codex/Cursor/Pi/
|
||||
Antigravity are covered by the add/remove/key tests), so a misrouted row is
|
||||
caught here.
|
||||
"""
|
||||
called: list[str] = []
|
||||
monkeypatch.setattr(
|
||||
f"omnigent.cli.{manager_attr}", lambda *a, **k: called.append(manager_attr)
|
||||
)
|
||||
result = CliRunner().invoke(cli, ["setup", "--no-internal-beta"], input=f"{choice}\nq\n")
|
||||
assert result.exit_code == 0, result.output
|
||||
assert called == [manager_attr]
|
||||
|
||||
|
||||
def test_overview_status_color_distinguishes_missing_from_unconfigured(
|
||||
isolated_config, monkeypatch
|
||||
) -> None:
|
||||
"""The ✗ status color encodes *absent* (red) vs *installed-but-unconfigured* (yellow).
|
||||
|
||||
The kind taxonomy (``missing`` → red, ``warn`` → yellow, ``ready`` → green)
|
||||
is the whole point of the status column, but the other overview tests assert
|
||||
only the glyph + text. Here we capture the raw row markup (pre-render) and
|
||||
pin the color so a regression that, say, paints an absent CLI yellow (telling
|
||||
a user a missing tool is merely "unconfigured") fails.
|
||||
"""
|
||||
# Installed but unconfigured → yellow ✗ (a usable harness awaiting setup).
|
||||
monkeypatch.setattr(
|
||||
"omnigent.onboarding.harness_install.harness_cli_installed", lambda family: True
|
||||
)
|
||||
options, selectable, _descriptions, _compact = _capture_setup_overview(monkeypatch)
|
||||
codex = options[_overview_row_names(options, selectable).index("Codex")]
|
||||
assert "[yellow]✗ Not configured[/]" in codex
|
||||
|
||||
# CLI absent → red ✗ (nothing to use yet).
|
||||
monkeypatch.setattr(
|
||||
"omnigent.onboarding.harness_install.harness_cli_installed", lambda family: False
|
||||
)
|
||||
options, selectable, _descriptions, _compact = _capture_setup_overview(monkeypatch)
|
||||
codex = options[_overview_row_names(options, selectable).index("Codex")]
|
||||
assert "[red]✗ Not installed[/]" in codex
|
||||
|
||||
|
||||
def test_drill_into_uninstalled_installs_then_proceeds(isolated_config, monkeypatch) -> None:
|
||||
@@ -1690,9 +1859,9 @@ def test_configure_harnesses_pi_page_sets_explicit_pi_default(isolated_config) -
|
||||
f,
|
||||
)
|
||||
|
||||
# L1 3=Pi → L2 (1=anthropic 2=openai 3=+Add): select openai (2) → L3
|
||||
# L1 6=Pi → L2 (1=anthropic 2=openai 3=+Add): select openai (2) → L3
|
||||
# 1=Make default for Pi → back to L2 q=back → L1 q=exit.
|
||||
stdin = "\n".join(["3", "2", "1", "q", "q"]) + "\n"
|
||||
stdin = "\n".join(["6", "2", "1", "q", "q"]) + "\n"
|
||||
result = CliRunner().invoke(cli, ["setup", "--no-internal-beta"], input=stdin)
|
||||
assert result.exit_code == 0, result.output
|
||||
|
||||
@@ -1742,9 +1911,9 @@ def test_configure_harnesses_pi_page_excludes_subscription_rows(isolated_config)
|
||||
f,
|
||||
)
|
||||
|
||||
# L1 3=Pi → L2 renders its rows → q=back → L1 q=exit. The L2 frame is
|
||||
# L1 6=Pi → L2 renders its rows → q=back → L1 q=exit. The L2 frame is
|
||||
# cleared on exit under a TTY but the numbered fallback echoes options.
|
||||
stdin = "\n".join(["3", "q", "q"]) + "\n"
|
||||
stdin = "\n".join(["6", "q", "q"]) + "\n"
|
||||
result = CliRunner().invoke(cli, ["setup", "--no-internal-beta"], input=stdin)
|
||||
assert result.exit_code == 0, result.output
|
||||
|
||||
@@ -1790,8 +1959,8 @@ def test_configure_harnesses_add_databricks_under_pi_scopes_to_pi(
|
||||
# Databricks position within the Pi add menu, computed live.
|
||||
pi_opts = add_menu_options_for_family(PI_SURFACE)
|
||||
db = next(i for i, o in enumerate(pi_opts) if o.kind == DATABRICKS_KIND) + 1
|
||||
# L1 3=Pi → L2 1=+Add → add menu <db>=Databricks → URL → q → q.
|
||||
stdin = "\n".join(["3", "1", str(db), "https://example.cloud.databricks.com", "q", "q"]) + "\n"
|
||||
# L1 6=Pi → L2 1=+Add → add menu <db>=Databricks → URL → q → q.
|
||||
stdin = "\n".join(["6", "1", str(db), "https://example.cloud.databricks.com", "q", "q"]) + "\n"
|
||||
result = CliRunner().invoke(cli, ["setup", "--no-internal-beta"], input=stdin)
|
||||
assert result.exit_code == 0, result.output
|
||||
|
||||
@@ -2041,9 +2210,9 @@ def test_cursor_set_api_key_paste_writes_block_and_secret(
|
||||
Proves the api-key path: the secret lands in the store (never plaintext in
|
||||
config) and the config references it via ``keychain:cursor``.
|
||||
"""
|
||||
# L1 4=Cursor → cursor menu 1=Set API key → paste key (crsr_ → no warn) →
|
||||
# L1 3=Cursor → cursor menu 1=Set API key → paste key (crsr_ → no warn) →
|
||||
# cursor menu q=back → L1 q=quit.
|
||||
stdin = "\n".join(["4", "1", "crsr_test_key_123", "q", "q"]) + "\n"
|
||||
stdin = "\n".join(["3", "1", "crsr_test_key_123", "q", "q"]) + "\n"
|
||||
result = CliRunner().invoke(cli, ["setup", "--no-internal-beta"], input=stdin)
|
||||
assert result.exit_code == 0, result.output
|
||||
|
||||
@@ -2063,9 +2232,9 @@ def test_cursor_adopt_env_api_key_writes_env_ref(
|
||||
at the live environment variable so the key never leaves the user's shell.
|
||||
"""
|
||||
monkeypatch.setenv("CURSOR_API_KEY", "crsr_env_key_456")
|
||||
# L1 4=Cursor → 1=Set API key → "y" adopt detected $CURSOR_API_KEY →
|
||||
# L1 3=Cursor → 1=Set API key → "y" adopt detected $CURSOR_API_KEY →
|
||||
# q back → q quit.
|
||||
stdin = "\n".join(["4", "1", "y", "q", "q"]) + "\n"
|
||||
stdin = "\n".join(["3", "1", "y", "q", "q"]) + "\n"
|
||||
result = CliRunner().invoke(cli, ["setup", "--no-internal-beta"], input=stdin)
|
||||
assert result.exit_code == 0, result.output
|
||||
|
||||
@@ -2084,9 +2253,9 @@ def test_cursor_remove_api_key_drops_block_and_secret(
|
||||
with open(config_path, "w") as f:
|
||||
yaml.safe_dump({"cursor": {"api_key_ref": "keychain:cursor"}}, f)
|
||||
|
||||
# L1 4=Cursor → cursor menu (key set: 1=Replace 2=Remove 3=Back) → 2=Remove
|
||||
# L1 3=Cursor → cursor menu (key set: 1=Replace 2=Remove 3=Back) → 2=Remove
|
||||
# → q back → q quit.
|
||||
stdin = "\n".join(["4", "2", "q", "q"]) + "\n"
|
||||
stdin = "\n".join(["3", "2", "q", "q"]) + "\n"
|
||||
result = CliRunner().invoke(cli, ["setup", "--no-internal-beta"], input=stdin)
|
||||
assert result.exit_code == 0, result.output
|
||||
|
||||
@@ -2103,9 +2272,9 @@ def test_cursor_set_api_key_non_crsr_declined_is_not_stored(
|
||||
The soft prefix check warns and asks to store anyway; declining must leave
|
||||
both the secret store and the config untouched.
|
||||
"""
|
||||
# L1 4=Cursor → 1=Set API key → paste non-crsr_ key → "n" decline warning →
|
||||
# L1 3=Cursor → 1=Set API key → paste non-crsr_ key → "n" decline warning →
|
||||
# q back → q quit.
|
||||
stdin = "\n".join(["4", "1", "sk-not-a-cursor-key", "n", "q", "q"]) + "\n"
|
||||
stdin = "\n".join(["3", "1", "sk-not-a-cursor-key", "n", "q", "q"]) + "\n"
|
||||
result = CliRunner().invoke(cli, ["setup", "--no-internal-beta"], input=stdin)
|
||||
assert result.exit_code == 0, result.output
|
||||
|
||||
@@ -2135,20 +2304,24 @@ def _cursor_sdk_absent(monkeypatch):
|
||||
)
|
||||
|
||||
|
||||
def test_cursor_overview_surfaces_install_command_when_sdk_missing(
|
||||
isolated_config, _cursor_sdk_absent
|
||||
def test_cursor_overview_install_command_is_selection_only(
|
||||
isolated_config, _cursor_sdk_absent, monkeypatch
|
||||
) -> None:
|
||||
"""L1 overview: the Cursor row names the extra install command when absent.
|
||||
"""With the cursor extra absent, the Cursor row's install command is its description.
|
||||
|
||||
The exact ``pip install "omnigent[cursor]"`` is shown (escaped so the
|
||||
literal brackets render).
|
||||
The exact ``pip install "omnigent[cursor]"`` (brackets included) is the
|
||||
selection-only hint — the selector's per-row description, shown when the row
|
||||
is highlighted — and is NOT baked into the always-visible row label. This is
|
||||
the "tooltip only on selection" behavior.
|
||||
"""
|
||||
result = CliRunner().invoke(cli, ["setup", "--no-internal-beta"], input="q\n")
|
||||
assert result.exit_code == 0, result.output
|
||||
out = result.output
|
||||
assert "not installed — open to install" in out
|
||||
# The literal command (brackets included) reaches the rendered output.
|
||||
assert 'pip install "omnigent[cursor]"' in out
|
||||
from rich.text import Text
|
||||
|
||||
options, selectable, descriptions, _ = _capture_setup_overview(monkeypatch)
|
||||
names = _overview_row_names(options, selectable)
|
||||
cursor = names.index("Cursor")
|
||||
assert 'pip install "omnigent[cursor]"' in Text.from_markup(descriptions[cursor]).plain
|
||||
# The command lives in the description only — never the always-visible row.
|
||||
assert "pip install" not in Text.from_markup(options[cursor]).plain
|
||||
|
||||
|
||||
def test_cursor_drillin_offers_install_when_sdk_missing(
|
||||
@@ -2159,8 +2332,8 @@ def test_cursor_drillin_offers_install_when_sdk_missing(
|
||||
Here the user picks "show the command" (choice 3), which prints it and falls
|
||||
through to the key menu, then backs out.
|
||||
"""
|
||||
# L1 4=Cursor → install offer 3=show command → key menu q=back → L1 q.
|
||||
stdin = "\n".join(["4", "3", "q", "q"]) + "\n"
|
||||
# L1 3=Cursor → install offer 3=show command → key menu q=back → L1 q.
|
||||
stdin = "\n".join(["3", "3", "q", "q"]) + "\n"
|
||||
result = CliRunner().invoke(cli, ["setup", "--no-internal-beta"], input=stdin)
|
||||
assert result.exit_code == 0, result.output
|
||||
out = result.output
|
||||
@@ -2175,9 +2348,9 @@ def test_cursor_key_settable_when_sdk_missing(isolated_config, _cursor_sdk_absen
|
||||
NOT gate key management on it. Here the user declines ("set the key anyway" =
|
||||
choice 2), then sets the key — which must persist as it does with the SDK.
|
||||
"""
|
||||
# L1 4=Cursor → install offer 2=set key anyway → key menu 1=Set →
|
||||
# L1 3=Cursor → install offer 2=set key anyway → key menu 1=Set →
|
||||
# paste crsr_ key → key menu q=back → L1 q=quit.
|
||||
stdin = "\n".join(["4", "2", "1", "crsr_key_no_sdk", "q", "q"]) + "\n"
|
||||
stdin = "\n".join(["3", "2", "1", "crsr_key_no_sdk", "q", "q"]) + "\n"
|
||||
result = CliRunner().invoke(cli, ["setup", "--no-internal-beta"], input=stdin)
|
||||
assert result.exit_code == 0, result.output
|
||||
|
||||
@@ -2205,8 +2378,8 @@ def test_cursor_install_now_invokes_runner_without_index(
|
||||
monkeypatch.setattr("omnigent.onboarding.cursor_auth.shutil.which", lambda name: None)
|
||||
monkeypatch.setattr("omnigent.onboarding.cursor_auth.subprocess.run", _run)
|
||||
|
||||
# L1 4=Cursor → install offer 1=install now → key menu q=back → L1 q.
|
||||
stdin = "\n".join(["4", "1", "q", "q"]) + "\n"
|
||||
# L1 3=Cursor → install offer 1=install now → key menu q=back → L1 q.
|
||||
stdin = "\n".join(["3", "1", "q", "q"]) + "\n"
|
||||
result = CliRunner().invoke(cli, ["setup", "--no-internal-beta"], input=stdin)
|
||||
assert result.exit_code == 0, result.output
|
||||
|
||||
@@ -2219,10 +2392,10 @@ def test_cursor_install_now_invokes_runner_without_index(
|
||||
|
||||
|
||||
# ── Antigravity Gemini API-key flow ─────────────────────────────────────────
|
||||
# Antigravity (Gemini-native, no provider family) drills in at L1 row 5
|
||||
# (Claude/Codex/Pi/Antigravity/Quit) and stores its key in the secret store +
|
||||
# the ``antigravity:`` config block. API-key-only menu (Set/Replace/Remove);
|
||||
# ``isolated_config`` clears ambient GEMINI_API_KEY / ANTIGRAVITY_API_KEY.
|
||||
# Antigravity (Gemini-native, no provider family) is row 7 on the overview (it
|
||||
# follows Pi) and stores its key in the secret store + the ``antigravity:``
|
||||
# config block. API-key-only menu (Set/Replace/Remove); ``isolated_config``
|
||||
# clears ambient GEMINI_API_KEY / ANTIGRAVITY_API_KEY.
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
@@ -2252,9 +2425,9 @@ def test_antigravity_set_api_key_paste_writes_block_and_secret(
|
||||
Proves the api-key path: the secret lands in the store (never plaintext in
|
||||
config) and the config references it via ``keychain:antigravity``.
|
||||
"""
|
||||
# L1 5=Antigravity → antigravity menu 1=Set API key → paste key (AIza → no
|
||||
# warn) → antigravity menu q=back → L1 q=quit.
|
||||
stdin = "\n".join(["5", "1", "AIza_test_key_123", "q", "q"]) + "\n"
|
||||
# L1 7=Antigravity → antigravity menu 1=Set API key →
|
||||
# paste key (AIza → no warn) → antigravity menu q=back → L1 q=quit.
|
||||
stdin = "\n".join(["7", "1", "AIza_test_key_123", "q", "q"]) + "\n"
|
||||
result = CliRunner().invoke(cli, ["setup", "--no-internal-beta"], input=stdin)
|
||||
assert result.exit_code == 0, result.output
|
||||
|
||||
@@ -2274,9 +2447,9 @@ def test_antigravity_adopt_env_api_key_writes_env_ref(
|
||||
at the live environment variable so the key never leaves the user's shell.
|
||||
"""
|
||||
monkeypatch.setenv("GEMINI_API_KEY", "AIza_env_key_456")
|
||||
# L1 5=Antigravity → 1=Set API key → "y" adopt detected $GEMINI_API_KEY →
|
||||
# q back → q quit.
|
||||
stdin = "\n".join(["5", "1", "y", "q", "q"]) + "\n"
|
||||
# L1 7=Antigravity → 1=Set API key →
|
||||
# "y" adopt detected $GEMINI_API_KEY → q back → q quit.
|
||||
stdin = "\n".join(["7", "1", "y", "q", "q"]) + "\n"
|
||||
result = CliRunner().invoke(cli, ["setup", "--no-internal-beta"], input=stdin)
|
||||
assert result.exit_code == 0, result.output
|
||||
|
||||
@@ -2295,9 +2468,9 @@ def test_antigravity_remove_api_key_drops_block_and_secret(
|
||||
with open(config_path, "w") as f:
|
||||
yaml.safe_dump({"antigravity": {"api_key_ref": "keychain:antigravity"}}, f)
|
||||
|
||||
# L1 5=Antigravity → antigravity menu (key set: 1=Replace 2=Remove 3=Back) →
|
||||
# 2=Remove → q back → q quit.
|
||||
stdin = "\n".join(["5", "2", "q", "q"]) + "\n"
|
||||
# L1 7=Antigravity → antigravity menu (key set:
|
||||
# 1=Replace 2=Remove 3=Back) → 2=Remove → q back → q quit.
|
||||
stdin = "\n".join(["7", "2", "q", "q"]) + "\n"
|
||||
result = CliRunner().invoke(cli, ["setup", "--no-internal-beta"], input=stdin)
|
||||
assert result.exit_code == 0, result.output
|
||||
|
||||
@@ -2322,8 +2495,9 @@ def test_antigravity_remove_does_not_delete_foreign_keychain_secret(
|
||||
with open(config_path, "w") as f:
|
||||
yaml.safe_dump({"antigravity": {"api_key_ref": "keychain:shared-gemini"}}, f)
|
||||
|
||||
# L1 5=Antigravity → antigravity menu 2=Remove → q back → q quit.
|
||||
stdin = "\n".join(["5", "2", "q", "q"]) + "\n"
|
||||
# L1 7=Antigravity → antigravity menu 2=Remove →
|
||||
# q back → q quit.
|
||||
stdin = "\n".join(["7", "2", "q", "q"]) + "\n"
|
||||
result = CliRunner().invoke(cli, ["setup", "--no-internal-beta"], input=stdin)
|
||||
assert result.exit_code == 0, result.output
|
||||
|
||||
@@ -2341,9 +2515,9 @@ def test_antigravity_set_api_key_non_aiza_declined_is_not_stored(
|
||||
The soft prefix check warns and asks to store anyway; declining must leave
|
||||
both the secret store and the config untouched.
|
||||
"""
|
||||
# L1 5=Antigravity → 1=Set API key → paste non-AIza key → "n" decline
|
||||
# warning → q back → q quit.
|
||||
stdin = "\n".join(["5", "1", "sk-not-a-gemini-key", "n", "q", "q"]) + "\n"
|
||||
# L1 7=Antigravity → 1=Set API key →
|
||||
# paste non-AIza key → "n" decline warning → q back → q quit.
|
||||
stdin = "\n".join(["7", "1", "sk-not-a-gemini-key", "n", "q", "q"]) + "\n"
|
||||
result = CliRunner().invoke(cli, ["setup", "--no-internal-beta"], input=stdin)
|
||||
assert result.exit_code == 0, result.output
|
||||
|
||||
@@ -2374,20 +2548,60 @@ def _antigravity_sdk_absent(monkeypatch):
|
||||
)
|
||||
|
||||
|
||||
def test_antigravity_overview_surfaces_install_command_when_sdk_missing(
|
||||
isolated_config, _antigravity_sdk_absent
|
||||
def test_antigravity_overview_install_command_is_selection_only(
|
||||
isolated_config, _antigravity_sdk_absent, monkeypatch
|
||||
) -> None:
|
||||
"""L1 overview: the Antigravity row names the extra install command when absent.
|
||||
"""With the antigravity extra absent, the Antigravity row's install command is its description.
|
||||
|
||||
The exact ``pip install "omnigent[antigravity]"`` is shown (escaped so the literal
|
||||
brackets render). Without the SDK-detection branch this line never appears.
|
||||
The exact ``pip install "omnigent[antigravity]"`` (brackets included) is the
|
||||
selection-only hint — the selector's per-row description — not baked into the
|
||||
always-visible row. Without the SDK-detection branch the hint never appears.
|
||||
"""
|
||||
result = CliRunner().invoke(cli, ["setup", "--no-internal-beta"], input="q\n")
|
||||
assert result.exit_code == 0, result.output
|
||||
out = result.output
|
||||
assert "not installed — open to install" in out
|
||||
# The literal command (brackets included) reaches the rendered output.
|
||||
assert 'pip install "omnigent[antigravity]"' in out
|
||||
from rich.text import Text
|
||||
|
||||
options, selectable, descriptions, _ = _capture_setup_overview(monkeypatch)
|
||||
names = _overview_row_names(options, selectable)
|
||||
antigravity = names.index("Antigravity")
|
||||
assert (
|
||||
'pip install "omnigent[antigravity]"' in Text.from_markup(descriptions[antigravity]).plain
|
||||
)
|
||||
assert "pip install" not in Text.from_markup(options[antigravity]).plain
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def _copilot_sdk_absent(monkeypatch):
|
||||
"""Force the ``github-copilot-sdk`` extra absent and clear ambient Copilot tokens.
|
||||
|
||||
Copilot is the third soft-SDK-extra harness (like Cursor / Antigravity): its
|
||||
readiness is a GitHub token, and a missing SDK is surfaced as an install
|
||||
hint rather than a hard block. This drives the unconfigured + SDK-absent
|
||||
state so the overview row reads "Not installed" with the extra's install
|
||||
command as its selection-only description.
|
||||
|
||||
:param monkeypatch: Pytest monkeypatch fixture.
|
||||
"""
|
||||
for var in ("COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"):
|
||||
monkeypatch.delenv(var, raising=False)
|
||||
monkeypatch.setattr("omnigent.onboarding.copilot_auth.copilot_sdk_installed", lambda: False)
|
||||
|
||||
|
||||
def test_copilot_overview_install_command_is_selection_only(
|
||||
isolated_config, _copilot_sdk_absent, monkeypatch
|
||||
) -> None:
|
||||
"""With the copilot extra absent, the Copilot row's install command is its description.
|
||||
|
||||
Mirrors the Cursor / Antigravity selection-only-hint contract for the third
|
||||
soft-SDK-extra harness: the ``omnigent[copilot]`` install command is the
|
||||
per-row description (shown only when highlighted), never baked into the
|
||||
always-visible row label.
|
||||
"""
|
||||
from rich.text import Text
|
||||
|
||||
options, selectable, descriptions, _ = _capture_setup_overview(monkeypatch)
|
||||
names = _overview_row_names(options, selectable)
|
||||
copilot = names.index("Copilot")
|
||||
assert "omnigent[copilot]" in Text.from_markup(descriptions[copilot]).plain
|
||||
assert "pip install" not in Text.from_markup(options[copilot]).plain
|
||||
|
||||
|
||||
def test_antigravity_drillin_offers_install_when_sdk_missing(
|
||||
@@ -2398,8 +2612,9 @@ def test_antigravity_drillin_offers_install_when_sdk_missing(
|
||||
The user picks "show the command" (choice 3), which prints the command and falls
|
||||
through to the key menu, then backs out.
|
||||
"""
|
||||
# L1 5=Antigravity → install offer 3=show command → key menu q=back → L1 q.
|
||||
stdin = "\n".join(["5", "3", "q", "q"]) + "\n"
|
||||
# L1 7=Antigravity → install offer 3=show command →
|
||||
# key menu q=back → L1 q.
|
||||
stdin = "\n".join(["7", "3", "q", "q"]) + "\n"
|
||||
result = CliRunner().invoke(cli, ["setup", "--no-internal-beta"], input=stdin)
|
||||
assert result.exit_code == 0, result.output
|
||||
out = result.output
|
||||
@@ -2416,9 +2631,9 @@ def test_antigravity_key_settable_when_sdk_missing(
|
||||
gate key management on it. The user declines ("set the key anyway" = choice 2),
|
||||
then sets the key, which must persist as it does with the SDK present.
|
||||
"""
|
||||
# L1 5=Antigravity → install offer 2=set key anyway → key menu 1=Set →
|
||||
# paste AIza key → key menu q=back → L1 q=quit.
|
||||
stdin = "\n".join(["5", "2", "1", "AIza_key_no_sdk", "q", "q"]) + "\n"
|
||||
# L1 7=Antigravity → install offer 2=set key anyway →
|
||||
# key menu 1=Set → paste AIza key → key menu q=back → L1 q=quit.
|
||||
stdin = "\n".join(["7", "2", "1", "AIza_key_no_sdk", "q", "q"]) + "\n"
|
||||
result = CliRunner().invoke(cli, ["setup", "--no-internal-beta"], input=stdin)
|
||||
assert result.exit_code == 0, result.output
|
||||
|
||||
@@ -2446,8 +2661,9 @@ def test_antigravity_install_now_invokes_runner_without_index(
|
||||
monkeypatch.setattr("omnigent.onboarding.antigravity_auth.shutil.which", lambda name: None)
|
||||
monkeypatch.setattr("omnigent.onboarding.antigravity_auth.subprocess.run", _run)
|
||||
|
||||
# L1 5=Antigravity → install offer 1=install now → key menu q=back → L1 q.
|
||||
stdin = "\n".join(["5", "1", "q", "q"]) + "\n"
|
||||
# L1 7=Antigravity → install offer 1=install now →
|
||||
# key menu q=back → L1 q.
|
||||
stdin = "\n".join(["7", "1", "q", "q"]) + "\n"
|
||||
result = CliRunner().invoke(cli, ["setup", "--no-internal-beta"], input=stdin)
|
||||
assert result.exit_code == 0, result.output
|
||||
|
||||
@@ -2501,8 +2717,8 @@ def test_configure_harnesses_add_other_key_no_remaining_providers_aborts_cleanly
|
||||
monkeypatch.setattr("omnigent.onboarding.configure_models.other_key_providers", list)
|
||||
|
||||
other = _other_key_add_menu_index(PI_SURFACE)
|
||||
# L1 3=Pi → L2 1=+Add → add menu <other>=Other provider — API key → L2 q=back → L1 q=exit.
|
||||
stdin = "\n".join(["3", "1", str(other), "q", "q"]) + "\n"
|
||||
# L1 6=Pi → L2 1=+Add → add menu <other>=Other provider — API key → L2 q=back → L1 q=exit.
|
||||
stdin = "\n".join(["6", "1", str(other), "q", "q"]) + "\n"
|
||||
result = CliRunner().invoke(cli, ["setup", "--no-internal-beta"], input=stdin)
|
||||
|
||||
# Pre-fix this exited non-zero with a ValueError; the guard makes it graceful.
|
||||
|
||||
Reference in New Issue
Block a user