v0.4.3 : persistance du theme actif choisi
- le REPL 'theme <name>' applique ET sauvegarde le theme (settings.theme dans le fichier de config actif, cree s'il manque) via persist_setting - correction racine : config::merge ignorait settings.theme - la valeur persistee n etait jamais relue au chargement, le theme par defaut etait toujours reapplique apres un redemarrage - 'am --theme <name>' reste un one-shot ; config set settings.theme <nom> fonctionne egalement - aides mises a jour (banniere, help theme) - tests de persistance + relecture au demarrage, man pages regenerees, version 0.4.3
This commit is contained in:
+3
-3
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
> Ce document décrit la conception, le fonctionnement et les fonctionnalités de **agent-manager** (binaire `am`), un CLI Rust multiplateforme pour gérer des agents IA de coding locaux.
|
> Ce document décrit la conception, le fonctionnement et les fonctionnalités de **agent-manager** (binaire `am`), un CLI Rust multiplateforme pour gérer des agents IA de coding locaux.
|
||||||
>
|
>
|
||||||
> 📦 Version : `0.4.2` · 🦀 Rust 2021 · ✅ Windows · ✅ Linux · ✅ macOS
|
> 📦 Version : `0.4.3` · 🦀 Rust 2021 · ✅ Windows · ✅ Linux · ✅ macOS
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -584,7 +584,7 @@ dist/ Binaires précompilés
|
|||||||
├── am-linux-x86_64
|
├── am-linux-x86_64
|
||||||
├── am-linux-aarch64
|
├── am-linux-aarch64
|
||||||
├── am-windows-x86_64.zip
|
├── am-windows-x86_64.zip
|
||||||
├── am_0.4.2_amd64.deb
|
├── am_0.4.3_amd64.deb
|
||||||
└── ...
|
└── ...
|
||||||
|
|
||||||
packaging/ Scripts de packaging
|
packaging/ Scripts de packaging
|
||||||
@@ -685,4 +685,4 @@ flowchart LR
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Document généré pour agent-manager v0.4.2* 🚀
|
*Document généré pour agent-manager v0.4.3* 🚀
|
||||||
|
|||||||
Generated
+1
-1
@@ -21,7 +21,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "agent-manager"
|
name = "agent-manager"
|
||||||
version = "0.4.2"
|
version = "0.4.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "agent-manager"
|
name = "agent-manager"
|
||||||
version = "0.4.2"
|
version = "0.4.3"
|
||||||
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"
|
||||||
|
|||||||
+5
-5
@@ -62,7 +62,7 @@ doit signaler aucune dépendance manquante.
|
|||||||
|
|
||||||
Une fois toutes les archives dans `dist/` :
|
Une fois toutes les archives dans `dist/` :
|
||||||
|
|
||||||
scripts/render-manifests.ps1 -Version 0.4.2
|
scripts/render-manifests.ps1 -Version 0.4.3
|
||||||
|
|
||||||
Ce script rend, avec la version et les sha256 réels :
|
Ce script rend, avec la version et les sha256 réels :
|
||||||
|
|
||||||
@@ -73,16 +73,16 @@ Ce script rend, avec la version et les sha256 réels :
|
|||||||
- `dist/homebrew/am.rb` → à servir via un tap, ou à soumettre
|
- `dist/homebrew/am.rb` → à servir via un tap, ou à soumettre
|
||||||
- `dist/rpm/am.spec` → rpmbuild -bb (avec am-linux-x86_64.tar.gz
|
- `dist/rpm/am.spec` → rpmbuild -bb (avec am-linux-x86_64.tar.gz
|
||||||
dans ~/rpmbuild/SOURCES)
|
dans ~/rpmbuild/SOURCES)
|
||||||
- Paquet deb : `packaging/deb/make-deb.sh 0.4.2` → dist/am_0.4.2_amd64.deb
|
- Paquet deb : `packaging/deb/make-deb.sh 0.4.3` → dist/am_0.4.3_amd64.deb
|
||||||
|
|
||||||
## 4. Créer la release sur Gitea
|
## 4. Créer la release sur Gitea
|
||||||
|
|
||||||
Via l'interface web : Releases > New Release, tag v0.4.2, attacher les
|
Via l'interface web : Releases > New Release, tag v0.4.3, attacher les
|
||||||
archives. Ou via l'API (jeton Gitea requis) :
|
archives. Ou via l'API (jeton Gitea requis) :
|
||||||
|
|
||||||
curl -X POST -H "Authorization: token <JETON>" \
|
curl -X POST -H "Authorization: token <JETON>" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"tag_name":"v0.4.2","name":"v0.4.2","body":"notes de version"}' \
|
-d '{"tag_name":"v0.4.3","name":"v0.4.3","body":"notes de version"}' \
|
||||||
https://git.dracodev.net/api/v1/repos/Projets/agent-manager/releases
|
https://git.dracodev.net/api/v1/repos/Projets/agent-manager/releases
|
||||||
|
|
||||||
curl -X POST -H "Authorization: token <JETON>" \
|
curl -X POST -H "Authorization: token <JETON>" \
|
||||||
@@ -95,7 +95,7 @@ archives. Ou via l'API (jeton Gitea requis) :
|
|||||||
|
|
||||||
## 5. Tag git (déclenche aussi le pipeline CI)
|
## 5. Tag git (déclenche aussi le pipeline CI)
|
||||||
|
|
||||||
git tag v0.4.2 && git push origin v0.4.2
|
git tag v0.4.3 && git push origin v0.4.3
|
||||||
|
|
||||||
## Conventions de nommage (attendues par les installateurs et self-update)
|
## Conventions de nommage (attendues par les installateurs et self-update)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
.ie \n(.g .ds Aq \(aq
|
.ie \n(.g .ds Aq \(aq
|
||||||
.el .ds Aq '
|
.el .ds Aq '
|
||||||
.TH am 1 "am 0.4.2"
|
.TH am 1 "am 0.4.3"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
am \- agent\-manager (am) — manage local AI coding agents
|
am \- agent\-manager (am) — manage local AI coding agents
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -174,4 +174,4 @@ Export the configuration and installation state (backup)
|
|||||||
am\-import(1)
|
am\-import(1)
|
||||||
Import a previously exported configuration and state
|
Import a previously exported configuration and state
|
||||||
.SH VERSION
|
.SH VERSION
|
||||||
v0.4.2
|
v0.4.3
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Renders the packaging templates (scoop, winget, homebrew, rpm) with the
|
# Renders the packaging templates (scoop, winget, homebrew, rpm) with the
|
||||||
# version and the sha256 of every release archive.
|
# version and the sha256 of every release archive.
|
||||||
# Usage: scripts/render-manifests.ps1 -Version 0.4.2 [-DistDir dist]
|
# Usage: scripts/render-manifests.ps1 -Version 0.4.3 [-DistDir dist]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)][string]$Version,
|
[Parameter(Mandatory = $true)][string]$Version,
|
||||||
[string]$DistDir = "dist"
|
[string]$DistDir = "dist"
|
||||||
|
|||||||
+112
-5
@@ -103,7 +103,28 @@ pub fn run(app: &App, cmd: &ConfigCmd) -> Result<i32> {
|
|||||||
/// configuration file, then validate the result.
|
/// configuration file, then validate the result.
|
||||||
pub fn set_value(app: &App, key: &str, value: &str) -> Result<i32> {
|
pub fn set_value(app: &App, key: &str, value: &str) -> Result<i32> {
|
||||||
let path = user_config_path(app)?;
|
let path = user_config_path(app)?;
|
||||||
let text = std::fs::read_to_string(&path)
|
write_dotted_key(&path, key, value)?;
|
||||||
|
reload_and_validate(&path)?;
|
||||||
|
app.log.success(&format!("{key} = {value}"));
|
||||||
|
Ok(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Persist one setting into the configuration file that is actually in
|
||||||
|
/// effect (the loaded origin), creating the user config file when none
|
||||||
|
/// exists. Used by the REPL 'theme <name>' command so a switched theme
|
||||||
|
/// survives a restart.
|
||||||
|
pub fn persist_setting(app: &App, key: &str, value: &str) -> Result<()> {
|
||||||
|
let path = match &app.config_origin {
|
||||||
|
Some(p) => p.clone(),
|
||||||
|
None => user_config_path(app)?,
|
||||||
|
};
|
||||||
|
write_dotted_key(&path, key, value)?;
|
||||||
|
reload_and_validate(&path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set one dotted key (e.g. settings.default_shell) in a YAML config file.
|
||||||
|
fn write_dotted_key(path: &Path, key: &str, value: &str) -> Result<()> {
|
||||||
|
let text = std::fs::read_to_string(path)
|
||||||
.with_context(|| format!("cannot read {}", path.display()))?;
|
.with_context(|| format!("cannot read {}", path.display()))?;
|
||||||
let mut root: serde_yaml::Value = serde_yaml::from_str(&text)
|
let mut root: serde_yaml::Value = serde_yaml::from_str(&text)
|
||||||
.with_context(|| format!("{} is not valid YAML", path.display()))?;
|
.with_context(|| format!("{} is not valid YAML", path.display()))?;
|
||||||
@@ -128,16 +149,18 @@ pub fn set_value(app: &App, key: &str, value: &str) -> Result<i32> {
|
|||||||
serde_yaml::Value::String(parts.last().unwrap().to_string()),
|
serde_yaml::Value::String(parts.last().unwrap().to_string()),
|
||||||
parsed,
|
parsed,
|
||||||
);
|
);
|
||||||
std::fs::write(&path, serde_yaml::to_string(&root)?)
|
std::fs::write(path, serde_yaml::to_string(&root)?)
|
||||||
.with_context(|| format!("cannot write {}", path.display()))?;
|
.with_context(|| format!("cannot write {}", path.display()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
// Validate the new configuration end to end.
|
/// Reload a written config file to make sure the change stays valid.
|
||||||
|
fn reload_and_validate(path: &Path) -> Result<()> {
|
||||||
let cli = crate::cli::Cli::parse_from(["am", "--config", path.to_str().unwrap_or_default()]);
|
let cli = crate::cli::Cli::parse_from(["am", "--config", path.to_str().unwrap_or_default()]);
|
||||||
config::load(&cli).map_err(|e| {
|
config::load(&cli).map_err(|e| {
|
||||||
anyhow!("the new value makes the configuration invalid: {e:#}")
|
anyhow!("the new value makes the configuration invalid: {e:#}")
|
||||||
})?;
|
})?;
|
||||||
app.log.success(&format!("{key} = {value}"));
|
Ok(())
|
||||||
Ok(0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a scalar: booleans and integers keep their type, everything else
|
/// Parse a scalar: booleans and integers keep their type, everything else
|
||||||
@@ -235,3 +258,87 @@ fn add_file(app: &App, file: &Path) -> Result<i32> {
|
|||||||
));
|
));
|
||||||
Ok(0)
|
Ok(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
/// App whose active config is a file inside a leaked TempDir (the same
|
||||||
|
/// pattern as profile_cmd's tests: the guard would delete the directory
|
||||||
|
/// when the helper returns).
|
||||||
|
fn app_with_config(text: &str) -> (App, PathBuf) {
|
||||||
|
let guard = tempfile::tempdir().unwrap();
|
||||||
|
let dir = guard.path().to_path_buf();
|
||||||
|
std::mem::forget(guard);
|
||||||
|
let cfg = dir.join("config.yaml");
|
||||||
|
std::fs::write(&cfg, text).unwrap();
|
||||||
|
let cli = crate::cli::Cli::parse_from(["am", "--config", cfg.to_str().unwrap()]);
|
||||||
|
let mut app = App::from_cli(cli).unwrap();
|
||||||
|
let mut p = app.paths.clone();
|
||||||
|
p.install_dir = dir.join("agents");
|
||||||
|
p.bin_dir = dir.join("agents").join("bin");
|
||||||
|
p.log_dir = dir.join("logs");
|
||||||
|
p.state_file = dir.join("state.json");
|
||||||
|
p.probe_cache_file = dir.join("probe-cache.json");
|
||||||
|
p.config_dir = Some(dir.join("config"));
|
||||||
|
app.paths = p;
|
||||||
|
(app, cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn persist_setting_writes_into_the_active_config() {
|
||||||
|
let (app, cfg) = app_with_config("version: \"1.0\"
|
||||||
|
settings:
|
||||||
|
theme: default
|
||||||
|
agents: []
|
||||||
|
");
|
||||||
|
persist_setting(&app, "settings.theme", "ocean").unwrap();
|
||||||
|
let text = std::fs::read_to_string(&cfg).unwrap();
|
||||||
|
let value: serde_yaml::Value = serde_yaml::from_str(&text).unwrap();
|
||||||
|
assert_eq!(value["settings"]["theme"], "ocean");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn persisted_theme_is_used_on_next_start() {
|
||||||
|
let (app, cfg) = app_with_config("version: \"1.0\"
|
||||||
|
settings:
|
||||||
|
theme: default
|
||||||
|
agents: []
|
||||||
|
");
|
||||||
|
persist_setting(&app, "settings.theme", "ocean").unwrap();
|
||||||
|
// A fresh start reads settings.theme from the config file.
|
||||||
|
let cli = crate::cli::Cli::parse_from(["am", "--config", cfg.to_str().unwrap()]);
|
||||||
|
let reloaded = App::from_cli(cli).unwrap();
|
||||||
|
assert_eq!(reloaded.theme().name, "ocean");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn persist_setting_creates_the_user_config_when_none_exists() {
|
||||||
|
let (mut app, _cfg) = app_with_config("version: \"1.0\"
|
||||||
|
agents: []
|
||||||
|
");
|
||||||
|
// Simulate a run without any user/local config: no origin.
|
||||||
|
app.config_origin = None;
|
||||||
|
persist_setting(&app, "settings.theme", "mono").unwrap();
|
||||||
|
let user_file = app.paths.config_dir.as_ref().unwrap().join(config::CONFIG_FILE_NAME);
|
||||||
|
assert!(user_file.is_file(), "user config was not created");
|
||||||
|
let text = std::fs::read_to_string(&user_file).unwrap();
|
||||||
|
let value: serde_yaml::Value = serde_yaml::from_str(&text).unwrap();
|
||||||
|
assert_eq!(value["settings"]["theme"], "mono");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn write_dotted_key_descends_into_nested_mappings() {
|
||||||
|
let guard = tempfile::tempdir().unwrap();
|
||||||
|
let path = guard.path().join("c.yaml");
|
||||||
|
std::fs::write(&path, "version: \"1.0\"
|
||||||
|
agents: []
|
||||||
|
").unwrap();
|
||||||
|
write_dotted_key(&path, "settings.default_shell", "pwsh").unwrap();
|
||||||
|
let value: serde_yaml::Value =
|
||||||
|
serde_yaml::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
|
||||||
|
assert_eq!(value["settings"]["default_shell"], "pwsh");
|
||||||
|
assert!(value["agents"].is_sequence());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -533,6 +533,9 @@ pub fn merge(base: &mut Config, overlay: Config) {
|
|||||||
if o.self_update_base_url.is_some() {
|
if o.self_update_base_url.is_some() {
|
||||||
s.self_update_base_url = o.self_update_base_url;
|
s.self_update_base_url = o.self_update_base_url;
|
||||||
}
|
}
|
||||||
|
if o.theme.is_some() {
|
||||||
|
s.theme = o.theme;
|
||||||
|
}
|
||||||
for (k, v) in o.hooks {
|
for (k, v) in o.hooks {
|
||||||
s.hooks.insert(k, v);
|
s.hooks.insert(k, v);
|
||||||
}
|
}
|
||||||
@@ -764,6 +767,30 @@ agents:
|
|||||||
assert!(problems.iter().any(|p| p.contains("'run' is required")));
|
assert!(problems.iter().any(|p| p.contains("'run' is required")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn merge_keeps_the_user_theme_setting() {
|
||||||
|
let mut base: Config = serde_yaml::from_str(DEFAULT_CONFIG).unwrap();
|
||||||
|
let overlay: Config = serde_yaml::from_str(
|
||||||
|
r#"
|
||||||
|
version: "1.0"
|
||||||
|
settings:
|
||||||
|
theme: ocean
|
||||||
|
agents: []
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
merge(&mut base, overlay);
|
||||||
|
assert_eq!(base.settings.theme.as_deref(), Some("ocean"));
|
||||||
|
// The embedded default catalog defines no theme: an overlay without
|
||||||
|
// one leaves the merged setting untouched.
|
||||||
|
let mut base: Config = serde_yaml::from_str(DEFAULT_CONFIG).unwrap();
|
||||||
|
let overlay: Config = serde_yaml::from_str("version: \"1.0\"
|
||||||
|
agents: []
|
||||||
|
").unwrap();
|
||||||
|
merge(&mut base, overlay);
|
||||||
|
assert_eq!(base.settings.theme, None);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn one_or_many_parses_both() {
|
fn one_or_many_parses_both() {
|
||||||
let a: OneOrMany = serde_yaml::from_str("\"make build\"").unwrap();
|
let a: OneOrMany = serde_yaml::from_str("\"make build\"").unwrap();
|
||||||
|
|||||||
+3
-3
@@ -940,7 +940,7 @@ pub static HELP_SPECS: &[HelpSpec] = &[
|
|||||||
name: "theme",
|
name: "theme",
|
||||||
category: "Shell",
|
category: "Shell",
|
||||||
usage: "theme {flags} [name]",
|
usage: "theme {flags} [name]",
|
||||||
about: "Show the color themes, or switch the active one inside the shell.",
|
about: "Show the color themes, or switch the active one inside the shell (the choice is saved to the config).",
|
||||||
search_terms: &["color", "colors", "palette"],
|
search_terms: &["color", "colors", "palette"],
|
||||||
flags: &[],
|
flags: &[],
|
||||||
subcommands: &[],
|
subcommands: &[],
|
||||||
@@ -950,7 +950,7 @@ pub static HELP_SPECS: &[HelpSpec] = &[
|
|||||||
io: None,
|
io: None,
|
||||||
examples: &[
|
examples: &[
|
||||||
HelpExample { desc: "List the available themes.", code: "theme" },
|
HelpExample { desc: "List the available themes.", code: "theme" },
|
||||||
HelpExample { desc: "Switch to the ocean theme.", code: "theme ocean" },
|
HelpExample { desc: "Switch to the ocean theme (kept for the next start).", code: "theme ocean" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
HelpSpec {
|
HelpSpec {
|
||||||
@@ -1386,7 +1386,7 @@ pub fn render_general(theme: &Theme, color: bool) -> String {
|
|||||||
out.push_str(&format!(" {bullet} help <name> - display help about a particular command or agent\n"));
|
out.push_str(&format!(" {bullet} help <name> - display help about a particular command or agent\n"));
|
||||||
out.push_str(&format!(" {bullet} help --find <text to search> - search through all help commands table\n"));
|
out.push_str(&format!(" {bullet} help --find <text to search> - search through all help commands table\n"));
|
||||||
out.push_str(&format!(
|
out.push_str(&format!(
|
||||||
" {bullet} am --theme <name> - choose a color theme ({})\n",
|
" {bullet} theme <name> - switch the color theme (saved) · am --theme <name> for a one-shot ({})\n",
|
||||||
crate::theme::names().join(", ")
|
crate::theme::names().join(", ")
|
||||||
));
|
));
|
||||||
out.push('\n');
|
out.push('\n');
|
||||||
|
|||||||
+11
-4
@@ -706,7 +706,7 @@ pub fn banner_box(
|
|||||||
rows.push(inner(dim(&line, theme, color)));
|
rows.push(inner(dim(&line, theme, color)));
|
||||||
}
|
}
|
||||||
rows.push(inner(dim(
|
rows.push(inner(dim(
|
||||||
" 'am --theme <name>' to switch · settings.theme in the config",
|
" 'theme <name>' to switch (saved) · 'am --theme <name>' for a one-shot",
|
||||||
theme,
|
theme,
|
||||||
color,
|
color,
|
||||||
)));
|
)));
|
||||||
@@ -1831,6 +1831,15 @@ fn theme_command(app: &App, arg: Option<&str>) {
|
|||||||
Some(name) => match crate::theme::find(name) {
|
Some(name) => match crate::theme::find(name) {
|
||||||
Some(t) => {
|
Some(t) => {
|
||||||
app.set_theme(t);
|
app.set_theme(t);
|
||||||
|
// Persist the choice (settings.theme in the active config
|
||||||
|
// file) so it survives an exit and restart.
|
||||||
|
if let Err(e) =
|
||||||
|
crate::commands::config_cmd::persist_setting(app, "settings.theme", t.name)
|
||||||
|
{
|
||||||
|
app.log.error(format!(
|
||||||
|
"theme applied for this session, but it could not be saved to the config: {e:#}"
|
||||||
|
));
|
||||||
|
}
|
||||||
let color = app.color();
|
let color = app.color();
|
||||||
println!(
|
println!(
|
||||||
"{} {}",
|
"{} {}",
|
||||||
@@ -1871,9 +1880,7 @@ fn print_theme_status(app: &App) {
|
|||||||
println!(" {}", theme.dimmed(&format!("⇄ {}", others.join(" · "))));
|
println!(" {}", theme.dimmed(&format!("⇄ {}", others.join(" · "))));
|
||||||
println!(
|
println!(
|
||||||
" {}",
|
" {}",
|
||||||
theme.dimmed(
|
theme.dimmed("'theme <name>' to switch (saved to the config) · 'am --theme <name>' for a one-shot")
|
||||||
"'theme <name>' to switch · '--theme <name>' or settings.theme to change the default"
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user