fix: installateur Windows — accepter l'entree am-windows-x86_64.exe dans l'archive de release + i18n lang-aware pour am ai
release / release (push) Successful in 13m14s
release / macos (push) Canceled after 0s

- install.ps1 ne trouvait que 'am.exe' dans le zip : les archives (CI et
  manuelles) utilisent le nom plateforme 'am-windows-x86_64.exe' -> repli
  systematique sur cargo install au lieu du binaire precompile
- Le script accepte desormais les deux noms (copie vers am.exe) ; chemin
  du message cargo install corrige (.cargo\bin\am.exe au lieu de binam)
- i18n : am ai utilisait tr()/tr_fmt() globaux (flaky selon le process) —
  passage a tr_in/tr_fmt_in(app.lang()) + helper tr_fmt_in ajoute a i18n.rs
- Archive v1.0.6 : les deux entrees (am.exe + am-windows-x86_64.exe) pour
  la compatibilite maximale (installateurs et self-update)
- Tests ai_test insensibles a la locale (FR/EN)
- v1.0.6 ; ROADMAP maintenance note ajoutee
This commit is contained in:
2026-08-20 12:45:10 -04:00
parent 313645eea2
commit 50f515e406
8 changed files with 58 additions and 22 deletions
Generated
+1 -1
View File
@@ -21,7 +21,7 @@ dependencies = [
[[package]] [[package]]
name = "agent-manager" name = "agent-manager"
version = "1.0.5" version = "1.0.6"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "agent-manager" name = "agent-manager"
version = "1.0.5" version = "1.0.6"
edition = "2021" edition = "2021"
description = "Manage local AI coding agents: list, install, start, stop, update — with automatic dependency handling and a YAML-driven catalog." description = "Manage local AI coding agents: list, install, start, stop, update — with automatic dependency handling and a YAML-driven catalog."
license = "MIT" license = "MIT"
+12 -3
View File
@@ -1,7 +1,7 @@
# 🗺️ ROADMAP agent-manager — vers le « super outil » # 🗺️ ROADMAP agent-manager — vers le « super outil »
> **Version du document : 2.5** · Statut : phases 0 à 3 livrées > **Version du document : 2.6** · Statut : phases 0 à 3 livrées
> (v0.3.0 → v1.0.1), maintenance v1.0.2 → v1.0.5 > (v0.3.0 → v1.0.1), maintenance v1.0.2 → v1.0.6
> Base : analyse du code **v0.2.7** (Rust, 45+ tests, zéro dépendance runtime) > Base : analyse du code **v0.2.7** (Rust, 45+ tests, zéro dépendance runtime)
> >
> ✅ **Phase 0 livrée en v0.3.0 (2026-08-17)** : issues #2 à #16 clôturées, > ✅ **Phase 0 livrée en v0.3.0 (2026-08-17)** : issues #2 à #16 clôturées,
@@ -47,6 +47,14 @@
> y/N pour les commandes risky, exécution via le shell de l'utilisateur), > y/N pour les commandes risky, exécution via le shell de l'utilisateur),
> flags --files/--provider/--model/--yes, alias CLI `am shell`, événements > flags --files/--provider/--model/--yes, alias CLI `am shell`, événements
> `shell_ai` journalisés. Épique Axe 13 (#93) clôturée. > `shell_ai` journalisés. Épique Axe 13 (#93) clôturée.
>
> 🔧 **Maintenance v1.0.6 (2026-08-20)** : installateur Windows corrigé —
> `install.ps1` n'acceptait que l'entrée `am.exe` dans l'archive de release
> (la CI et les archives manuelles utilisent `am-windows-x86_64.exe`) →
> repli systématique sur `cargo install` au lieu du binaire précompilé.
> Le script accepte désormais les deux noms ; archives v1.0.6 publiées avec
> les deux entrées pour la compatibilité maximale. Chemin du message
> cargo install corrigé (`.cargo\bin\am.exe`).
--- ---
@@ -628,4 +636,5 @@ configuration manuelle (tokens au keyring, provider/modèle par défaut).
Phases livrées : 0 (v0.3.0), 1 (v0.4.1), 2 (v0.6.0), v0.7.0 (providers, Phases livrées : 0 (v0.3.0), 1 (v0.4.1), 2 (v0.6.0), v0.7.0 (providers,
2026-08-19), 3 (v1.0.0, #76–#80, 2026-08-20) ; maintenance v1.0.1/v1.0.2 2026-08-19), 3 (v1.0.0, #76–#80, 2026-08-20) ; maintenance v1.0.1/v1.0.2
(self-update décompression, REPL), v1.0.3 (détection externe), v1.0.4 (self-update décompression, REPL), v1.0.3 (détection externe), v1.0.4
(catalogue shell AI, #94 #95), v1.0.5 (commande am ai, #93 #96 #97).* (catalogue shell AI, #94 #95), v1.0.5 (commande am ai, #93 #96 #97),
v1.0.6 (installateur Windows accepte l'entrée am-windows-x86_64.exe).*
+4 -1
View File
@@ -24,7 +24,7 @@ function Install-FromSource {
Write-Host "Installation depuis les sources (cargo install --git $BaseUrl.git) ..." Write-Host "Installation depuis les sources (cargo install --git $BaseUrl.git) ..."
cargo install --git "$BaseUrl.git" --locked cargo install --git "$BaseUrl.git" --locked
if ($LASTEXITCODE -ne 0) { Fail "cargo install a echoue" } if ($LASTEXITCODE -ne 0) { Fail "cargo install a echoue" }
$bin = Join-Path $env:USERPROFILE ".cargo\binam.exe" $bin = Join-Path $env:USERPROFILE ".cargo\bin\am.exe"
Write-Host "" Write-Host ""
Write-Host "OK - am est installe dans $bin" Write-Host "OK - am est installe dans $bin"
Write-Host "Essayez : am list | am doctor (mise a jour : relancez ce script)" Write-Host "Essayez : am list | am doctor (mise a jour : relancez ce script)"
@@ -47,7 +47,10 @@ if ($arch -eq "x86_64") {
$tmpDir = Join-Path $env:TEMP "am-$tag" $tmpDir = Join-Path $env:TEMP "am-$tag"
Remove-Item $tmpDir -Recurse -Force -ErrorAction SilentlyContinue Remove-Item $tmpDir -Recurse -Force -ErrorAction SilentlyContinue
Expand-Archive -Path $tmp -DestinationPath $tmpDir -Force Expand-Archive -Path $tmp -DestinationPath $tmpDir -Force
# L'entrée de l'archive varie selon le pipeline : « am.exe » ou
# « am-windows-x86_64.exe » (nom de l'asset). Accepter les deux.
$exe = Join-Path $tmpDir "am.exe" $exe = Join-Path $tmpDir "am.exe"
if (-not (Test-Path $exe)) { $exe = Join-Path $tmpDir "am-windows-x86_64.exe" }
if (Test-Path $exe) { if (Test-Path $exe) {
New-Item -ItemType Directory -Force -Path $binDir | Out-Null New-Item -ItemType Directory -Force -Path $binDir | Out-Null
Copy-Item $exe (Join-Path $binDir "am.exe") -Force Copy-Item $exe (Join-Path $binDir "am.exe") -Force
+9 -5
View File
@@ -129,22 +129,26 @@ fn exec_mode(
); );
let decision = crate::shell_ai::decide(mode, risk, dry_run, yes); let decision = crate::shell_ai::decide(mode, risk, dry_run, yes);
let lang = app.lang();
let mut executed = false; let mut executed = false;
match decision { match decision {
Decision::Abort => { Decision::Abort => {
let msg = if dry_run { let msg = if dry_run {
"dry-run : commande non exécutée" crate::i18n::tr_in(lang, "dry-run : commande non exécutée")
} else { } else {
"politique de sécurité (dry-run par défaut) : commande non exécutée — utilisez --yes" crate::i18n::tr_in(
lang,
"politique de sécurité (dry-run par défaut) : commande non exécutée — utilisez --yes",
)
}; };
app.log.info(crate::i18n::tr(msg)); app.log.info(msg);
} }
Decision::Ask => { Decision::Ask => {
let ok = app.confirm(crate::i18n::tr("Exécuter ?"))?; let ok = app.confirm(crate::i18n::tr_in(lang, "Exécuter ?"))?;
if ok { if ok {
executed = true; executed = true;
} else { } else {
app.log.info(crate::i18n::tr("annulé")); app.log.info(crate::i18n::tr_in(lang, "annulé"));
} }
} }
Decision::Execute => { Decision::Execute => {
+9
View File
@@ -436,6 +436,15 @@ pub fn tr_fmt<'a>(key: &'a str, args: &[&dyn std::fmt::Display]) -> String {
} }
} }
/// Format a message for a GIVEN locale (deterministic — follows the app's
/// resolved language instead of the process-global locale).
pub fn tr_fmt_in<'a>(lang: Lang, key: &'a str, args: &[&dyn std::fmt::Display]) -> String {
match lang {
Lang::Fr => fmt_args(key, args),
Lang::En => fmt_args(lookup_en(key).unwrap_or(key), args),
}
}
/// Replace `{}` placeholders (positional) with the Display of each argument. /// Replace `{}` placeholders (positional) with the Display of each argument.
fn fmt_args(template: &str, args: &[&dyn std::fmt::Display]) -> String { fn fmt_args(template: &str, args: &[&dyn std::fmt::Display]) -> String {
let mut out = String::with_capacity(template.len() + 16); let mut out = String::with_capacity(template.len() + 16);
+19 -9
View File
@@ -226,10 +226,10 @@ pub fn provider_env(
} else { } else {
known.join(", ") known.join(", ")
}; };
bail!(crate::tr_fmt!( bail!(crate::i18n::tr_fmt_in(
app.lang(),
"provider '{}' inconnu — providers enregistrés: {}", "provider '{}' inconnu — providers enregistrés: {}",
provider.unwrap_or(""), &[&provider.unwrap_or(""), &hint]
hint
)); ));
} }
return Ok((Vec::new(), BTreeMap::new())); return Ok((Vec::new(), BTreeMap::new()));
@@ -307,16 +307,26 @@ pub fn generate(
if !out.status.success() { if !out.status.success() {
let err = String::from_utf8_lossy(&out.stderr); let err = String::from_utf8_lossy(&out.stderr);
let err = err.trim(); let err = err.trim();
bail!(crate::tr_fmt!( bail!(crate::i18n::tr_fmt_in(
app.lang(),
"aichat n'a pas généré de commande (exit {}){}", "aichat n'a pas généré de commande (exit {}){}",
out.status.code().unwrap_or(-1), &[
if err.is_empty() { String::new() } else { format!(": {err}") } &out.status.code().unwrap_or(-1).to_string(),
&if err.is_empty() {
String::new()
} else {
format!(": {err}")
},
]
)); ));
} }
let stdout = String::from_utf8_lossy(&out.stdout); let stdout = String::from_utf8_lossy(&out.stdout);
extract_command(&stdout).ok_or_else(|| anyhow!(crate::i18n::tr( extract_command(&stdout).ok_or_else(|| {
"aichat n'a retourné aucune commande (dry-run)" anyhow!(crate::i18n::tr_in(
))) app.lang(),
"aichat n'a retourné aucune commande (dry-run)"
))
})
} }
/// Execute a generated command through the user's shell (default_shell > /// Execute a generated command through the user's shell (default_shell >
+3 -2
View File
@@ -92,7 +92,7 @@ fn ai_exec_dry_run_is_the_default_and_never_executes() {
); );
assert_eq!(code, 0); assert_eq!(code, 0);
assert!( assert!(
log.contains("non exécutée"), log.contains("non exécutée") || log.contains("not executed"),
"dry-run policy must abort, log: {log}" "dry-run policy must abort, log: {log}"
); );
assert!(!log.contains("exécution:"), "nothing must run, log: {log}"); assert!(!log.contains("exécution:"), "nothing must run, log: {log}");
@@ -148,7 +148,8 @@ fn ai_exec_respects_configured_confirm_mode() {
); );
assert_eq!(code, 0); assert_eq!(code, 0);
assert!( assert!(
log.contains("annulé") || log.contains("cancelled") || log.contains("non exécutée"), log.contains("annulé") || log.contains("cancelled") || log.contains("non exécutée")
|| log.contains("not executed"),
"declined confirmation must abort, log: {log}" "declined confirmation must abort, log: {log}"
); );
assert!(!log.contains("exécution:"), "log: {log}"); assert!(!log.contains("exécution:"), "log: {log}");