Skip to main content

mnemo/
init.rs

1//! Commande `mnemo init` : initialisation et assistant d'onboarding.
2//!
3//! `mnemo init` crée la configuration et la base si nécessaire, puis affiche le
4//! snippet Bash à installer. `mnemo init --wizard` ajoute un accompagnement
5//! interactif (intégration Bash, import de l'historique, diagnostic) en
6//! réutilisant la logique existante de `doctor`, `shell` et `importer`.
7//!
8//! Invariant de sûreté : le wizard est strictement non destructif. Il n'efface
9//! et ne purge jamais de données. En contexte non interactif sans `--yes`, il
10//! refuse de s'exécuter plutôt que de prendre des décisions silencieuses.
11
12use anyhow::Result;
13use std::io::{self, IsTerminal, Write};
14use std::path::{Path, PathBuf};
15
16use crate::{backup, config, db, doctor, importer, shell};
17
18/// Point d'entrée de `mnemo init`.
19pub fn run(wizard: bool, assume_yes: bool) -> Result<()> {
20    if wizard {
21        run_wizard(assume_yes)
22    } else {
23        run_basic()
24    }
25}
26
27/// Point d'entrée de `mnemo shell upgrade`.
28///
29/// Met à niveau le bloc d'intégration Bash existant vers la version courante
30/// (capture de `MNEMO_SESSION_ID`). Non destructif : sauvegarde systématique,
31/// aucun bloc créé s'il est absent.
32pub fn run_shell_upgrade() -> Result<()> {
33    let Some(bashrc) = bashrc_path() else {
34        eprintln!("Répertoire personnel introuvable : mise à niveau impossible.");
35        std::process::exit(1);
36    };
37
38    match shell::upgrade_block(&bashrc)? {
39        shell::ShellUpgrade::NotInstalled => {
40            println!(
41                "Aucune intégration Bash mnemo détectée dans {}.",
42                display_home(&bashrc)
43            );
44            println!("Lancez `mnemo init` pour l'installer.");
45        }
46        shell::ShellUpgrade::AlreadyCurrent => {
47            println!(
48                "Intégration Bash mnemo déjà à jour dans {} : aucun changement.",
49                display_home(&bashrc)
50            );
51        }
52        shell::ShellUpgrade::Upgraded { backup } => {
53            println!("Intégration Bash mnemo détectée : obsolète.");
54            println!("Sauvegarde créée : {}", display_home(&backup));
55            println!("Bloc d'intégration Bash mnemo mis à niveau.");
56            println!("Prochaine étape : source ~/.bashrc");
57        }
58    }
59    Ok(())
60}
61
62/// Initialisation simple (comportement historique de `mnemo init`).
63fn run_basic() -> Result<()> {
64    ensure_config_and_db()?;
65
66    println!();
67    println!("Ajoutez ces lignes à votre ~/.bashrc :");
68    println!("------------------------------------------------------------");
69    print!("{}", shell::bashrc_snippet());
70    println!("------------------------------------------------------------");
71    println!("Puis rechargez : source ~/.bashrc");
72    Ok(())
73}
74
75/// Assistant d'onboarding interactif.
76fn run_wizard(assume_yes: bool) -> Result<()> {
77    let interactive = io::stdin().is_terminal();
78    if !interactive && !assume_yes {
79        eprintln!(
80            "mnemo init --wizard nécessite un terminal interactif. En contexte non \
81             interactif (script, CI, pipe), relancez avec --yes pour accepter les \
82             choix sûrs par défaut (jamais de suppression)."
83        );
84        std::process::exit(1);
85    }
86
87    println!("Bienvenue dans mnemo.");
88    println!();
89    println!(
90        "mnemo va vérifier votre installation locale, initialiser la configuration \
91         si nécessaire, puis vous proposer l'intégration Bash et l'import de votre \
92         historique existant. Aucune donnée n'est jamais supprimée."
93    );
94    println!();
95
96    print_install_overview()?;
97    println!();
98
99    ensure_config_and_db()?;
100    println!();
101
102    setup_bash_integration(interactive)?;
103    println!();
104
105    match bash_history_path() {
106        Some(hist) if hist.exists() => {
107            let question = format!("Importer {} maintenant ?", display_home(&hist));
108            if ask(&question, false, interactive)? {
109                import_history(&hist)?;
110            }
111        }
112        Some(hist) => {
113            println!(
114                "Historique {} introuvable : import ignoré.",
115                display_home(&hist)
116            );
117        }
118        None => {}
119    }
120    println!();
121
122    if ask("Lancer mnemo doctor ?", true, interactive)? {
123        println!();
124        // Le diagnostic est optionnel : une erreur ne doit pas faire échouer
125        // l'onboarding déjà réalisé.
126        if let Err(err) = doctor::run(false, false) {
127            eprintln!("Diagnostic ignoré : {err}");
128        }
129    }
130
131    print_next_steps();
132    Ok(())
133}
134
135/// Crée la configuration et la base si nécessaire (idempotent) et resserre les
136/// permissions. Affiche les chemins concernés, comme le `mnemo init` historique.
137fn ensure_config_and_db() -> Result<()> {
138    let cfg_path = config::config_path()?;
139    if cfg_path.exists() {
140        println!("Configuration existante : {}", cfg_path.display());
141    } else {
142        config::Config::default().save(&cfg_path)?;
143        println!("Configuration créée : {}", cfg_path.display());
144    }
145    // Idempotent : resserre les permissions même si la config préexistait.
146    config::harden_file(&cfg_path);
147
148    let db_path = config::db_path()?;
149    db::open(&db_path)?;
150    config::harden_file(&db_path);
151    println!("Base de données : {}", db_path.display());
152    Ok(())
153}
154
155/// Étape d'intégration Bash du wizard : installe le bloc s'il est absent, le met
156/// à niveau s'il est obsolète, ou ne fait rien s'il est déjà à jour. Toujours
157/// non destructif (sauvegarde avant toute écriture).
158fn setup_bash_integration(interactive: bool) -> Result<()> {
159    let Some(bashrc) = bashrc_path() else {
160        eprintln!("Répertoire personnel introuvable : intégration Bash ignorée.");
161        return Ok(());
162    };
163    let content = std::fs::read_to_string(&bashrc).unwrap_or_default();
164
165    match shell::block_state(&content) {
166        shell::BlockState::Absent => {
167            if ask(
168                "Ajouter l'intégration Bash dans ~/.bashrc ?",
169                true,
170                interactive,
171            )? {
172                add_bash_integration()?;
173            }
174        }
175        shell::BlockState::Legacy => {
176            if ask(
177                "Mettre à jour l'intégration Bash existante (active les sessions) ?",
178                true,
179                interactive,
180            )? {
181                upgrade_bash_integration(&bashrc)?;
182            }
183        }
184        shell::BlockState::Current => {
185            println!(
186                "Intégration Bash déjà à jour dans {} : aucun changement.",
187                display_home(&bashrc)
188            );
189        }
190    }
191    Ok(())
192}
193
194/// Ajoute le bloc d'intégration Bash au `.bashrc` (sauvegarde automatique,
195/// jamais de doublon) via [`shell::install_block`].
196fn add_bash_integration() -> Result<()> {
197    let Some(bashrc) = bashrc_path() else {
198        eprintln!("Répertoire personnel introuvable : intégration Bash ignorée.");
199        return Ok(());
200    };
201    if shell::install_block(&bashrc)? {
202        println!(
203            "Intégration Bash ajoutée à {} (sauvegarde du fichier créée).",
204            display_home(&bashrc)
205        );
206    } else {
207        println!(
208            "Intégration Bash déjà présente dans {} : aucun changement.",
209            display_home(&bashrc)
210        );
211    }
212    Ok(())
213}
214
215/// Met à niveau un bloc d'intégration Bash obsolète via [`shell::upgrade_block`].
216fn upgrade_bash_integration(bashrc: &Path) -> Result<()> {
217    match shell::upgrade_block(bashrc)? {
218        shell::ShellUpgrade::Upgraded { backup } => {
219            println!(
220                "Intégration Bash mise à niveau dans {} (sauvegarde {} créée).",
221                display_home(bashrc),
222                display_home(&backup)
223            );
224        }
225        shell::ShellUpgrade::AlreadyCurrent => {
226            println!("Intégration Bash déjà à jour : aucun changement.");
227        }
228        shell::ShellUpgrade::NotInstalled => {
229            // État inattendu (le bloc a disparu entre-temps) : installation propre.
230            add_bash_integration()?;
231        }
232    }
233    Ok(())
234}
235
236/// Importe l'historique Bash pointé par `path` dans la base.
237fn import_history(path: &Path) -> Result<()> {
238    let cfg = config::Config::load()?;
239    let conn = db::open(&config::db_path()?)?;
240    let stats = importer::import_bash_history(&conn, path, &cfg)?;
241    println!("Import depuis {}", display_home(path));
242    println!("  Importées          : {}", stats.imported);
243    println!("  Sensibles ignorées : {}", stats.skipped_sensitive);
244    println!("  Doublons ignorés   : {}", stats.skipped_duplicate);
245    Ok(())
246}
247
248/// Affiche l'emplacement des données locales gérées par mnemo.
249fn print_install_overview() -> Result<()> {
250    println!("Données :");
251    println!(
252        "  Configuration : {}",
253        display_home(&config::config_path()?)
254    );
255    println!("  Base SQLite   : {}", display_home(&config::db_path()?));
256    if let Ok(exe) = std::env::current_exe() {
257        println!("  Binaire       : {}", display_home(&exe));
258    }
259    if let Ok(backups) = backup::backups_dir() {
260        println!("  Sauvegardes   : {}", display_home(&backups));
261    }
262    Ok(())
263}
264
265/// Affiche les commandes utiles après l'onboarding.
266fn print_next_steps() {
267    println!();
268    println!("Prochaines étapes :");
269    println!("  source ~/.bashrc");
270    println!("  mnemo import");
271    println!("  mnemo search");
272    println!("  mnemo doctor");
273}
274
275/// Pose une question oui/non. En mode non interactif (atteint uniquement avec
276/// `--yes`), retient la réponse par défaut.
277fn ask(question: &str, default_yes: bool, interactive: bool) -> Result<bool> {
278    if !interactive {
279        return Ok(default_yes);
280    }
281    let suffix = if default_yes { "[O/n]" } else { "[o/N]" };
282    print!("{question} {suffix} ");
283    io::stdout().flush()?;
284
285    let mut answer = String::new();
286    io::stdin().read_line(&mut answer)?;
287    let answer = answer.trim().to_lowercase();
288    if answer.is_empty() {
289        return Ok(default_yes);
290    }
291    Ok(answer == "o" || answer == "oui" || answer == "y" || answer == "yes")
292}
293
294fn bashrc_path() -> Option<PathBuf> {
295    dirs::home_dir().map(|h| h.join(".bashrc"))
296}
297
298fn bash_history_path() -> Option<PathBuf> {
299    dirs::home_dir().map(|h| h.join(".bash_history"))
300}
301
302/// Raccourcit un chemin sous le répertoire personnel en `~/...` pour l'affichage.
303fn display_home(path: &Path) -> String {
304    if let Some(home) = dirs::home_dir() {
305        if let Ok(rest) = path.strip_prefix(&home) {
306            return format!("~/{}", rest.display());
307        }
308    }
309    path.display().to_string()
310}