Skip to main content

mnemo/lifecycle/
update.rs

1//! `mnemo update` : vérifie la disponibilité d'une nouvelle version. Par
2//! défaut, n'installe rien ; en terminal interactif, propose d'enchaîner
3//! `mnemo upgrade`, et l'option `--upgrade` lance directement l'installation.
4
5use std::io::{self, IsTerminal, Write};
6
7use anyhow::Result;
8use serde::Serialize;
9
10use super::github::fetch_latest_release;
11use super::{current_version, normalize_tag, target_triple, update_available};
12
13/// Rapport de vérification de mise à jour (sérialisable en JSON).
14#[derive(Debug, Clone, Serialize)]
15pub struct UpdateReport {
16    pub current_version: String,
17    pub latest_version: String,
18    pub update_available: bool,
19    pub asset_target: String,
20}
21
22impl UpdateReport {
23    /// Construit le rapport à partir des versions courante et distante.
24    pub fn new(current: &str, latest: &str) -> Self {
25        Self {
26            current_version: normalize_tag(current),
27            latest_version: normalize_tag(latest),
28            update_available: update_available(current, latest),
29            asset_target: target_triple().to_string(),
30        }
31    }
32}
33
34/// Interprète une réponse interactive : `o`/`oui`/`y`/`yes` (insensible à la
35/// casse) valent « oui » ; tout le reste, y compris une ligne vide (`Entrée`),
36/// vaut « non ». Fonction pure, testable sans terminal.
37fn interpret_yes(answer: &str) -> bool {
38    let a = answer.trim().to_lowercase();
39    a == "o" || a == "oui" || a == "y" || a == "yes"
40}
41
42/// Demande, en terminal interactif uniquement, s'il faut installer la mise à
43/// jour maintenant. Renvoie `false` sans rien afficher dès que stdin **ou**
44/// stdout n'est pas un terminal (CI, script, cron, pipe) : `update` reste alors
45/// une simple vérification. Réponse par défaut (`Entrée`) : non.
46fn prompt_install_now() -> Result<bool> {
47    if !io::stdin().is_terminal() || !io::stdout().is_terminal() {
48        return Ok(false);
49    }
50    print!("Installer maintenant avec `mnemo upgrade` ? [o/N] ");
51    io::stdout().flush()?;
52    let mut answer = String::new();
53    io::stdin().read_line(&mut answer)?;
54    Ok(interpret_yes(&answer))
55}
56
57/// Point d'entrée de la commande.
58///
59/// - `json` : sortie machine, vérification seule (aucune proposition d'upgrade).
60/// - `upgrade` : si une mise à jour est disponible, enchaîne `mnemo upgrade`.
61/// - `assume_yes` : avec `upgrade`, installe sans confirmation (automatisation).
62/// - `require_signature` : avec `upgrade`, rend la vérification Sigstore
63///   obligatoire (transmis tel quel à `upgrade`).
64pub fn run(json: bool, upgrade: bool, assume_yes: bool, require_signature: bool) -> Result<()> {
65    let current = current_version();
66    let latest = fetch_latest_release()?.tag_name;
67    let report = UpdateReport::new(&current, &latest);
68
69    if json {
70        // Mode machine : on reste strictement en vérification, sans prompt ni
71        // installation, pour garder une sortie exploitable par un script.
72        println!("{}", serde_json::to_string_pretty(&report)?);
73        return Ok(());
74    }
75
76    println!("Version installée : {}", report.current_version);
77    println!("Dernière version  : {}", report.latest_version);
78
79    if !report.update_available {
80        println!("mnemo est à jour ✓");
81        return Ok(());
82    }
83
84    println!("Mise à jour disponible ✓");
85
86    // Option explicite `--upgrade` : on lance l'installation. `--yes` et
87    // `--require-signature` sont transmis tels quels ; sans `--yes`, c'est
88    // `upgrade` lui-même qui demandera la confirmation finale (un seul prompt,
89    // pas de double question).
90    if upgrade {
91        return super::upgrade::run(false, assume_yes, None, None, require_signature);
92    }
93
94    // Sans `--upgrade` : proposer l'installation uniquement en terminal
95    // interactif. Le consentement donné ici vaut confirmation, donc on appelle
96    // `upgrade` avec `assume_yes = true` pour éviter un second prompt. La
97    // politique de signature reste celle demandée (`require_signature`).
98    if prompt_install_now()? {
99        return super::upgrade::run(false, true, None, None, require_signature);
100    }
101
102    // Mode vérification seule (non interactif, ou refus de l'utilisateur) :
103    // on conserve l'indication classique.
104    println!("  Lancez `mnemo upgrade` pour l'installer.");
105    Ok(())
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn rapport_mise_a_jour_disponible() {
114        let r = UpdateReport::new("v0.4.0", "v0.5.0");
115        assert_eq!(r.current_version, "v0.4.0");
116        assert_eq!(r.latest_version, "v0.5.0");
117        assert!(r.update_available);
118        assert!(r.asset_target.contains("linux-musl"));
119    }
120
121    #[test]
122    fn rapport_a_jour() {
123        let r = UpdateReport::new("v0.5.0", "v0.5.0");
124        assert!(!r.update_available);
125    }
126
127    #[test]
128    fn rapport_serialise_json() {
129        let r = UpdateReport::new("0.4.0", "0.5.0");
130        let json = serde_json::to_value(&r).unwrap();
131        assert_eq!(json["current_version"], "v0.4.0");
132        assert_eq!(json["latest_version"], "v0.5.0");
133        assert_eq!(json["update_available"], true);
134        assert!(json["asset_target"].is_string());
135    }
136
137    #[test]
138    fn interpret_yes_accepte_les_reponses_affirmatives() {
139        for ok in ["o", "O", "oui", "OUI", "y", "Y", "yes", "Yes", " oui "] {
140            assert!(interpret_yes(ok), "« {ok} » devrait valoir oui");
141        }
142    }
143
144    #[test]
145    fn interpret_yes_refuse_le_reste_et_la_ligne_vide() {
146        for no in ["", " ", "\n", "n", "non", "no", "nope", "x", "1"] {
147            assert!(!interpret_yes(no), "« {no} » devrait valoir non");
148        }
149    }
150}