Compare commits

..
1 Commits
Author SHA1 Message Date
bruno b7f29baea5 fix: assistant setup — liste des providers construite depuis le registre (QwenCloud absents de la liste codee en dur, v1.1.9)
release / release (push) Successful in 13m41s
release / macos (push) Canceled after 0s
2026-09-01 11:40:05 -04:00
5 changed files with 79 additions and 20 deletions
Generated
+1 -1
View File
@@ -21,7 +21,7 @@ dependencies = [
[[package]]
name = "agent-manager"
version = "1.1.8"
version = "1.1.9"
dependencies = [
"anyhow",
"base64",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "agent-manager"
version = "1.1.8"
version = "1.1.9"
edition = "2021"
description = "Manage local AI coding agents: list, install, start, stop, update — with automatic dependency handling and a YAML-driven catalog."
license = "MIT"
+8
View File
@@ -147,6 +147,14 @@
> OpenAI-compatible OPENAI_API_KEY/BASE_URL/MODEL) avec hints pour basculer
> sur le Token Plan (`--provider qwen-cloud-token-plan`, clé dédiée via
> `am providers set-token`).
>
> 🐛 **Maintenance v1.1.9 (2026-09-01)** : l'assistant `am setup` listait ses
> providers dans une constante codée en dur — les QwenCloud ajoutés au
> registre n'y apparaissaient pas. La liste est maintenant construite depuis
> le registre fusionné (embarqué + config utilisateur), ollama et custom en
> plus, sans doublon. Test de non-régression ajouté. Bonus : le wizard ne
> boucle plus à l'infini sur stdin fermé (erreur claire + suggestion
> `--yes`).
---
+2 -2
View File
@@ -1,6 +1,6 @@
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.TH am 1 "am 1.1.8"
.TH am 1 "am 1.1.9"
.SH NAME
am \- agent\-manager (am) — manage local AI coding agents
.SH SYNOPSIS
@@ -250,4 +250,4 @@ Export the configuration and installation state (backup)
am\-import(1)
Import a previously exported configuration and state
.SH VERSION
v1.1.8
v1.1.9
+67 -16
View File
@@ -14,9 +14,22 @@ use crate::secrets::SecretStore;
use anyhow::{bail, Result};
use std::io::Write;
/// Known providers offered by the wizard (from the embedded registry plus
/// the local Ollama runtime and a custom endpoint).
const CHOICES: &[&str] = &["anthropic", "openai", "deepseek", "google", "openrouter", "ollama", "custom"];
/// Providers offered by the wizard: every provider defined in the merged
/// registry (embedded + user config, sorted), plus the local Ollama runtime
/// and a custom endpoint. Built dynamically so new registry providers
/// (e.g. the QwenCloud token-plan / pay-as-you-go pair) show up without a
/// code change — CHOICES used to be a hardcoded list that silently drifted.
fn choices(app: &App) -> Vec<String> {
let mut v: Vec<String> = crate::providers::defined(&app.config)
.map(|(k, _)| k.clone())
.collect();
v.sort();
if !v.iter().any(|p| p == "ollama") {
v.push("ollama".to_string());
}
v.push("custom".to_string());
v
}
/// True when the user has not configured a usable default provider yet:
/// no `settings.default_provider`, or the default provider has no API
@@ -36,10 +49,14 @@ pub fn needs_setup(app: &App) -> bool {
.is_none()
}
fn read_line() -> Result<String> {
/// Read one trimmed line from stdin. `None` on EOF — callers must not spin
/// on empty input when nothing is connected (pipes, --yes in CI).
fn read_line() -> Result<Option<String>> {
let mut line = String::new();
std::io::stdin().read_line(&mut line)?;
Ok(line.trim().to_string())
if std::io::stdin().read_line(&mut line)? == 0 {
return Ok(None);
}
Ok(Some(line.trim().to_string()))
}
/// Ask a y/N question on stderr, defaulting to yes when `yes` is set.
@@ -50,7 +67,7 @@ fn ask(app: &App, question: &str, default_yes: bool) -> Result<bool> {
let suffix = if default_yes { "[Y/n]" } else { "[y/N]" };
eprint!("{question} {suffix} ");
std::io::stderr().flush()?;
let answer = read_line()?.to_lowercase();
let answer = read_line()?.unwrap_or_default().to_lowercase();
Ok(match answer.as_str() {
"" => default_yes,
"y" | "yes" | "o" | "oui" | "true" => true,
@@ -61,9 +78,10 @@ fn ask(app: &App, question: &str, default_yes: bool) -> Result<bool> {
/// Pick a provider from the list (or the current default when --yes).
fn pick_provider(app: &App) -> Result<(String, Option<String>)> {
let current = crate::providers::default_name(&app.config);
let choices = choices(app);
println!("Provider disponible :");
for (i, p) in CHOICES.iter().enumerate() {
let star = if Some(*p) == current { " (actuel)" } else { "" };
for (i, p) in choices.iter().enumerate() {
let star = if Some(p.as_str()) == current { " (actuel)" } else { "" };
println!(" {}) {}{star}", i + 1, p);
}
if app.cli.yes {
@@ -71,16 +89,18 @@ fn pick_provider(app: &App) -> Result<(String, Option<String>)> {
return Ok((name, None));
}
loop {
print!("Choisissez (1-{}) : ", CHOICES.len());
print!("Choisissez (1-{}) : ", choices.len());
std::io::stdout().flush()?;
let raw = read_line()?;
let Some(raw) = read_line()? else {
bail!("aucune entrée sur stdin — relancez en terminal interactif ou utilisez 'am setup --yes'");
};
if let Ok(n) = raw.parse::<usize>() {
if (1..=CHOICES.len()).contains(&n) {
let name = CHOICES[n - 1];
if (1..=choices.len()).contains(&n) {
let name = &choices[n - 1];
if name == "custom" {
print!("Base URL de l'API (ex: https://api.openai.com/v1) : ");
std::io::stdout().flush()?;
let url = read_line()?;
let url = read_line()?.unwrap_or_default();
if url.is_empty() {
continue;
}
@@ -126,7 +146,9 @@ fn pick_model(app: &App, name: &str) -> Result<String> {
loop {
print!("Modèle par défaut : ");
std::io::stdout().flush()?;
let raw = read_line()?;
let Some(raw) = read_line()? else {
bail!("aucune entrée sur stdin — relancez en terminal interactif ou utilisez 'am setup --yes'");
};
if let Ok(n) = raw.parse::<usize>() {
if n >= 1 && n <= models.len() {
return Ok(models[n - 1].clone());
@@ -134,7 +156,7 @@ fn pick_model(app: &App, name: &str) -> Result<String> {
if n == models.len() + 1 {
print!("Nom du modèle : ");
std::io::stdout().flush()?;
let custom = read_line()?;
let custom = read_line()?.unwrap_or_default();
if !custom.is_empty() {
return Ok(custom);
}
@@ -347,6 +369,35 @@ mod tests {
assert!(needs_setup(&app));
}
#[test]
fn wizard_offers_every_registry_provider() {
// Regression (v1.1.8): the wizard used a hardcoded CHOICES list and
// the QwenCloud providers added to the registry never appeared.
// The list must now track the merged registry + ollama + custom.
let app = app_with("");
let c = choices(&app);
for p in [
"anthropic",
"openai",
"deepseek",
"google",
"openrouter",
"qwen-cloud",
"qwen-cloud-token-plan",
"ollama",
"custom",
] {
assert!(c.iter().any(|x| x == p), "wizard must offer {p}: {c:?}");
}
// User-defined providers show up too, ollama is never duplicated.
let app2 = app_with(
" default_provider: anthropic\n providers:\n ollama:\n base_url: http://localhost:11434/v1\n my-llm:\n base_url: https://llm.internal/v1\n",
);
let c2 = choices(&app2);
assert_eq!(c2.iter().filter(|x| *x == "ollama").count(), 1, "{c2:?}");
assert!(c2.iter().any(|x| x == "my-llm"), "{c2:?}");
}
#[test]
fn needs_setup_false_for_local_ollama() {
let app = app_with(