feat: hints de configuration post-install (setup_hints) — commandes manuelles affichees apres install avec substitution {model}/{provider}/{base_url} (picoclaw onboard/model/auth)
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "agent-manager"
|
name = "agent-manager"
|
||||||
version = "1.1.3"
|
version = "1.1.4"
|
||||||
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"
|
||||||
|
|||||||
@@ -92,6 +92,14 @@
|
|||||||
> enregistré existant » (les branches non-installé/binaire manquant
|
> enregistré existant » (les branches non-installé/binaire manquant
|
||||||
> étaient déjà correctes). Test de régression
|
> étaient déjà correctes). Test de régression
|
||||||
> `resolve_exec_installed_agent_does_not_leak_run_token`.
|
> `resolve_exec_installed_agent_does_not_leak_run_token`.
|
||||||
|
>
|
||||||
|
> 🔧 **Maintenance v1.1.4 (2026-08-21)** : hints de configuration
|
||||||
|
> post-install — nouveau champ `setup_hints` dans la définition d'un agent
|
||||||
|
> (config.yaml) : commandes de configuration manuelle affichées après
|
||||||
|
> l'install quand l'agent gère sa propre config (ex. picoclaw
|
||||||
|
> onboard/model/auth), avec substitution des placeholders `{model}`,
|
||||||
|
> `{provider}` et `{base_url}` depuis le provider résolu. picoclaw est le
|
||||||
|
> premier agent du catalogue à les déclarer.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1025,6 +1025,12 @@ agents:
|
|||||||
repo: sipeed/picoclaw
|
repo: sipeed/picoclaw
|
||||||
binary: picoclaw
|
binary: picoclaw
|
||||||
run: picoclaw
|
run: picoclaw
|
||||||
|
# PicoClaw gère sa config par sous-commandes (pas de fichier à écrire) :
|
||||||
|
# ces commandes sont affichées après l'install avec {model} substitué.
|
||||||
|
setup_hints:
|
||||||
|
- "picoclaw onboard"
|
||||||
|
- "picoclaw model {model}"
|
||||||
|
- "picoclaw auth login"
|
||||||
tags: [go, tiny]
|
tags: [go, tiny]
|
||||||
|
|
||||||
- name: ironclaw
|
- name: ironclaw
|
||||||
|
|||||||
@@ -111,6 +111,46 @@ pub fn apply_env_map_resolved(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Substitute the {model}, {provider} and {base_url} placeholders of a
|
||||||
|
/// setup hint with the resolved provider configuration (v1.1.4). When no
|
||||||
|
/// provider is resolved, the hint is returned unchanged.
|
||||||
|
pub fn render_hint(
|
||||||
|
hint: &str,
|
||||||
|
resolved: &Option<(String, Option<String>)>,
|
||||||
|
config: &Config,
|
||||||
|
) -> String {
|
||||||
|
let Some((pname, model)) = resolved else {
|
||||||
|
return hint.to_string();
|
||||||
|
};
|
||||||
|
let base_url = crate::providers::get(config, pname)
|
||||||
|
.map(|d| d.base_url.clone())
|
||||||
|
.unwrap_or_default();
|
||||||
|
hint.replace("{provider}", pname)
|
||||||
|
.replace(
|
||||||
|
"{model}",
|
||||||
|
model.as_deref().unwrap_or("{model}"),
|
||||||
|
)
|
||||||
|
.replace("{base_url}", &base_url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print the manual setup commands of an agent (agents that manage their
|
||||||
|
/// own configuration, e.g. picoclaw onboard/model/auth). No-op when the
|
||||||
|
/// agent declares none (v1.1.4).
|
||||||
|
fn print_hints(
|
||||||
|
app: &App,
|
||||||
|
agent: &AgentDef,
|
||||||
|
resolved: &Option<(String, Option<String>)>,
|
||||||
|
) {
|
||||||
|
if agent.setup_hints.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
app.log
|
||||||
|
.success(crate::i18n::tr("Configuration manuelle requise — à exécuter :"));
|
||||||
|
for hint in &agent.setup_hints {
|
||||||
|
app.log.info(&format!(" {}", render_hint(hint, resolved, &app.config)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Write the agent configuration files after a successful install
|
/// Write the agent configuration files after a successful install
|
||||||
/// (issue #91). The caller skips this on `--no-config`, on non-configurable
|
/// (issue #91). The caller skips this on `--no-config`, on non-configurable
|
||||||
/// agents, and in dry-run (never reached). Never writes a token in clear:
|
/// agents, and in dry-run (never reached). Never writes a token in clear:
|
||||||
@@ -124,6 +164,7 @@ pub fn apply_post_install(
|
|||||||
app.log.info(crate::i18n::tr(
|
app.log.info(crate::i18n::tr(
|
||||||
"agent sans adaptateur de config — voir sa doc",
|
"agent sans adaptateur de config — voir sa doc",
|
||||||
));
|
));
|
||||||
|
print_hints(app, agent, resolved);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
if cfg.files.is_empty() {
|
if cfg.files.is_empty() {
|
||||||
@@ -136,12 +177,14 @@ pub fn apply_post_install(
|
|||||||
"config par variables d'env — injectée au démarrage",
|
"config par variables d'env — injectée au démarrage",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
print_hints(app, agent, resolved);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let Some((pname, model)) = resolved else {
|
let Some((pname, model)) = resolved else {
|
||||||
app.log.warn(crate::i18n::tr(
|
app.log.warn(crate::i18n::tr(
|
||||||
"aucun provider résolu — fichiers de config non écrits",
|
"aucun provider résolu — fichiers de config non écrits",
|
||||||
));
|
));
|
||||||
|
print_hints(app, agent, resolved);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
let Some(def) = crate::providers::get(&app.config, pname) else {
|
let Some(def) = crate::providers::get(&app.config, pname) else {
|
||||||
@@ -418,6 +461,7 @@ mod tests {
|
|||||||
provider,
|
provider,
|
||||||
model: None,
|
model: None,
|
||||||
config: Some(config),
|
config: Some(config),
|
||||||
|
setup_hints: vec![],
|
||||||
sandbox: None,
|
sandbox: None,
|
||||||
tags: vec![],
|
tags: vec![],
|
||||||
installable: false,
|
installable: false,
|
||||||
@@ -515,4 +559,64 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(provider_pref(&agent2), Some("openai"));
|
assert_eq!(provider_pref(&agent2), Some("openai"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_hint_substitutes_resolved_values() {
|
||||||
|
let c: Config = serde_yaml::from_str(
|
||||||
|
"version: \"1.0\"\nsettings:\n default_provider: openrouter\n providers:\n openrouter:\n base_url: https://openrouter.ai/api/v1\n default_model: deepseek/deepseek-chat-v3-0324\n models: [deepseek/deepseek-chat-v3-0324]\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let resolved = Some(("openrouter".to_string(), Some("deepseek/deepseek-chat-v3-0324".to_string())));
|
||||||
|
assert_eq!(
|
||||||
|
render_hint("picoclaw model {model}", &resolved, &c),
|
||||||
|
"picoclaw model deepseek/deepseek-chat-v3-0324"
|
||||||
|
);
|
||||||
|
assert_eq!(render_hint("picoclaw onboard", &resolved, &c), "picoclaw onboard");
|
||||||
|
assert_eq!(
|
||||||
|
render_hint("cli --provider {provider} --url {base_url}", &resolved, &c),
|
||||||
|
"cli --provider openrouter --url https://openrouter.ai/api/v1"
|
||||||
|
);
|
||||||
|
// No resolved provider: the hint is left untouched.
|
||||||
|
assert_eq!(
|
||||||
|
render_hint("picoclaw model {model}", &None, &c),
|
||||||
|
"picoclaw model {model}"
|
||||||
|
);
|
||||||
|
// Resolved provider without a model: {model} stays visible.
|
||||||
|
let no_model = Some(("openrouter".to_string(), None));
|
||||||
|
assert_eq!(
|
||||||
|
render_hint("picoclaw model {model}", &no_model, &c),
|
||||||
|
"picoclaw model {model}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn apply_post_install_with_setup_hints_is_ok() {
|
||||||
|
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,
|
||||||
|
"version: \"1.0\"\nsettings:\n auto_install_deps: false\n confirm_before_run: false\nagents: []\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let cli = crate::cli::Cli::parse_from(["am", "--config", cfg.to_str().unwrap()]);
|
||||||
|
let app = crate::app::App::from_cli(cli).unwrap();
|
||||||
|
let mut agent = agent_with_config(
|
||||||
|
AgentConfig {
|
||||||
|
env_map: BTreeMap::new(),
|
||||||
|
files: vec![],
|
||||||
|
provider_default: None,
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
agent.config = None;
|
||||||
|
agent.setup_hints = vec![
|
||||||
|
"picoclaw onboard".to_string(),
|
||||||
|
"picoclaw model {model}".to_string(),
|
||||||
|
];
|
||||||
|
// No provider resolved: hints printed with placeholders, no crash.
|
||||||
|
assert!(apply_post_install(&app, &agent, &None).is_ok());
|
||||||
|
assert!(apply_post_install(&app, &agent, &Some(("openrouter".to_string(), None))).is_ok());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -669,6 +669,7 @@ mod tests {
|
|||||||
files: vec![],
|
files: vec![],
|
||||||
provider_default: None,
|
provider_default: None,
|
||||||
}),
|
}),
|
||||||
|
setup_hints: vec![],
|
||||||
sandbox: None,
|
sandbox: None,
|
||||||
tags: vec![],
|
tags: vec![],
|
||||||
installable: false,
|
installable: false,
|
||||||
|
|||||||
@@ -200,6 +200,7 @@ mod tests {
|
|||||||
provider: None,
|
provider: None,
|
||||||
model: None,
|
model: None,
|
||||||
config: None,
|
config: None,
|
||||||
|
setup_hints: vec![],
|
||||||
sandbox: None,
|
sandbox: None,
|
||||||
tags: tags.iter().map(|s| s.to_string()).collect(),
|
tags: tags.iter().map(|s| s.to_string()).collect(),
|
||||||
installable: false,
|
installable: false,
|
||||||
|
|||||||
@@ -467,6 +467,12 @@ pub struct AgentDef {
|
|||||||
/// Post-install provider configuration (issue #91).
|
/// Post-install provider configuration (issue #91).
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub config: Option<AgentConfig>,
|
pub config: Option<AgentConfig>,
|
||||||
|
/// Manual setup commands shown after install when the agent manages its
|
||||||
|
/// own configuration (onboard/model/auth...). Placeholders {model},
|
||||||
|
/// {provider} and {base_url} are substituted with the resolved values
|
||||||
|
/// (v1.1.4).
|
||||||
|
#[serde(default)]
|
||||||
|
pub setup_hints: Vec<String>,
|
||||||
/// Sandbox profile (issue #79): allowed commands, working directories
|
/// Sandbox profile (issue #79): allowed commands, working directories
|
||||||
/// and network policy. Disabled by default — the unsandboxed mode
|
/// and network policy. Disabled by default — the unsandboxed mode
|
||||||
/// stays available and documented.
|
/// stays available and documented.
|
||||||
|
|||||||
@@ -214,6 +214,7 @@ pub const CATALOG: &[(&str, &str)] = &[
|
|||||||
("provider {} (modèle {}) au lancement de {}", "provider {} (model {}) when launching {}"),
|
("provider {} (modèle {}) au lancement de {}", "provider {} (model {}) when launching {}"),
|
||||||
("agent sans adaptateur de config — voir sa doc", "agent without a config adapter — see its docs"),
|
("agent sans adaptateur de config — voir sa doc", "agent without a config adapter — see its docs"),
|
||||||
("config par variables d'env — injectée au démarrage", "env-var configuration — injected at start"),
|
("config par variables d'env — injectée au démarrage", "env-var configuration — injected at start"),
|
||||||
|
("Configuration manuelle requise — à exécuter :", "Manual setup required — run:"),
|
||||||
("aucun provider résolu — fichiers de config non écrits", "no provider resolved — config files not written"),
|
("aucun provider résolu — fichiers de config non écrits", "no provider resolved — config files not written"),
|
||||||
("provider '{}' introuvable — fichiers de config non écrits", "provider '{}' not found — config files not written"),
|
("provider '{}' introuvable — fichiers de config non écrits", "provider '{}' not found — config files not written"),
|
||||||
("format inconnu pour {} — adaptateurs: toml, yaml, json, key=value", "unknown format for {} — adapters: toml, yaml, json, key=value"),
|
("format inconnu pour {} — adaptateurs: toml, yaml, json, key=value", "unknown format for {} — adapters: toml, yaml, json, key=value"),
|
||||||
|
|||||||
Reference in New Issue
Block a user