Axe 11 (modèles) : - am models : inventaire des runtimes locaux (ollama list --format json, llama.cpp .gguf sur disque, LM Studio API locale) avec cache façon probe.rs (TTL 30s) et absence de runtime signalée sans erreur fatale - am run <agent> --model <m> : résolution dans l'inventaire, transmission via model_env/model_arg de l'agent (défaut AM_MODEL), tracé en session - am models --prune : candidats inutilisés depuis settings.models_prune_days (défaut 30), :latest toujours protégé, dry-run + confirmation + événement EventKind::Model journalisé Axes 7/9 (catalogue) : - am catalog update : fetch du catalogue officiel, diff (ajoutés/retirés/ modifiés), confirmation, enregistrement en include - am catalog add <url> : validation du format (parse + validate), cache catalog-cache.json (TTL 3600s), événement EventKind::Catalog journalisé - include par URL (http/https/file) dans config.yaml, résolu au chargement avec cache + TTL ; override local toujours prioritaire (issue #67) - am suggest <requête> : réutilise le scoring fuzzy du catalogue, boost par usage réel (lancements + durée depuis le journal), top 5 justifié, --json REPL : commandes models/catalog/suggest (parse, complétion, descriptions), help specs, tip cheat sheet, man pages régénérées (49 pages, dont 3 nouvelles). Version bump 0.4.5, ROADMAP cases cochées, 266 tests verts.
167 lines
5.5 KiB
Rust
167 lines
5.5 KiB
Rust
//! catalog: manage remote catalogs (issues #60 #67, axes 7 & 9).
|
|
//! 'am catalog update' refreshes the official catalog and applies it as an
|
|
//! include; 'am catalog add <url>' registers any external catalog (same
|
|
//! YAML format) the same way. Everything goes through the probe-style
|
|
//! cache in catalog_remote so offline runs keep working.
|
|
|
|
use super::*;
|
|
use crate::catalog_remote::{self, CatalogDiff};
|
|
use crate::cli::CatalogCmd;
|
|
use crate::commands::config_cmd::register_include;
|
|
use crate::events::{Event, EventKind};
|
|
use crate::output::print_json;
|
|
use anyhow::{anyhow, Result};
|
|
|
|
pub fn run(app: &App, cmd: &CatalogCmd) -> Result<i32> {
|
|
match cmd {
|
|
CatalogCmd::Update => update(app),
|
|
CatalogCmd::Add { url } => add(app, url),
|
|
CatalogCmd::List => list(app),
|
|
}
|
|
}
|
|
|
|
/// am catalog update: fetch the official catalog, show the diff against the
|
|
/// local catalog, ask for confirmation, then register the URL as an include.
|
|
fn update(app: &App) -> Result<i32> {
|
|
let url = catalog_remote::official_url(app);
|
|
app.log.info(&format!("fetching the official catalog: {url}"));
|
|
let text = catalog_remote::fetch(app, &url, true)?;
|
|
let remote = catalog_remote::parse_catalog(&text)?;
|
|
let diff = catalog_remote::diff_catalogs(&app.config, &remote);
|
|
if app.json() {
|
|
print_json(&serde_json::json!({
|
|
"url": url,
|
|
"agents": remote.agents.len(),
|
|
"added": diff.added,
|
|
"removed": diff.removed,
|
|
"changed": diff.changed.iter().map(|(n, from, to)| serde_json::json!({"name": n, "run_from": from, "run_to": to})).collect::<Vec<_>>(),
|
|
}));
|
|
return Ok(0);
|
|
}
|
|
print_diff(&diff);
|
|
if diff.added.is_empty() && diff.removed.is_empty() && diff.changed.is_empty() {
|
|
app.log
|
|
.success(&format!("catalog is up to date ({} agents)", remote.agents.len()));
|
|
return Ok(0);
|
|
}
|
|
if app.dry_run() {
|
|
app.log.dry("would register the official catalog as an include");
|
|
return Ok(0);
|
|
}
|
|
if !app
|
|
.confirm(&format!(
|
|
"apply this catalog ({} agents, {} changes)?",
|
|
remote.agents.len(),
|
|
diff.added.len() + diff.removed.len() + diff.changed.len()
|
|
))?
|
|
{
|
|
app.log.info("catalog update cancelled");
|
|
return Ok(0);
|
|
}
|
|
register_include(app, &url)?;
|
|
app.emit(
|
|
&Event::now(EventKind::Catalog)
|
|
.with_args(vec![url.clone()])
|
|
.with_reason("update"),
|
|
);
|
|
app.log.success(&format!(
|
|
"official catalog registered as an include ({url}) — it is effective from the next run"
|
|
));
|
|
Ok(0)
|
|
}
|
|
|
|
/// am catalog add <url>: validate and register an external catalog.
|
|
fn add(app: &App, url: &str) -> Result<i32> {
|
|
crate::download::validate_url(url)?;
|
|
if config_cmd::user_includes(app).iter().any(|i| i == url) {
|
|
app.log
|
|
.info(&format!("catalog {url} is already registered as an include"));
|
|
return Ok(0);
|
|
}
|
|
app.log.info(&format!("fetching catalog: {url}"));
|
|
// force: 'catalog add' must validate the current content of the URL,
|
|
// not a possibly stale cached copy.
|
|
let text = catalog_remote::fetch(app, url, true)?;
|
|
let remote = catalog_remote::parse_catalog(&text)?;
|
|
if app.json() {
|
|
print_json(&serde_json::json!({
|
|
"url": url,
|
|
"agents": remote.agents.len(),
|
|
"registered": false,
|
|
}));
|
|
return Ok(0);
|
|
}
|
|
if remote.agents.is_empty() {
|
|
return Err(anyhow!(
|
|
"the catalog at {url} defines no agents — refusing to register it"
|
|
));
|
|
}
|
|
if app.dry_run() {
|
|
app.log.dry(&format!(
|
|
"would register {url} ({:?} agents)",
|
|
remote.agents.len()
|
|
));
|
|
return Ok(0);
|
|
}
|
|
register_include(app, url)?;
|
|
app.emit(
|
|
&Event::now(EventKind::Catalog)
|
|
.with_args(vec![url.to_string()])
|
|
.with_reason("add"),
|
|
);
|
|
app.log.success(&format!(
|
|
"catalog {url} registered ({} agents) — effective from the next run",
|
|
remote.agents.len()
|
|
));
|
|
Ok(0)
|
|
}
|
|
|
|
/// am catalog list: show the registered remote catalogs (includes).
|
|
/// The resolved config consumes its includes at load time, so this reads
|
|
/// the user config file directly (like 'config add').
|
|
fn list(app: &App) -> Result<i32> {
|
|
let urls: Vec<String> = config_cmd::user_includes(app)
|
|
.iter()
|
|
.filter(|i| {
|
|
i.starts_with("http://")
|
|
|| i.starts_with("https://")
|
|
|| i.starts_with("file://")
|
|
})
|
|
.cloned()
|
|
.collect();
|
|
if app.json() {
|
|
print_json(&urls);
|
|
return Ok(0);
|
|
}
|
|
if urls.is_empty() {
|
|
app.log.info(
|
|
"no remote catalog registered — 'am catalog add <url>' or 'am catalog update'",
|
|
);
|
|
return Ok(0);
|
|
}
|
|
println!("remote catalogs ({}):", urls.len());
|
|
for u in &urls {
|
|
println!(" - {u}");
|
|
}
|
|
Ok(0)
|
|
}
|
|
|
|
fn print_diff(diff: &CatalogDiff) {
|
|
if !diff.added.is_empty() {
|
|
println!(" + {} new agent(s): {}", diff.added.len(), diff.added.join(", "));
|
|
}
|
|
if !diff.removed.is_empty() {
|
|
println!(
|
|
" - {} removed: {}",
|
|
diff.removed.len(),
|
|
diff.removed.join(", ")
|
|
);
|
|
}
|
|
if !diff.changed.is_empty() {
|
|
println!(" ~ {} changed run command(s):", diff.changed.len());
|
|
for (name, from, to) in &diff.changed {
|
|
println!(" {name}: '{from}' -> '{to}'");
|
|
}
|
|
}
|
|
}
|