- #65: update policies + per-agent version pinning - add settings.update_policy (stable/latest/skip) - add AgentDef.pin_version - honor both in update_cmd - add update_policy tests - #69: REPL color theme CLI command - add am theme [name] (show/switch) - persist selected theme to config - #72: doctor checks local model runtimes - verify ollama and llama-server on PATH - for ollama, also check server responds (ollama ls) Version 0.4.4
This commit is contained in:
+1
-1
@@ -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.3` · 🦀 Rust 2021 · ✅ Windows · ✅ Linux · ✅ macOS
|
> 📦 Version : `0.4.4` · 🦀 Rust 2021 · ✅ Windows · ✅ Linux · ✅ macOS
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Generated
+1
-1
@@ -21,7 +21,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "agent-manager"
|
name = "agent-manager"
|
||||||
version = "0.4.3"
|
version = "0.4.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "agent-manager"
|
name = "agent-manager"
|
||||||
version = "0.4.3"
|
version = "0.4.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"
|
||||||
|
|||||||
+3
-3
@@ -446,14 +446,14 @@ moins de 2 secondes.
|
|||||||
| [#62](https://git.dracodev.net/Projets/agent-manager/issues/62) | am lab — benchmark : même tâche sur N agents | L |
|
| [#62](https://git.dracodev.net/Projets/agent-manager/issues/62) | am lab — benchmark : même tâche sur N agents | L |
|
||||||
| [#63](https://git.dracodev.net/Projets/agent-manager/issues/63) | am audit — qui a modifié quoi quand | M |
|
| [#63](https://git.dracodev.net/Projets/agent-manager/issues/63) | am audit — qui a modifié quoi quand | M |
|
||||||
| [#64](https://git.dracodev.net/Projets/agent-manager/issues/64) | am update --rollback — backup automatique avant mise à jour | M |
|
| [#64](https://git.dracodev.net/Projets/agent-manager/issues/64) | am update --rollback — backup automatique avant mise à jour | M |
|
||||||
| [#65](https://git.dracodev.net/Projets/agent-manager/issues/65) | Politiques — pin de version, settings.update_policy | S |
|
| [#65](https://git.dracodev.net/Projets/agent-manager/issues/65) | ✅ Politiques — pin de version, settings.update_policy | S |
|
||||||
| [#66](https://git.dracodev.net/Projets/agent-manager/issues/66) | Synchronisation git automatique — am sync | L |
|
| [#66](https://git.dracodev.net/Projets/agent-manager/issues/66) | Synchronisation git automatique — am sync | L |
|
||||||
| [#67](https://git.dracodev.net/Projets/agent-manager/issues/67) | Partage de catalogue d'équipe (include par URL) | S |
|
| [#67](https://git.dracodev.net/Projets/agent-manager/issues/67) | Partage de catalogue d'équipe (include par URL) | S |
|
||||||
| [#68](https://git.dracodev.net/Projets/agent-manager/issues/68) | am migrate — assistant de transfert machine A → B | M |
|
| [#68](https://git.dracodev.net/Projets/agent-manager/issues/68) | am migrate — assistant de transfert machine A → B | M |
|
||||||
| [#69](https://git.dracodev.net/Projets/agent-manager/issues/69) | Thèmes couleurs du REPL | S |
|
| [#69](https://git.dracodev.net/Projets/agent-manager/issues/69) | ✅ Thèmes couleurs du REPL | S |
|
||||||
| [#70](https://git.dracodev.net/Projets/agent-manager/issues/70) | am models — inventaire des modèles locaux (ollama, llama.cpp, LM Studio) | M |
|
| [#70](https://git.dracodev.net/Projets/agent-manager/issues/70) | am models — inventaire des modèles locaux (ollama, llama.cpp, LM Studio) | M |
|
||||||
| [#71](https://git.dracodev.net/Projets/agent-manager/issues/71) | Lien agent ↔ modèle — am run --model | M |
|
| [#71](https://git.dracodev.net/Projets/agent-manager/issues/71) | Lien agent ↔ modèle — am run --model | M |
|
||||||
| [#72](https://git.dracodev.net/Projets/agent-manager/issues/72) | doctor vérifie ollama / llama-server comme n'importe quel outil | S |
|
| [#72](https://git.dracodev.net/Projets/agent-manager/issues/72) | ✅ doctor vérifie ollama / llama-server comme n'importe quel outil | S |
|
||||||
| [#73](https://git.dracodev.net/Projets/agent-manager/issues/73) | am models prune — purge des modèles inutilisés | S |
|
| [#73](https://git.dracodev.net/Projets/agent-manager/issues/73) | am models prune — purge des modèles inutilisés | S |
|
||||||
| [#74](https://git.dracodev.net/Projets/agent-manager/issues/74) | i18n — messages EN/FR | L |
|
| [#74](https://git.dracodev.net/Projets/agent-manager/issues/74) | i18n — messages EN/FR | L |
|
||||||
| [#75](https://git.dracodev.net/Projets/agent-manager/issues/75) | Plugin scripts (hooks avancés, intégration CI) | M |
|
| [#75](https://git.dracodev.net/Projets/agent-manager/issues/75) | Plugin scripts (hooks avancés, intégration CI) | M |
|
||||||
|
|||||||
@@ -157,6 +157,11 @@ pub enum Command {
|
|||||||
#[arg(long, value_name = "DIR")]
|
#[arg(long, value_name = "DIR")]
|
||||||
output: Option<PathBuf>,
|
output: Option<PathBuf>,
|
||||||
},
|
},
|
||||||
|
/// Set or show the active color theme (persisted for the next run)
|
||||||
|
Theme {
|
||||||
|
/// Theme name (omit to list available themes)
|
||||||
|
name: Option<String>,
|
||||||
|
},
|
||||||
/// Internal: list agent names for shell completions (hidden)
|
/// Internal: list agent names for shell completions (hidden)
|
||||||
#[command(name = "_agents", hide = true)]
|
#[command(name = "_agents", hide = true)]
|
||||||
Agents {
|
Agents {
|
||||||
|
|||||||
+3
-1
@@ -28,6 +28,7 @@ pub mod sessions_cmd;
|
|||||||
pub mod stats_cmd;
|
pub mod stats_cmd;
|
||||||
pub mod self_update;
|
pub mod self_update;
|
||||||
pub mod status_cmd;
|
pub mod status_cmd;
|
||||||
|
pub mod theme_cmd;
|
||||||
pub mod timeline_cmd;
|
pub mod timeline_cmd;
|
||||||
pub mod tip_cmd;
|
pub mod tip_cmd;
|
||||||
pub mod uninstall_cmd;
|
pub mod uninstall_cmd;
|
||||||
@@ -37,7 +38,7 @@ pub mod version_cmd;
|
|||||||
|
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::cli::Command;
|
use crate::cli::Command;
|
||||||
use crate::config::{AgentDef, MethodSpec};
|
use crate::config::{AgentDef, MethodSpec, UpdatePolicy};
|
||||||
use crate::state::InstalledEntry;
|
use crate::state::InstalledEntry;
|
||||||
use anyhow::{anyhow, bail, Result};
|
use anyhow::{anyhow, bail, Result};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
@@ -164,6 +165,7 @@ pub fn execute_command(app: &App, cmd: &Command) -> Result<i32> {
|
|||||||
Command::Version => version_cmd::run(app),
|
Command::Version => version_cmd::run(app),
|
||||||
Command::Tip { random } => tip_cmd::run(app, *random),
|
Command::Tip { random } => tip_cmd::run(app, *random),
|
||||||
Command::Config(sub) => config_cmd::run(app, sub),
|
Command::Config(sub) => config_cmd::run(app, sub),
|
||||||
|
Command::Theme { name } => theme_cmd::run(app, name.as_deref()),
|
||||||
Command::Doctor { fix } => doctor_cmd::run(app, *fix),
|
Command::Doctor { fix } => doctor_cmd::run(app, *fix),
|
||||||
Command::Run { agent, args } => run_cmd::run(app, agent, args),
|
Command::Run { agent, args } => run_cmd::run(app, agent, args),
|
||||||
Command::Completion { shell, installed } => completion_cmd::run(*shell, *installed),
|
Command::Completion { shell, installed } => completion_cmd::run(*shell, *installed),
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
//! theme: switch the active color theme and persist it for the next start.
|
||||||
|
//!
|
||||||
|
//! With no argument, prints the current theme and the available list.
|
||||||
|
//! With a name, switches to that theme (also used by the REPL 'theme' command).
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::theme;
|
||||||
|
|
||||||
|
pub fn run(app: &App, name: Option<&str>) -> Result<i32> {
|
||||||
|
if let Some(name) = name {
|
||||||
|
let theme = theme::find(name).ok_or_else(|| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"unknown theme '{name}' — available: {}",
|
||||||
|
theme::names().join(", ")
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
app.set_theme(theme);
|
||||||
|
crate::commands::config_cmd::persist_setting(app, "settings.theme", theme.name)?;
|
||||||
|
app.log.success(&format!(
|
||||||
|
"theme set to {} ({})",
|
||||||
|
theme.name,
|
||||||
|
theme.title
|
||||||
|
));
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No argument: show current and list.
|
||||||
|
let current = app.theme();
|
||||||
|
if app.json() {
|
||||||
|
crate::output::print_json(&serde_json::json!({
|
||||||
|
"current": current.name,
|
||||||
|
"title": current.title,
|
||||||
|
"available": theme::names(),
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
println!("current theme: {} — {}", current.name, current.title);
|
||||||
|
println!("available themes: {}", theme::names().join(", "));
|
||||||
|
println!("use 'am theme <name>' to switch");
|
||||||
|
}
|
||||||
|
Ok(0)
|
||||||
|
}
|
||||||
@@ -11,7 +11,17 @@ use std::path::PathBuf;
|
|||||||
|
|
||||||
pub fn run(app: &App, agent_name: Option<&str>, all: bool) -> Result<i32> {
|
pub fn run(app: &App, agent_name: Option<&str>, all: bool) -> Result<i32> {
|
||||||
let runner = SystemRunner::new(app.dry_run(), app.cli.verbose, &app.log);
|
let runner = SystemRunner::new(app.dry_run(), app.cli.verbose, &app.log);
|
||||||
|
|
||||||
|
let policy = app.config.settings.update_policy.unwrap_or_default();
|
||||||
|
if policy == UpdatePolicy::None && all {
|
||||||
|
app.log.warn("update policy is 'none'; 'am update --all' is a no-op");
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(name) = agent_name {
|
if let Some(name) = agent_name {
|
||||||
|
if policy == UpdatePolicy::None && !app.yes() {
|
||||||
|
app.log.warn("update policy is 'none'; use --yes to force a single-agent update");
|
||||||
|
}
|
||||||
return update_one(app, &runner, name);
|
return update_one(app, &runner, name);
|
||||||
}
|
}
|
||||||
if all {
|
if all {
|
||||||
@@ -31,6 +41,30 @@ pub fn run(app: &App, agent_name: Option<&str>, all: bool) -> Result<i32> {
|
|||||||
|
|
||||||
fn update_one(app: &App, runner: &dyn Runner, name: &str) -> Result<i32> {
|
fn update_one(app: &App, runner: &dyn Runner, name: &str) -> Result<i32> {
|
||||||
let agent = require_agent(app, name)?;
|
let agent = require_agent(app, name)?;
|
||||||
|
|
||||||
|
// Issue #65: honor per-agent pinned version.
|
||||||
|
if let Some(pin) = agent.pin_version.as_deref() {
|
||||||
|
let current = app
|
||||||
|
.state
|
||||||
|
.get(&agent.name)
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.and_then(|e| e.version.clone());
|
||||||
|
if current.as_deref() == Some(pin) {
|
||||||
|
app.log.success(&format!(
|
||||||
|
"{} is pinned to v{pin} and already at that version",
|
||||||
|
agent.title()
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
app.log.warn(&format!(
|
||||||
|
"{} is pinned to v{pin} but currently at {}; update skipped",
|
||||||
|
agent.title(),
|
||||||
|
current.as_deref().unwrap_or("unknown")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
|
||||||
let Some(entry) = app.state.get(&agent.name).ok().flatten() else {
|
let Some(entry) = app.state.get(&agent.name).ok().flatten() else {
|
||||||
bail!(
|
bail!(
|
||||||
"agent '{}' is not installed by agent-manager — use 'am install {}'",
|
"agent '{}' is not installed by agent-manager — use 'am install {}'",
|
||||||
|
|||||||
@@ -102,6 +102,35 @@ pub struct Settings {
|
|||||||
/// lines (global; project profiles can add their own).
|
/// lines (global; project profiles can add their own).
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub hooks: BTreeMap<String, Vec<String>>,
|
pub hooks: BTreeMap<String, Vec<String>>,
|
||||||
|
/// Update policy: how aggressively 'am update' should act (issue #65).
|
||||||
|
#[serde(default)]
|
||||||
|
pub update_policy: Option<UpdatePolicy>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Policy controlling how aggressively 'am update' upgrades installed agents.
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum UpdatePolicy {
|
||||||
|
/// Never update automatically; 'am update --all' is a no-op.
|
||||||
|
None,
|
||||||
|
/// Default: update only when explicitly requested by name or with --all.
|
||||||
|
#[default]
|
||||||
|
Manual,
|
||||||
|
/// Update only to the next patch/minor that is known safe (future use).
|
||||||
|
Safe,
|
||||||
|
/// Always update to the latest available version.
|
||||||
|
Latest,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UpdatePolicy {
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
UpdatePolicy::None => "none",
|
||||||
|
UpdatePolicy::Manual => "manual",
|
||||||
|
UpdatePolicy::Safe => "safe",
|
||||||
|
UpdatePolicy::Latest => "latest",
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A project profile: everything am needs to know about one project.
|
/// A project profile: everything am needs to know about one project.
|
||||||
@@ -172,6 +201,9 @@ pub struct AgentDef {
|
|||||||
/// Known version (otherwise detected via --version).
|
/// Known version (otherwise detected via --version).
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub version: Option<String>,
|
pub version: Option<String>,
|
||||||
|
/// Pinned version: 'am update' will refuse to install a different one (issue #65).
|
||||||
|
#[serde(default)]
|
||||||
|
pub pin_version: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub tags: Vec<String>,
|
pub tags: Vec<String>,
|
||||||
/// When false, the agent is listed but cannot be installed locally.
|
/// When false, the agent is listed but cannot be installed locally.
|
||||||
|
|||||||
+40
-2
@@ -76,8 +76,7 @@ pub fn run(app: &App, runner: &dyn Runner) -> DoctorReport {
|
|||||||
let empty = BTreeMap::new();
|
let empty = BTreeMap::new();
|
||||||
if let Ok(out) = runner.run(tool, &probe_args(tool), &empty, None, true) {
|
if let Ok(out) = runner.run(tool, &probe_args(tool), &empty, None, true) {
|
||||||
if out.code == 0 {
|
if out.code == 0 {
|
||||||
let combined = format!("{}
|
let combined = format!("{}\n{}", out.stdout, out.stderr);
|
||||||
{}", out.stdout, out.stderr);
|
|
||||||
if let Some(v) = crate::version::find_version(&combined) {
|
if let Some(v) = crate::version::find_version(&combined) {
|
||||||
detail = format!("{} (v{v})", path.display());
|
detail = format!("{} (v{v})", path.display());
|
||||||
}
|
}
|
||||||
@@ -113,6 +112,45 @@ pub fn run(app: &App, runner: &dyn Runner) -> DoctorReport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- local model runtimes (issue #72) -------------------------------
|
||||||
|
for runtime in ["ollama", "llama-server"] {
|
||||||
|
if let Some(path) = runner.which(runtime) {
|
||||||
|
let mut detail = path.display().to_string();
|
||||||
|
let empty = BTreeMap::new();
|
||||||
|
if let Ok(out) = runner.run(runtime, &["--version".to_string()], &empty, None, true) {
|
||||||
|
if out.code == 0 {
|
||||||
|
let combined = format!("{}\n{}", out.stdout, out.stderr);
|
||||||
|
if let Some(v) = crate::version::find_version(&combined) {
|
||||||
|
detail = format!("{} (v{})", path.display(), v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// For ollama, also check that the server answers.
|
||||||
|
let status = if runtime == "ollama" {
|
||||||
|
match runner.run(runtime, &["ls".to_string()], &empty, None, true) {
|
||||||
|
Ok(out) if out.code == 0 => Status::Ok,
|
||||||
|
_ => {
|
||||||
|
detail.push_str(" — server does not answer (ollama serve?)");
|
||||||
|
Status::Warn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Status::Ok
|
||||||
|
};
|
||||||
|
checks.push(Check {
|
||||||
|
name: runtime.to_string(),
|
||||||
|
status,
|
||||||
|
detail,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
checks.push(Check {
|
||||||
|
name: runtime.to_string(),
|
||||||
|
status: Status::Ok,
|
||||||
|
detail: "not installed (optional)".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- configuration ---------------------------------------------------
|
// --- configuration ---------------------------------------------------
|
||||||
match &app.config_origin {
|
match &app.config_origin {
|
||||||
Some(path) => checks.push(Check {
|
Some(path) => checks.push(Check {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ fn report_contains_core_checks() {
|
|||||||
let runner = SystemRunner::new(false, false, &app.log);
|
let runner = SystemRunner::new(false, false, &app.log);
|
||||||
let report = doctor::run(&app, &runner);
|
let report = doctor::run(&app, &runner);
|
||||||
let names: Vec<&str> = report.checks.iter().map(|c| c.name.as_str()).collect();
|
let names: Vec<&str> = report.checks.iter().map(|c| c.name.as_str()).collect();
|
||||||
for expected in ["git", "config", "config-schema", "state", "install-dir", "bin-on-path"] {
|
for expected in ["git", "config", "config-schema", "state", "install-dir", "bin-on-path", "ollama", "llama-server"] {
|
||||||
assert!(
|
assert!(
|
||||||
names.contains(&expected),
|
names.contains(&expected),
|
||||||
"missing check '{expected}' in {names:?}"
|
"missing check '{expected}' in {names:?}"
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
use agent_manager::commands::update_cmd;
|
||||||
|
use agent_manager::config::UpdatePolicy;
|
||||||
|
use agent_manager::state::{InstalledEntry, StateFile};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
mod common;
|
||||||
|
|
||||||
|
fn installed_state(name: &str, version: &str) -> StateFile {
|
||||||
|
StateFile {
|
||||||
|
version: 3,
|
||||||
|
installed: BTreeMap::from([(
|
||||||
|
name.to_string(),
|
||||||
|
InstalledEntry {
|
||||||
|
name: name.to_string(),
|
||||||
|
version: Some(version.to_string()),
|
||||||
|
method: "binary".to_string(),
|
||||||
|
install_dir: "/tmp".to_string(),
|
||||||
|
bins: vec![name.to_string()],
|
||||||
|
run: name.to_string(),
|
||||||
|
installed_at: "2026-01-01T00:00:00Z".to_string(),
|
||||||
|
updated_at: None,
|
||||||
|
pid: None,
|
||||||
|
started_at: None,
|
||||||
|
},
|
||||||
|
)]),
|
||||||
|
sessions_count: 0,
|
||||||
|
last_used: None,
|
||||||
|
annotations: BTreeMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn agent_yaml(name: &str, pin: Option<&str>) -> String {
|
||||||
|
let pin_line = pin
|
||||||
|
.map(|v| format!(" pin_version: \"{v}\"\n"))
|
||||||
|
.unwrap_or_default();
|
||||||
|
format!(
|
||||||
|
"agents:\n - name: {name}\n display_name: {name}\n run: \"{name}\"\n install:\n type: binary\n binary: {name}\n{pin_line}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn update_policy_none_blocks_all() {
|
||||||
|
let body = agent_yaml("pi", None);
|
||||||
|
let mut app = common::test_app("policy_none", &["--yes"]);
|
||||||
|
app.config.settings.update_policy = Some(UpdatePolicy::None);
|
||||||
|
app.state.save(&installed_state("pi", "0.1.0")).unwrap();
|
||||||
|
let code = update_cmd::run(&app, None, true).unwrap();
|
||||||
|
assert_eq!(code, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pinned_agent_is_skipped() {
|
||||||
|
let body = agent_yaml("pi", Some("0.1.0"));
|
||||||
|
let app = common::test_app("pinned_ok", &[]);
|
||||||
|
app.state.save(&installed_state("pi", "0.1.0")).unwrap();
|
||||||
|
let code = update_cmd::run(&app, Some("pi"), false).unwrap();
|
||||||
|
assert_eq!(code, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pinned_agent_mismatch_warns() {
|
||||||
|
let body = agent_yaml("pi", Some("0.1.0"));
|
||||||
|
let app = common::test_app("pinned_warn", &[]);
|
||||||
|
app.state.save(&installed_state("pi", "0.2.0")).unwrap();
|
||||||
|
let code = update_cmd::run(&app, Some("pi"), false).unwrap();
|
||||||
|
assert_eq!(code, 0);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user