diff --git a/apps/craft-anything-cli/src/rebuild_cmd.rs b/apps/craft-anything-cli/src/rebuild_cmd.rs index 1fa99066..9359517f 100644 --- a/apps/craft-anything-cli/src/rebuild_cmd.rs +++ b/apps/craft-anything-cli/src/rebuild_cmd.rs @@ -38,7 +38,7 @@ pub struct RebuildArgs { /// the computer stopped); the request is ignored. #[arg(long, requires = "continue_in")] pub resume: bool, - /// The coding agent to use (a backend name from `ai list`). + /// The coding agent to use (a backend name from `ai list`, or an installed agent such as `Claude Code`). #[arg(long)] pub backend: Option, /// Refinement rounds after the first build. @@ -71,14 +71,21 @@ pub fn rebuild(cli: &Cli, a: &RebuildArgs) -> u8 { } } -/// Add the first agent app found on this computer when no enabled AI can build at all. -fn ensure_agent(hub: &AiHub) -> Result<(), ToolError> { - // An enabled API backend builds with our own agent loop, so this run needs no agent app. Only a - // computer with no AI set up needs one found for it. - if hub.backends().iter().any(|b| b.enabled) { +/// Add the agent app this run asks for, or the first one found when no enabled AI can build. +fn ensure_agent(hub: &AiHub, requested: Option<&str>) -> Result<(), ToolError> { + let configured = hub.backends(); + // An enabled backend can build, so a run that names none needs no agent app. + if requested.is_none() && configured.iter().any(|b| b.enabled) { return Ok(()); } - if let Some(d) = hub.detect().into_iter().find(|d| d.suggestion.kind == BackendKind::AgentCli) { + // A name that is configured is used as it is (including a backend that is off): don't add a + // second one under the same name. + if requested.is_some_and(|name| configured.iter().any(|b| b.name == name)) { + return Ok(()); + } + // An explicit name may be an installed agent that isn't added yet: add only that one. With no + // name, add the first agent found. + if let Some(d) = hub.detect().into_iter().find(|d| d.suggestion.kind == BackendKind::AgentCli && requested.is_none_or(|name| d.suggestion.name == name)) { eprintln!("Using {} ({}).", d.suggestion.name, d.reason); hub.add_backend(d.suggestion, None)?; } @@ -95,7 +102,7 @@ fn run(cli: &Cli, a: &RebuildArgs) -> Result { if a.generate_assets && a.continue_in.is_some() { return Err(ToolError::invalid("--generate-assets is for new projects.").with_remediation("Use project-assets-allow for an existing project.")); } - ensure_agent(&hub)?; + ensure_agent(&hub, a.backend.as_deref())?; let (ws, message) = match &a.continue_in { Some(dir) => (Workspace::open(&std::path::absolute(dir).map_err(|e| ToolError::io(dir.display(), e))?)?, (!a.resume).then(|| a.request.clone())), None => { diff --git a/apps/craft-anything-cli/tests/rebuild_choice.rs b/apps/craft-anything-cli/tests/rebuild_choice.rs index 61a437d1..283bd8db 100644 --- a/apps/craft-anything-cli/tests/rebuild_choice.rs +++ b/apps/craft-anything-cli/tests/rebuild_choice.rs @@ -102,6 +102,47 @@ fn an_enabled_api_backend_is_enough_without_being_named() { assert!(!saved.contains("agent_cli"), "no agent app may be saved when an API backend is enabled:\n{saved}"); } +/// A named agent that is installed but not added yet is still registered, so the run can use it. +#[test] +fn a_named_installed_agent_is_added_when_missing() { + let config = tempfile::tempdir().unwrap(); + let home = tempfile::tempdir().unwrap(); + let bin = tempfile::tempdir().unwrap(); + let project = tempfile::tempdir().unwrap(); + api_config(config.path()); + fake_claude(bin.path()); + + let args = stopped_run(project.path(), Some("Claude Code")); + let refs: Vec<&str> = args.iter().map(String::as_str).collect(); + let out = cli(&refs, config.path(), home.path(), bin.path()); + let log = out.stderr_text(); + assert!(!out.success, "the empty project folder must stop the run:\n{log}"); + assert!(log.contains("craft.json"), "the run must stop at the missing project, before any build:\n{log}"); + assert!(log.contains("Using Claude Code"), "the named agent must still be registered:\n{log}"); + let saved = std::fs::read_to_string(config.path().join("config.toml")).unwrap(); + assert!(saved.contains("agent_cli") && saved.contains("name = \"Claude Code\""), "the named agent must be saved:\n{saved}"); +} + +/// An unknown name must not add a different agent that happens to be installed. +#[test] +fn an_unknown_backend_does_not_add_an_agent() { + let config = tempfile::tempdir().unwrap(); + let home = tempfile::tempdir().unwrap(); + let bin = tempfile::tempdir().unwrap(); + let project = tempfile::tempdir().unwrap(); + fake_claude(bin.path()); + + let args = stopped_run(project.path(), Some("No Such AI")); + let refs: Vec<&str> = args.iter().map(String::as_str).collect(); + let out = cli(&refs, config.path(), home.path(), bin.path()); + let log = out.stderr_text(); + assert!(!out.success, "the empty project folder must stop the run:\n{log}"); + assert!(log.contains("craft.json"), "the run must stop at the missing project, before any build:\n{log}"); + assert!(!log.contains("Using "), "an unknown name must not pick an installed agent:\n{log}"); + let saved = std::fs::read_to_string(config.path().join("config.toml")).unwrap_or_default(); + assert!(!saved.contains("agent_cli"), "an unknown name must not save an agent:\n{saved}"); +} + /// A command refused before any build starts must not change the person's AI settings either. #[test] fn an_invalid_command_does_not_add_an_agent() { diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 602ba8dd..1cb1eea0 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -319,8 +319,8 @@ Owner: open · Status: todo settings and prints "Using Claude Code (…)": `ensure_agent` (`rebuild_cmd.rs`) runs whenever no agent app is set up, though this run won't use one. Run it only when the build needs one — claimed: AlexJoaquimPereira, branch `bugfix/r5.5-no-agent-with-api-backend` -- [ ] `--backend ` with no enabled AI still finds and saves an agent - before the name is refused: check the name before `ensure_agent` runs +- [ ] `--backend ` still builds with it: `builder_backend`'s named + branch doesn't check `enabled`, while Settings treats an AI that is off as unusable - [x] Bound accumulated data across all lines of one server-sent event, so short lines cannot grow a streamed API response without limit — claimed: Afloat16, branch bugfix/r5.5-bounded-sse-events-20261103