From 28420730f733f259e9b4cb971d02773dc20d2d72 Mon Sep 17 00:00:00 2001 From: AarushiShah-db Date: Wed, 12 Aug 2026 17:11:52 +0000 Subject: [PATCH] configure: launch ucode setup in place for admins + wording fixes When an admin accepts the setup offer during `ucode configure`, launch the setup flow right there instead of exiting and telling them to re-run it. setup_command now takes an already-resolved workspace/profile so it doesn't re-prompt for them. Also a few managed-setup wording fixes: - "Select the model" -> "Select the default model" - budget blurb: "pick anything they have access to" -> "pick any Model Service to which they have access" - fix the global-settings prompt/blurb/summary, which described the choice as machine-wide vs per-user. It's really: write the agent's own global settings file (routes to the gateway even without ucode) vs a ucode-specific file (needs ucode to route). Co-authored-by: Isaac --- src/ucode/cli.py | 16 ++++++++++++---- src/ucode/managed_wizard.py | 36 ++++++++++++++++++++++++------------ tests/test_cli.py | 29 ++++++++++++++++++++++++++++- 3 files changed, 64 insertions(+), 17 deletions(-) diff --git a/src/ucode/cli.py b/src/ucode/cli.py index ffaf0ec9..6d2c7edd 100644 --- a/src/ucode/cli.py +++ b/src/ucode/cli.py @@ -262,10 +262,18 @@ def _maybe_offer_admin_setup(workspace: str, profile: str | None) -> None: "the agents, models, MCPs, and skills once, and every developer picks them up automatically." ) if prompt_yes_no("Set one up now with `ucode setup`?"): - print_note( - "Run `ucode setup` to author your workspace's managed config, then `ucode apply`." - ) - raise typer.Exit(0) + # Launch the setup flow in place rather than telling them to re-run a command. Reuse the + # workspace/profile we already resolved and authenticated against so setup doesn't prompt + # for them again. + try: + code = setup_command(workspace=workspace, profile=profile) + except RuntimeError as exc: + print_err(str(exc)) + raise typer.Exit(1) from None + except KeyboardInterrupt: + print_err("Interrupted.") + raise typer.Exit(130) from None + raise typer.Exit(code or 0) def _print_discovery_diagnostics(state: dict) -> None: diff --git a/src/ucode/managed_wizard.py b/src/ucode/managed_wizard.py index f5da862b..d96dab72 100644 --- a/src/ucode/managed_wizard.py +++ b/src/ucode/managed_wizard.py @@ -73,19 +73,20 @@ spinner, ) -# What `use_as_global_settings` actually does, in plain terms. Admins are choosing between a -# machine-wide managed settings file and a per-user one, which is not obvious from the field name. +# What `use_as_global_settings` actually does, in plain terms. Admins are choosing whether to write +# the agent's own global settings file (so it points at the gateway even when launched directly) or +# a ucode-specific one (so the agent only routes through the gateway when launched via ucode). GLOBAL_SETTINGS_BLURB = ( - "Write this agent's config to the machine's managed settings file, which applies to every " - "user on the machine and cannot be overridden locally. Answer no to write the per-user " - "settings file instead, which developers can still change." + "Answer Yes to write this agent's own global settings file, so it points at the Databricks " + "gateway even when launched directly, without ucode. Answer no to write a ucode-specific " + "settings file instead, so the agent only routes through the gateway when launched via ucode." ) BUDGET_POLICY_BLURB = ( "A budget policy moves developers onto cheaper agents and models as the workspace spends " "against a budget — for example Claude Code on Opus by default, then Sonnet at 80%, then " - "OpenCode on Kimi at 100%. It only changes the default; developers can still pick anything " - "they have access to. Hard caps stay with the budget's own blocking threshold." + "OpenCode on Kimi at 100%. It only changes the default; developers can still pick any Model " + "Service to which they have access. Hard caps stay with the budget's own blocking threshold." ) @@ -355,7 +356,7 @@ def _prompt_models_for_agent(tool: str, state: dict, provider_service: dict | No if tool in SINGLE_MODEL_AGENTS: return { "default_model": _require_selection( - f"Select the model for {display}:", [(model, model) for model in options] + f"Select the default model for {display}:", [(model, model) for model in options] ) } @@ -820,7 +821,7 @@ def _render_summary(workspace: str, manifest: dict) -> None: provider = model_config.get("model_provider_service") if provider: detail = f"{detail} via {provider}" - scope = "machine-wide" if agent_config.get("use_as_global_settings") else "per-user" + scope = "global settings" if agent_config.get("use_as_global_settings") else "ucode-only" lines.append(kv_line(display, f"{detail} ({scope})")) # Spell out the per-family slots and model lists: the one-line default alone doesn't show # which families an admin configured, which is most of what they chose for claude. @@ -1008,9 +1009,18 @@ def _print_next_steps() -> None: print_note("Publish it to the workspace: ucode apply") -def setup_command(from_file: str | None = None) -> int: +def setup_command( + from_file: str | None = None, + *, + workspace: str | None = None, + profile: str | None = None, +) -> int: """Author the workspace's managed coding-agent config interactively. + ``workspace``/``profile`` let a caller that has already resolved (and authenticated against) a + workspace hand it in so the admin isn't prompted to pick one again — e.g. `ucode configure` + launching setup after its admin offer. When ``workspace`` is None the flow prompts as usual. + Returns a process exit code. Raises RuntimeError for actionable failures (not an admin, no agents available) and KeyboardInterrupt when the admin aborts a picker; the CLI maps both. """ @@ -1025,7 +1035,8 @@ def setup_command(from_file: str | None = None) -> int: print_note("Author the managed coding config for this workspace.") print_note("Developers pull it automatically when they run ucode.") - workspace, profile = _prompt_for_configuration() + if workspace is None: + workspace, profile = _prompt_for_configuration() # `configure_shared_state` below authenticates too and prints its own success line, so this one # stays quiet rather than reporting the same thing twice. It still has to run first: the admin # gate and the existing-config check both need a token before discovery. @@ -1082,7 +1093,8 @@ def setup_command(from_file: str | None = None) -> int: "model_config": _prompt_models_for_agent(tool, state, provider_service) } agent_config["use_as_global_settings"] = prompt_yes_no_default( - f"Apply {TOOL_SPECS[tool]['display']} config machine-wide? ({GLOBAL_SETTINGS_BLURB})", + f"Write {TOOL_SPECS[tool]['display']}'s config to its global settings file? " + f"({GLOBAL_SETTINGS_BLURB})", default=False, ) enabled_agents[tool] = agent_config diff --git a/tests/test_cli.py b/tests/test_cli.py index d0049373..cb40e03d 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -2373,7 +2373,9 @@ def test_admin_who_declines_proceeds_with_configure(self, monkeypatch): entries = self._resolve([("https://w", None)]) assert entries == [("https://w", None)] - def test_admin_who_accepts_exits_to_run_setup(self, monkeypatch): + def test_admin_who_accepts_launches_setup_in_place(self, monkeypatch): + # Accepting the offer runs `setup_command` right there — reusing the workspace/profile we + # already resolved so setup doesn't re-prompt for them — then exits with its code. import typer monkeypatch.setenv("ENABLE_MANAGED_AGENT_CONFIG", "1") @@ -2382,9 +2384,34 @@ def test_admin_who_accepts_exits_to_run_setup(self, monkeypatch): monkeypatch.setattr("ucode.cli.get_databricks_token", lambda ws, profile=None: "tok") monkeypatch.setattr("ucode.cli.is_workspace_admin", lambda ws, tok: True) monkeypatch.setattr("ucode.cli.prompt_yes_no", lambda prompt: True) + calls = {} + monkeypatch.setattr( + "ucode.cli.setup_command", + lambda **kwargs: calls.update(kwargs) or 0, + ) with pytest.raises(typer.Exit) as exc: self._resolve([("https://w", None)]) assert exc.value.exit_code == 0 + assert calls == {"workspace": "https://w", "profile": None} + + def test_admin_who_accepts_propagates_setup_failure(self, monkeypatch): + # A RuntimeError from setup (e.g. discovery failed) surfaces as a non-zero exit, not a stack + # trace bubbling out of configure. + import typer + + monkeypatch.setenv("ENABLE_MANAGED_AGENT_CONFIG", "1") + monkeypatch.setattr("ucode.cli.set_current_workspace", lambda ws: None) + monkeypatch.setattr("ucode.cli.refresh_managed_config", lambda state: None) + monkeypatch.setattr("ucode.cli.get_databricks_token", lambda ws, profile=None: "tok") + monkeypatch.setattr("ucode.cli.is_workspace_admin", lambda ws, tok: True) + monkeypatch.setattr("ucode.cli.prompt_yes_no", lambda prompt: True) + monkeypatch.setattr( + "ucode.cli.setup_command", + lambda **kwargs: (_ for _ in ()).throw(RuntimeError("no agents")), + ) + with pytest.raises(typer.Exit) as exc: + self._resolve([("https://w", None)]) + assert exc.value.exit_code == 1 def test_non_admin_is_never_prompted_for_setup(self, monkeypatch): monkeypatch.setenv("ENABLE_MANAGED_AGENT_CONFIG", "1")