Skip to content
Closed
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ Discovered external MCP connections are listed directly. MCP auth uses a Databri
| `ucode configure --dry-run` | Preview config files without writing them |
| `ucode configure --agents claude,codex` | Configure specific agents without the interactive picker |
| `ucode configure --workspaces https://first.databricks.com,https://second.databricks.com` | Configure workspaces without the interactive picker |
| `ucode configure --skip-install` | Configure workspace auth + Gateway routing only; don't install or update the agent CLIs (for callers that manage the binaries themselves) |
| `ucode configure --skip-validation` | Skip the post-configure check that runs each agent with a test message |

## Managed Local Files

Expand Down
72 changes: 59 additions & 13 deletions src/ucode/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,9 @@ def configure_workspace_command(
tool: str | None = None,
selected_tools: list[str] | None = None,
workspaces: list[tuple[str, str | None]] | None = None,
*,
skip_install: bool = False,
skip_validation: bool = False,
) -> int:
if tool is not None and selected_tools is not None:
raise RuntimeError("Use either --agent or --agents, not both.")
Expand All @@ -272,6 +275,8 @@ def configure_workspace_command(
expand=False,
)
)
if skip_validation:
return 0
with spinner(f"Validating {spec['display']}..."):
ok, err = validate_tool(tool)
Comment on lines +278 to 281
if ok:
Expand Down Expand Up @@ -320,8 +325,9 @@ def configure_workspace_command(
print_note("No coding agents selected — nothing to configure.")
return 0

for tool_name in picked:
install_tool_binary(tool_name, strict=False, update_existing=True)
if not skip_install:
for tool_name in picked:
install_tool_binary(tool_name, strict=False, update_existing=True)

state = configure_selected_tools(state, picked)

Expand All @@ -338,10 +344,11 @@ def configure_workspace_command(
)
)

# Limit validation to just-configured tools so we don't re-validate
# previously-configured tools the user didn't touch this run.
validate_state = {**state, "available_tools": picked}
validate_all_tools(validate_state)
if not skip_validation:
# Limit validation to just-configured tools so we don't re-validate
# previously-configured tools the user didn't touch this run.
validate_state = {**state, "available_tools": picked}
validate_all_tools(validate_state)
return 0


Expand Down Expand Up @@ -618,6 +625,24 @@ def configure(
help="Also enable MLflow tracing for the configured workspace(s).",
),
] = False,
skip_install: Annotated[
bool,
typer.Option(
"--skip-install",
help=(
"Don't install or update the agent CLIs; only configure workspace "
"auth + Gateway routing. Use when the agent binaries are managed "
"elsewhere (e.g. by a tool that embeds ucode)."
),
),
] = False,
skip_validation: Annotated[
bool,
typer.Option(
"--skip-validation",
help="Skip the post-configure check that runs each agent with a test message.",
),
] = False,
) -> None:
"""Configure workspace URL and AI Gateway."""
if ctx.invoked_subcommand is not None:
Expand All @@ -630,26 +655,47 @@ def configure(
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)
if not skip_install:
install_tool_binary(tool, strict=True, update_existing=True)
if workspace_entries is None:
configure_workspace_command(tool)
configure_workspace_command(
tool, skip_install=skip_install, skip_validation=skip_validation
)
else:
configure_workspace_command(tool, workspaces=workspace_entries)
configure_workspace_command(
tool,
workspaces=workspace_entries,
skip_install=skip_install,
skip_validation=skip_validation,
)
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,
skip_install=skip_install,
skip_validation=skip_validation,
)
else:
configure_workspace_command(
selected_tools=selected_tools, workspaces=workspace_entries
selected_tools=selected_tools,
workspaces=workspace_entries,
skip_install=skip_install,
skip_validation=skip_validation,
)
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(
skip_install=skip_install, skip_validation=skip_validation
)
else:
configure_workspace_command(workspaces=workspace_entries)
configure_workspace_command(
workspaces=workspace_entries,
skip_install=skip_install,
skip_validation=skip_validation,
)
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
125 changes: 117 additions & 8 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ def test_no_flag_calls_configure_all(self):
):
result = runner.invoke(app, ["configure"])
assert result.exit_code == 0, result.output
mock_cfg.assert_called_once_with()
mock_cfg.assert_called_once_with(skip_install=False, skip_validation=False)

def test_agents_flag_calls_configure_with_tools(self):
with (
Expand All @@ -391,7 +391,9 @@ def test_agents_flag_calls_configure_with_tools(self):
result = runner.invoke(app, ["configure", "--agents", "claude,codex"])
assert result.exit_code == 0, result.output
mock_install.assert_not_called()
mock_cfg.assert_called_once_with(selected_tools=["claude", "codex"])
mock_cfg.assert_called_once_with(
selected_tools=["claude", "codex"], skip_install=False, skip_validation=False
)

def test_agents_flag_normalizes_aliases_and_dedupes(self):
with (
Expand All @@ -401,7 +403,9 @@ def test_agents_flag_normalizes_aliases_and_dedupes(self):
):
result = runner.invoke(app, ["configure", "--agents", " claude-code, codex,claude "])
assert result.exit_code == 0, result.output
mock_cfg.assert_called_once_with(selected_tools=["claude", "codex"])
mock_cfg.assert_called_once_with(
selected_tools=["claude", "codex"], skip_install=False, skip_validation=False
)

def test_workspaces_flag_calls_configure_with_workspaces(self):
with (
Expand All @@ -422,7 +426,9 @@ def test_workspaces_flag_calls_configure_with_workspaces(self):
workspaces=[
("https://first.databricks.com", None),
("https://second.databricks.com", None),
]
],
skip_install=False,
skip_validation=False,
)

def test_agents_and_workspaces_flags_call_configure_with_both(self):
Expand All @@ -437,7 +443,10 @@ def test_agents_and_workspaces_flags_call_configure_with_both(self):
)
assert result.exit_code == 0, result.output
mock_cfg.assert_called_once_with(
selected_tools=["claude", "codex"], workspaces=[("https://first.com", None)]
selected_tools=["claude", "codex"],
workspaces=[("https://first.com", None)],
skip_install=False,
skip_validation=False,
)

def test_agent_and_workspaces_flags_call_configure_with_both(self):
Expand All @@ -452,7 +461,12 @@ def test_agent_and_workspaces_flags_call_configure_with_both(self):
)
assert result.exit_code == 0, result.output
mock_install.assert_called_once_with("claude", strict=True, update_existing=True)
mock_cfg.assert_called_once_with("claude", workspaces=[("https://first.com", None)])
mock_cfg.assert_called_once_with(
"claude",
workspaces=[("https://first.com", None)],
skip_install=False,
skip_validation=False,
)

def test_agent_flag_calls_configure_with_tool(self):
with (
Expand All @@ -463,7 +477,7 @@ def test_agent_flag_calls_configure_with_tool(self):
result = runner.invoke(app, ["configure", "--agent", "claude"])
assert result.exit_code == 0, result.output
mock_install.assert_called_once_with("claude", strict=True, update_existing=True)
mock_cfg.assert_called_once_with("claude")
mock_cfg.assert_called_once_with("claude", skip_install=False, skip_validation=False)

def test_agent_flag_normalizes_alias(self):
with (
Expand All @@ -473,7 +487,7 @@ def test_agent_flag_normalizes_alias(self):
):
result = runner.invoke(app, ["configure", "--agent", "claude-code"])
assert result.exit_code == 0, result.output
mock_cfg.assert_called_once_with("claude")
mock_cfg.assert_called_once_with("claude", skip_install=False, skip_validation=False)

def test_upgrade_runs_uv_tool_install(self):
with patch("subprocess.run") as mock_run:
Expand Down Expand Up @@ -704,3 +718,98 @@ def test_skips_purge_when_workspace_unchanged(self, monkeypatch):
cli_mod.configure_shared_state("https://same.databricks.com")

assert purge_calls == []


class TestConfigureSkipFlags:
def test_skip_flags_forwarded_from_agents_invocation(self):
with (
patch("ucode.cli.install_databricks_cli"),
patch("ucode.cli.install_tool_binary") as mock_install,
patch("ucode.cli.configure_workspace_command") as mock_cfg,
):
result = runner.invoke(
app,
[
"configure",
"--agents",
"codex",
"--workspaces",
"https://x.databricks.com",
"--skip-install",
"--skip-validation",
],
)
assert result.exit_code == 0, result.output
mock_install.assert_not_called()
mock_cfg.assert_called_once_with(
selected_tools=["codex"],
workspaces=[("https://x.databricks.com", None)],
skip_install=True,
skip_validation=True,
)

def test_skip_install_skips_eager_single_agent_install(self):
with (
patch("ucode.cli.install_databricks_cli"),
patch("ucode.cli.install_tool_binary") as mock_install,
patch("ucode.cli.configure_workspace_command") as mock_cfg,
):
result = runner.invoke(app, ["configure", "--agent", "codex", "--skip-install"])
assert result.exit_code == 0, result.output
# The eager pre-configure install for --agent must be skipped...
mock_install.assert_not_called()
# ...and the flag forwarded so the inner command skips its install too.
mock_cfg.assert_called_once_with("codex", skip_install=True, skip_validation=False)

def test_configure_help_lists_skip_flags(self):
result = runner.invoke(app, ["configure", "--help"])
assert result.exit_code == 0
output = _strip_ansi(result.output)
assert "--skip-install" in output
assert "--skip-validation" in output

def test_skip_flags_avoid_install_and_validation(self):
from ucode.cli import configure_workspace_command

state = {"workspace": "https://x.databricks.com", "available_tools": []}
with (
patch("ucode.cli._configure_shared_workspace_states", return_value=[state]),
patch("ucode.cli.check_gateway_endpoint", return_value=True),
patch("ucode.cli.install_tool_binary") as mock_install,
patch("ucode.cli.configure_selected_tools", return_value=state) as mock_configure,
patch("ucode.cli.validate_all_tools") as mock_validate,
):
rc = configure_workspace_command(
selected_tools=["codex"],
workspaces=[("https://x.databricks.com", None)],
skip_install=True,
skip_validation=True,
)
assert rc == 0
# The point of the flags: no agent binaries installed/updated and no
# test-message validation runs...
mock_install.assert_not_called()
mock_validate.assert_not_called()
# ...but workspace auth + Gateway routing config still happen.
mock_configure.assert_called_once_with(state, ["codex"])

def test_default_runs_install_and_validation(self):
from ucode.cli import configure_workspace_command

state = {"workspace": "https://x.databricks.com", "available_tools": []}
with (
patch("ucode.cli._configure_shared_workspace_states", return_value=[state]),
patch("ucode.cli.check_gateway_endpoint", return_value=True),
patch("ucode.cli.install_tool_binary") as mock_install,
patch("ucode.cli.configure_selected_tools", return_value=state),
patch("ucode.cli.validate_all_tools") as mock_validate,
):
rc = configure_workspace_command(
selected_tools=["codex"],
workspaces=[("https://x.databricks.com", None)],
)
assert rc == 0
# Without the flags the existing behavior is unchanged — both the
# install and the validation run (proves the guards aren't always-skip).
mock_install.assert_called_once_with("codex", strict=False, update_existing=True)
mock_validate.assert_called_once()
Loading