Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 36 additions & 13 deletions src/ucode/agents/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from ucode.telemetry import agent_version
from ucode.ui import (
console,
is_low_verbosity,
print_err,
print_note,
print_section,
Expand Down Expand Up @@ -115,7 +116,13 @@ def _confirm_update_installed_tool_binary(tool: str) -> bool:
return prompt_yes_no(f"(Optional) Update {spec['display']} from {current} to {latest}?")


def install_tool_binary(tool: str, *, strict: bool = True, update_existing: bool = False) -> bool:
def install_tool_binary(
tool: str,
*,
strict: bool = True,
update_existing: bool = False,
prompt_optional_updates: bool = True,
) -> bool:
spec = TOOL_SPECS[tool]
binary = spec["binary"]
package = spec["package"]
Expand All @@ -124,10 +131,12 @@ def install_tool_binary(tool: str, *, strict: bool = True, update_existing: bool
if update_existing:
required_update = _required_update_message(tool)
if required_update:
# Required updates are forced regardless of prompt preference;
# the tool won't function on an unsupported version.
print_warning(required_update)
if not _update_installed_tool_binary(tool):
raise RuntimeError(_minimum_version_error(tool) or required_update)
elif _confirm_update_installed_tool_binary(tool):
elif prompt_optional_updates and _confirm_update_installed_tool_binary(tool):
_update_installed_tool_binary(tool)

version_error = _minimum_version_error(tool)
Expand Down Expand Up @@ -175,9 +184,19 @@ def ensure_tool_binary_available(tool: str) -> None:
)


def ensure_bootstrap_dependencies(tool: str, *, update_existing: bool = False) -> None:
def ensure_bootstrap_dependencies(
tool: str,
*,
update_existing: bool = False,
prompt_optional_updates: bool = True,
) -> None:
install_databricks_cli()
install_tool_binary(tool, strict=True, update_existing=update_existing)
install_tool_binary(
tool,
strict=True,
update_existing=update_existing,
prompt_optional_updates=prompt_optional_updates,
)


def default_model_for_tool(tool: str, state: dict) -> str | None:
Expand Down Expand Up @@ -389,15 +408,19 @@ def validate_all_tools(state: dict) -> None:
from ucode.agents.pi import PI_SETTINGS_BACKUP_PATH, PI_SETTINGS_PATH
from ucode.config_io import restore_file

low_verbosity = is_low_verbosity()
console.print()
console.print(
Panel(
"Testing each tool with a quick message...",
title="Validating",
style="bold blue",
expand=False,
if low_verbosity:
console.print("[bold blue]Validating...[/bold blue]")
else:
console.print(
Panel(
"Testing each tool with a quick message...",
title="Validating",
style="bold blue",
expand=False,
)
)
)
results: list[tuple[str, bool]] = []
available_tools = list(state.get("available_tools") or [])
for tool, spec in TOOL_SPECS.items():
Expand All @@ -419,9 +442,9 @@ def validate_all_tools(state: dict) -> None:
state["available_tools"] = available_tools
save_state(state)

console.print()
success_tools = [(t, s) for t, s in results if s]
if success_tools:
if success_tools and not low_verbosity:
console.print()
lines = []
for tool, _ in success_tools:
spec = TOOL_SPECS[tool]
Expand Down
55 changes: 49 additions & 6 deletions src/ucode/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
print_success,
prompt_for_tools,
prompt_for_workspace,
set_verbosity,
spinner,
status_badge,
)
Expand Down Expand Up @@ -252,6 +253,8 @@ def configure_workspace_command(
tool: str | None = None,
selected_tools: list[str] | None = None,
workspaces: list[tuple[str, str | None]] | None = None,
*,
prompt_optional_updates: bool = True,
) -> int:
if tool is not None and selected_tools is not None:
raise RuntimeError("Use either --agent or --agents, not both.")
Expand Down Expand Up @@ -321,7 +324,12 @@ def configure_workspace_command(
return 0

for tool_name in picked:
install_tool_binary(tool_name, strict=False, update_existing=True)
install_tool_binary(
tool_name,
strict=False,
update_existing=True,
prompt_optional_updates=prompt_optional_updates,
)

state = configure_selected_tools(state, picked)

Expand Down Expand Up @@ -618,38 +626,73 @@ def configure(
help="Also enable MLflow tracing for the configured workspace(s).",
),
] = False,
skip_upgrade: Annotated[
bool,
typer.Option(
"--skip-upgrade",
help="Don't prompt to upgrade already-installed agent CLIs to a newer version. "
"Required updates (when an agent is below its minimum supported version) are "
"still applied.",
),
] = False,
verbose: Annotated[
str,
typer.Option(
"--verbose",
help="Output verbosity: 'normal' (default) renders decorative panels; "
"'low' prints terse single-line status instead.",
),
] = "normal",
) -> None:
"""Configure workspace URL and AI Gateway."""
if ctx.invoked_subcommand is not None:
return
if verbose not in ("normal", "low"):
print_err("--verbose must be one of: normal, low.")
raise typer.Exit(2)
set_dry_run(dry_run)
set_verbosity(verbose)
prompt_optional_updates = not skip_upgrade
try:
install_databricks_cli()
if agent is not None and agents is not None:
raise RuntimeError("Use either --agent or --agents, not both.")
workspace_entries = _parse_workspaces_option(workspaces) if workspaces is not None else None
if agent is not None:
tool = normalize_tool(agent)
install_tool_binary(tool, strict=True, update_existing=True)
install_tool_binary(
tool,
strict=True,
update_existing=True,
prompt_optional_updates=prompt_optional_updates,
)
if workspace_entries is None:
configure_workspace_command(tool)
else:
configure_workspace_command(tool, workspaces=workspace_entries)
elif agents is not None:
selected_tools = _parse_agents_option(agents)
if workspace_entries is None:
configure_workspace_command(selected_tools=selected_tools)
configure_workspace_command(
selected_tools=selected_tools,
prompt_optional_updates=prompt_optional_updates,
)
else:
configure_workspace_command(
selected_tools=selected_tools, workspaces=workspace_entries
selected_tools=selected_tools,
workspaces=workspace_entries,
prompt_optional_updates=prompt_optional_updates,
)
else:
# Tool binaries are installed after the user picks which agents
# they want, in configure_workspace_command.
if workspace_entries is None:
configure_workspace_command()
configure_workspace_command(prompt_optional_updates=prompt_optional_updates)
else:
configure_workspace_command(workspaces=workspace_entries)
configure_workspace_command(
workspaces=workspace_entries,
prompt_optional_updates=prompt_optional_updates,
)
if tracing:
# The workspaces were just configured, so enable tracing for them
# directly instead of re-prompting. Fall back to the workspace that
Expand Down
17 changes: 17 additions & 0 deletions src/ucode/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,23 @@
console = Console(highlight=False)
err_console = Console(stderr=True, highlight=False)

# Output verbosity. "normal" (default) renders decorative panels; "low" trades
# them for terse single-line output. Set once at CLI entry via set_verbosity.
_verbosity = "normal"


def set_verbosity(value: str) -> None:
global _verbosity
_verbosity = value or "normal"


def get_verbosity() -> str:
return _verbosity


def is_low_verbosity() -> bool:
return _verbosity == "low"


def print_section(title: str) -> None:
console.print()
Expand Down
85 changes: 85 additions & 0 deletions tests/test_agents_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,60 @@ def fake_run(args, **kwargs):
assert calls == []
assert "Updating OpenCode..." not in capsys.readouterr().out

def test_optional_update_prompt_suppressed_when_disabled(self, monkeypatch):
"""prompt_optional_updates=False must skip the optional update check
entirely — the confirm prompt should never be reached."""

def fake_which(binary: str) -> str | None:
return f"/usr/bin/{binary}"

monkeypatch.setattr("ucode.agents.shutil.which", fake_which)
monkeypatch.setattr("ucode.agents._minimum_version_error", lambda _: None)
monkeypatch.setattr("ucode.agents._required_update_message", lambda _: None)

def boom(_tool: str) -> bool:
raise AssertionError("optional update prompt should not be reached")

monkeypatch.setattr("ucode.agents._confirm_update_installed_tool_binary", boom)

assert (
install_tool_binary(
"opencode",
strict=False,
update_existing=True,
prompt_optional_updates=False,
)
is True
)

def test_required_update_runs_even_when_optional_prompt_disabled(self, monkeypatch):
"""A required (minimum-version) update is forced regardless of the
prompt_optional_updates preference."""
calls: list[list[str]] = []

def fake_which(binary: str) -> str | None:
return f"/usr/bin/{binary}"

def fake_run(args, **kwargs):
calls.append(args)
return subprocess.CompletedProcess(args, 0)

monkeypatch.setattr("ucode.agents.shutil.which", fake_which)
monkeypatch.setattr("ucode.agents.subprocess.run", fake_run)
monkeypatch.setattr("ucode.agents._required_update_message", lambda _: "must upgrade")
monkeypatch.setattr("ucode.agents._minimum_version_error", lambda _: None)

assert (
install_tool_binary(
"opencode",
strict=True,
update_existing=True,
prompt_optional_updates=False,
)
is True
)
assert calls and calls[0][:3] == ["npm", "install", "-g"]

def test_update_failure_keeps_existing_binary_available(self, monkeypatch):
def fake_which(binary: str) -> str | None:
return f"/usr/bin/{binary}"
Expand Down Expand Up @@ -339,3 +393,34 @@ def test_empty_selection_preserves_existing(self, monkeypatch):
state = {"workspace": "https://x.databricks.com", "available_tools": ["codex"]}
result = configure_selected_tools(state, [])
assert result["available_tools"] == ["codex"]


class TestValidateAllToolsVerbosity:
def _run(self, monkeypatch, capsys):
from contextlib import nullcontext

monkeypatch.setattr(agents_mod, "validate_tool", lambda tool: (True, ""))
monkeypatch.setattr(agents_mod, "save_state", lambda s: None)
monkeypatch.setattr(agents_mod, "spinner", lambda *_a, **_kw: nullcontext())
agents_mod.validate_all_tools({"available_tools": ["codex"], "managed_configs": {}})
return capsys.readouterr().out

def test_normal_verbosity_renders_panels(self, monkeypatch, capsys):
import ucode.ui as ui_mod

monkeypatch.setattr(ui_mod, "_verbosity", "normal")
out = self._run(monkeypatch, capsys)
assert "Testing each tool with a quick message" in out
assert "Ready" in out
assert "Codex is working" in out

def test_low_verbosity_omits_panels(self, monkeypatch, capsys):
import ucode.ui as ui_mod

monkeypatch.setattr(ui_mod, "_verbosity", "low")
out = self._run(monkeypatch, capsys)
assert "Validating..." in out
assert "Testing each tool with a quick message" not in out
assert "Ready" not in out
# Per-tool success line is still printed.
assert "Codex is working" in out
Loading
Loading