Skip to main content

mnemo/
cli.rs

1use clap::{Parser, Subcommand, ValueEnum};
2use clap_complete::Shell;
3use std::path::PathBuf;
4
5use crate::export::ExportFormat;
6use crate::runbook::{GroupBy, RunbookFormat};
7
8#[derive(Parser, Debug)]
9#[command(
10    name = "mnemo",
11    version,
12    about = "Navigation et recherche dans l'historique Bash",
13    long_about = None,
14)]
15pub struct Cli {
16    #[command(subcommand)]
17    pub command: Command,
18}
19
20#[derive(Subcommand, Debug)]
21pub enum Command {
22    /// Initialise la configuration et la base de données.
23    Init {
24        /// Lance l'assistant d'onboarding interactif (intégration Bash, import,
25        /// diagnostic). Toutes les actions proposées sont non destructives.
26        #[arg(long)]
27        wizard: bool,
28        /// En mode `--wizard` non interactif, accepte les choix sûrs par défaut
29        /// sans rien supprimer ni purger.
30        #[arg(long)]
31        yes: bool,
32    },
33
34    /// Génère un script de complétion shell sur stdout (bash, zsh, fish).
35    ///
36    /// mnemo n'écrit jamais dans vos fichiers shell : redirigez la sortie vers
37    /// l'emplacement adéquat (voir `docs/UX_ONBOARDING.md`).
38    Completions {
39        /// Shell cible.
40        #[arg(value_enum)]
41        shell: CompletionShell,
42    },
43
44    /// Importe l'historique Bash (~/.bash_history par défaut) dans la base.
45    Import {
46        /// Fichier d'historique à importer.
47        #[arg(long)]
48        file: Option<PathBuf>,
49    },
50
51    /// Ajoute une commande dans la base.
52    Add {
53        /// Commande à enregistrer.
54        #[arg(long)]
55        cmd: String,
56        /// Répertoire de travail (défaut : répertoire courant).
57        #[arg(long)]
58        cwd: Option<String>,
59        /// Code de sortie de la commande.
60        #[arg(long = "exit-code", default_value_t = 0)]
61        exit_code: i64,
62    },
63
64    /// Ouvre l'interface TUI interactive de recherche.
65    Search {
66        /// Requête initiale (positionnelle, optionnelle).
67        query: Option<String>,
68        /// Requête explicite (équivalent à l'argument positionnel).
69        #[arg(long = "query", value_name = "TEXTE", conflicts_with = "query")]
70        query_opt: Option<String>,
71        /// Mode non interactif : imprime les résultats sur stdout sans TUI.
72        #[arg(long)]
73        print: bool,
74        /// Nombre maximal de résultats affichés en mode --print.
75        #[arg(long, default_value_t = 20)]
76        limit: usize,
77        /// Filtre sur un projet Git (nom du dossier racine ou chemin git_root).
78        #[arg(long, value_name = "NOM")]
79        project: Option<String>,
80        /// Filtre sur une branche Git.
81        #[arg(long, value_name = "BRANCHE")]
82        branch: Option<String>,
83        /// Filtre sur un code de sortie exact (ex : 0, 1, 127).
84        #[arg(long = "exit-code", value_name = "CODE")]
85        exit_code: Option<i64>,
86        /// N'affiche que les commandes en échec (exit_code ≠ 0).
87        #[arg(long, conflicts_with = "exit_code")]
88        failed: bool,
89        /// Limite l'âge des résultats (durée `24h`/`7d`/`2w`/`3m`/`1y` ou date `AAAA-MM-JJ`).
90        #[arg(long, value_name = "DURÉE|DATE")]
91        since: Option<String>,
92        /// N'affiche que les commandes antérieures à une date (`AAAA-MM-JJ`).
93        /// Alias : `--until`.
94        #[arg(long, visible_alias = "until", value_name = "DATE")]
95        before: Option<String>,
96        /// Filtre sur un répertoire de travail exact.
97        #[arg(long, value_name = "CHEMIN")]
98        cwd: Option<String>,
99        /// Filtre sur un shell exact (ex : bash, zsh).
100        #[arg(long, value_name = "SHELL")]
101        shell: Option<String>,
102        /// Produit une sortie JSON stable (implique le mode non interactif).
103        #[arg(long)]
104        json: bool,
105        /// N'affiche que les identifiants, un par ligne (implique le mode non
106        /// interactif). Pratique pour chaîner avec `mnemo show`/`mnemo print`.
107        #[arg(long = "id-only", conflicts_with = "json")]
108        id_only: bool,
109    },
110
111    /// Ouvre la TUI avancée (interface interactive principale).
112    Tui {
113        /// Requête initiale (positionnelle, optionnelle).
114        query: Option<String>,
115        /// Filtre initial sur un projet Git (nom du dossier racine).
116        #[arg(long, value_name = "NOM")]
117        project: Option<String>,
118        /// Filtre initial sur une branche Git.
119        #[arg(long, value_name = "BRANCHE")]
120        branch: Option<String>,
121        /// Filtre initial sur un répertoire de travail.
122        #[arg(long, value_name = "CHEMIN")]
123        cwd: Option<String>,
124        /// N'affiche que les commandes en échec (exit_code ≠ 0).
125        #[arg(long)]
126        failed: bool,
127    },
128
129    /// Affiche le snippet d'intégration Bash à ajouter dans ~/.bashrc.
130    Bashrc,
131
132    /// Gère l'intégration shell installée dans ~/.bashrc.
133    Shell {
134        #[command(subcommand)]
135        action: ShellCommand,
136    },
137
138    /// Applique les migrations de schéma SQLite en attente.
139    Migrate,
140
141    /// Affiche des statistiques d'usage (texte simple).
142    Stats {
143        /// Filtre sur un projet Git (nom du dossier racine, chemin git_root, ou `current`).
144        #[arg(long, value_name = "NOM")]
145        project: Option<String>,
146        /// Filtre sur une branche Git.
147        #[arg(long, value_name = "BRANCHE")]
148        branch: Option<String>,
149        /// Limite la fenêtre d'analyse (durée `24h`/`7d`/`2w`/`3m`/`1y` ou date `AAAA-MM-JJ`).
150        #[arg(long, value_name = "DURÉE|DATE")]
151        since: Option<String>,
152        /// Produit une sortie JSON exploitable.
153        #[arg(long)]
154        json: bool,
155    },
156
157    /// Diagnostique l'installation locale de mnemo.
158    Doctor {
159        /// Répare les éléments manquants (config, base, bloc .bashrc).
160        #[arg(long)]
161        fix: bool,
162        /// Produit une sortie JSON exploitable.
163        #[arg(long)]
164        json: bool,
165    },
166
167    /// Gère la configuration locale de mnemo.
168    Config {
169        #[command(subcommand)]
170        action: ConfigCommand,
171    },
172
173    /// Crée une sauvegarde locale complète (archive .tar.gz).
174    Backup {
175        /// Dossier de destination (défaut : ~/.local/share/mnemo/backups/).
176        #[arg(long, value_name = "DOSSIER")]
177        output: Option<PathBuf>,
178        /// Produit une sortie JSON exploitable.
179        #[arg(long)]
180        json: bool,
181    },
182
183    /// Restaure une sauvegarde (.tar.gz) après vérification.
184    Restore {
185        /// Chemin de l'archive de sauvegarde.
186        archive: PathBuf,
187        /// Montre ce qui serait fait sans rien modifier.
188        #[arg(long = "dry-run")]
189        dry_run: bool,
190        /// Confirme la restauration sans question interactive.
191        #[arg(long)]
192        yes: bool,
193    },
194
195    /// Exporte les commandes en JSON ou CSV.
196    Export {
197        /// Format de sortie.
198        #[arg(long, value_enum)]
199        format: ExportFormat,
200        /// Filtre sur un projet Git (nom du dossier racine ou chemin git_root).
201        #[arg(long, value_name = "NOM")]
202        project: Option<String>,
203        /// Filtre sur une branche Git.
204        #[arg(long, value_name = "BRANCHE")]
205        branch: Option<String>,
206        /// Fichier de sortie (défaut : stdout).
207        #[arg(long, value_name = "FICHIER")]
208        output: Option<PathBuf>,
209        /// Compresse la sortie en gzip (`.json.gz` / `.csv.gz`).
210        #[arg(long)]
211        gzip: bool,
212    },
213
214    /// Affiche les dernières commandes avec leurs IDs.
215    List {
216        /// Nombre de commandes affichées (défaut : 20).
217        #[arg(long)]
218        limit: Option<usize>,
219        /// Filtre sur un projet Git (nom du dossier racine ou chemin git_root).
220        #[arg(long, value_name = "NOM")]
221        project: Option<String>,
222        /// Filtre sur une branche Git.
223        #[arg(long, value_name = "BRANCHE")]
224        branch: Option<String>,
225        /// Produit une sortie JSON exploitable.
226        #[arg(long)]
227        json: bool,
228    },
229
230    /// Affiche le détail complet d'une commande par son ID, sans l'exécuter.
231    ///
232    /// mnemo n'exécute jamais une commande de l'historique : `show` se contente
233    /// de lire la base. Si la commande a déjà été redactée, sa forme redactée
234    /// stockée est affichée telle quelle.
235    Show {
236        /// Identifiant de la commande (voir `mnemo list` ou `mnemo search`).
237        id: i64,
238    },
239
240    /// Imprime uniquement la commande brute sur stdout, sans décor ni exécution.
241    ///
242    /// Aucun label, aucune couleur : la sortie peut être copiée ou redirigée.
243    /// mnemo n'exécute jamais la commande ; l'utilisateur reste responsable de
244    /// ce qu'il fait de la sortie.
245    Print {
246        /// Identifiant de la commande (voir `mnemo list` ou `mnemo search`).
247        id: i64,
248    },
249
250    /// Supprime une commande par son ID (après confirmation).
251    Delete {
252        /// Identifiant de la commande à supprimer.
253        id: i64,
254        /// Montre la commande ciblée sans la supprimer.
255        #[arg(long = "dry-run")]
256        dry_run: bool,
257        /// Confirme la suppression sans question interactive.
258        #[arg(long)]
259        yes: bool,
260    },
261
262    /// Nettoie les commandes plus anciennes qu'une durée donnée.
263    Prune {
264        /// Durée d'ancienneté (ex : 30d, 12w, 6m, 1y).
265        #[arg(long = "older-than", value_name = "DURÉE")]
266        older_than: String,
267        /// Filtre sur un projet Git (nom du dossier racine ou chemin git_root).
268        #[arg(long, value_name = "NOM")]
269        project: Option<String>,
270        /// Filtre sur une branche Git.
271        #[arg(long, value_name = "BRANCHE")]
272        branch: Option<String>,
273        /// Montre ce qui serait supprimé sans rien modifier.
274        #[arg(long = "dry-run")]
275        dry_run: bool,
276        /// Confirme le nettoyage sans question interactive.
277        #[arg(long)]
278        yes: bool,
279    },
280
281    /// Affiche des informations détaillées de version et de build.
282    Version,
283
284    /// Vérifie si une nouvelle version est disponible (sans rien installer).
285    ///
286    /// En terminal interactif, si une mise à jour existe, propose de lancer
287    /// `mnemo upgrade` immédiatement (réponse par défaut : non). En mode non
288    /// interactif (CI, script, cron, pipe), reste une simple vérification.
289    /// `--upgrade` enchaîne directement l'installation quand une mise à jour est
290    /// disponible ; combiné à `--yes`, il permet un upgrade automatisé.
291    /// `--require-signature` rend la vérification Sigstore (cosign) obligatoire
292    /// lors de l'upgrade enchaîné.
293    Update {
294        /// Sortie au format JSON (vérification seule, sans proposition).
295        #[arg(long)]
296        json: bool,
297        /// Si une mise à jour est disponible, lance directement `mnemo upgrade`.
298        #[arg(long)]
299        upgrade: bool,
300        /// Avec `--upgrade`, installe sans confirmation interactive.
301        #[arg(long)]
302        yes: bool,
303        /// Avec `--upgrade`, exige une signature Sigstore valide (cosign).
304        #[arg(long = "require-signature")]
305        require_signature: bool,
306    },
307
308    /// Télécharge et installe la dernière version stable (remplace le binaire).
309    Upgrade {
310        /// Montre ce qui serait fait sans rien télécharger ni remplacer.
311        #[arg(long = "dry-run")]
312        dry_run: bool,
313        /// Confirme l'installation sans question interactive.
314        #[arg(long)]
315        yes: bool,
316        /// Force une version précise (ex : v0.5.0) au lieu de la dernière.
317        #[arg(long, value_name = "VERSION")]
318        version: Option<String>,
319        /// Force un triplet cible (ex : aarch64-unknown-linux-musl).
320        #[arg(long, value_name = "CIBLE")]
321        target: Option<String>,
322        /// Exige une signature Sigstore valide (cosign requis) avant d'installer.
323        #[arg(long = "require-signature")]
324        require_signature: bool,
325    },
326
327    /// Désinstalle mnemo : binaire + intégration shell. Conserve les données.
328    Uninstall {
329        /// Montre ce qui serait supprimé sans rien modifier.
330        #[arg(long = "dry-run")]
331        dry_run: bool,
332        /// Confirme la désinstallation sans question interactive.
333        #[arg(long)]
334        yes: bool,
335        /// Supprime AUSSI la configuration, la base et les sauvegardes.
336        #[arg(long)]
337        purge: bool,
338    },
339
340    /// Inspecte le projet courant et les projets connus de l'historique.
341    Project {
342        #[command(subcommand)]
343        action: ProjectCommand,
344    },
345
346    /// Maintenance de l'historique (nettoyage automatique configurable).
347    Maintenance {
348        #[command(subcommand)]
349        action: MaintenanceCommand,
350    },
351
352    /// Navigue, consulte et exporte des sessions de travail.
353    ///
354    /// Une session regroupe les commandes partageant un même `session_id`,
355    /// capturé par l'intégration shell (`MNEMO_SESSION_ID`). Les commandes
356    /// importées ou enregistrées sans cet identifiant ne sont pas rattachées à
357    /// une session.
358    Session {
359        #[command(subcommand)]
360        action: SessionCommand,
361    },
362
363    /// Analyse et redacte les secrets présents dans l'historique déjà stocké.
364    ///
365    /// `scan` repère les commandes potentiellement sensibles et les affiche
366    /// toujours sous forme redactée. `redact` les nettoie en place (dry-run par
367    /// défaut, sauvegarde obligatoire avant toute écriture). Aucun secret n'est
368    /// jamais affiché en clair.
369    Secrets {
370        #[command(subcommand)]
371        action: SecretsCommand,
372    },
373
374    /// Génère un runbook Markdown réutilisable à partir d'une session ou d'un
375    /// projet Git.
376    ///
377    /// Chaque commande est présentée dans une section Markdown numérotée avec
378    /// son répertoire de travail, triée par ordre chronologique (le plus ancien
379    /// en premier). Les commandes vides sont exclues. Le résultat peut être
380    /// écrit sur stdout (défaut) ou dans un fichier (`--output`).
381    ///
382    /// Exactement un des drapeaux `--last`, `--session` ou `--project` est
383    /// requis. Si aucun n'est fourni, une erreur claire est affichée.
384    Runbook {
385        /// Cible la dernière session (incompatible avec `--session` et
386        /// `--project`).
387        #[arg(long, conflicts_with_all = ["session", "project"])]
388        last: bool,
389        /// Identifiant de session explicite (incompatible avec `--last` et
390        /// `--project`).
391        #[arg(long, value_name = "ID", conflicts_with_all = ["last", "project"])]
392        session: Option<String>,
393        /// Nom court ou chemin `git_root` d'un projet (incompatible avec
394        /// `--last` et `--session`).
395        #[arg(long, value_name = "NOM|CHEMIN", conflicts_with_all = ["last", "session"])]
396        project: Option<String>,
397        /// Fichier de sortie (défaut : stdout).
398        #[arg(long, value_name = "FICHIER")]
399        output: Option<PathBuf>,
400        /// Autorise l'écrasement du fichier de sortie si déjà existant.
401        #[arg(long)]
402        force: bool,
403        /// Nombre maximal de commandes incluses dans le runbook.
404        #[arg(long, value_name = "N")]
405        limit: Option<u32>,
406        /// Titre personnalisé du runbook (défaut : identifiant de session ou
407        /// nom de projet).
408        #[arg(long, value_name = "TITRE")]
409        title: Option<String>,
410        /// Format de sortie. Par défaut, rendu Markdown. `json` produit un
411        /// document JSON stable et déterministe.
412        #[arg(long, value_name = "FORMAT", default_value = "markdown")]
413        format: RunbookFormat,
414        /// Désactive la redaction des secrets (activée par défaut).
415        /// Sans ce flag, chaque commande est analysée et les valeurs sensibles
416        /// détectées sont remplacées par [REDACTED] avant inclusion dans le
417        /// runbook.
418        #[arg(long)]
419        no_redact: bool,
420        /// Mode de groupement des commandes dans le runbook.
421        /// `none` (défaut) : liste plate numérotée.
422        /// `cwd` : sections par répertoire de travail.
423        /// `project` : sections par racine Git.
424        #[arg(long, value_name = "MODE", default_value = "none")]
425        group_by: GroupBy,
426    },
427}
428
429/// Regroupe les options de `mnemo search` pour éviter une fonction à trop
430/// d'arguments (filtres combinables passés en un bloc).
431#[derive(Debug, Default)]
432pub struct SearchArgs {
433    pub query: Option<String>,
434    pub print: bool,
435    pub limit: usize,
436    pub project: Option<String>,
437    pub branch: Option<String>,
438    pub exit_code: Option<i64>,
439    pub failed: bool,
440    pub since: Option<String>,
441    pub before: Option<String>,
442    pub cwd: Option<String>,
443    pub shell: Option<String>,
444    pub json: bool,
445    pub id_only: bool,
446}
447
448/// Shells supportés par `mnemo completions`. Limité volontairement à bash, zsh
449/// et fish (un shell inconnu produit une erreur claire de clap). L'enregistrement
450/// automatique du hook reste, lui, Bash-first.
451#[derive(Clone, Copy, Debug, ValueEnum)]
452pub enum CompletionShell {
453    Bash,
454    Zsh,
455    Fish,
456}
457
458impl CompletionShell {
459    /// Convertit vers le générateur `clap_complete` correspondant.
460    pub fn generator(self) -> Shell {
461        match self {
462            CompletionShell::Bash => Shell::Bash,
463            CompletionShell::Zsh => Shell::Zsh,
464            CompletionShell::Fish => Shell::Fish,
465        }
466    }
467}
468
469#[derive(Subcommand, Debug)]
470pub enum ConfigCommand {
471    /// Affiche la configuration effective (valeurs par défaut incluses).
472    Show,
473    /// Affiche le chemin du fichier de configuration.
474    Path,
475    /// Ouvre la configuration dans l'éditeur ($EDITOR, sinon nano/vi).
476    Edit,
477    /// Vérifie la validité du fichier de configuration.
478    Validate,
479    /// Gère la liste des commandes ignorées dans `mnemo stats`.
480    StatsIgnore {
481        #[command(subcommand)]
482        action: StatsIgnoreCommand,
483    },
484}
485
486#[derive(Subcommand, Debug)]
487pub enum ProjectCommand {
488    /// Affiche le projet détecté pour le répertoire courant.
489    Current,
490    /// Liste les projets connus de l'historique.
491    List {
492        /// Nombre maximal de projets affichés.
493        #[arg(long, value_name = "N")]
494        limit: Option<usize>,
495        /// Sortie JSON.
496        #[arg(long)]
497        json: bool,
498    },
499    /// Affiche le détail d'un projet : activité, branches, derniers échecs.
500    Show {
501        /// Racine ou nom court du projet (incompatible avec `--current`).
502        #[arg(value_name = "PROJET", conflicts_with = "current")]
503        project: Option<String>,
504        /// Cible le projet du répertoire courant.
505        #[arg(long)]
506        current: bool,
507        /// Nombre maximal de commandes récentes affichées.
508        #[arg(long, value_name = "N")]
509        limit: Option<usize>,
510        /// Sortie JSON.
511        #[arg(long)]
512        json: bool,
513    },
514    /// Génère un rapport d'activité réutilisable (Markdown par défaut, ou JSON).
515    Report {
516        /// Racine ou nom court du projet (incompatible avec `--current`).
517        #[arg(value_name = "PROJET", conflicts_with = "current")]
518        project: Option<String>,
519        /// Cible le projet du répertoire courant.
520        #[arg(long)]
521        current: bool,
522        /// Borne inférieure (`24h`, `7d`, `2w`, `3m`, `1y` ou `AAAA-MM-JJ`).
523        #[arg(long, value_name = "DURÉE|DATE")]
524        since: Option<String>,
525        /// Borne supérieure (`AAAA-MM-JJ` exclue, ou durée).
526        #[arg(long, value_name = "DURÉE|DATE")]
527        until: Option<String>,
528        /// Format de sortie (`markdown` par défaut).
529        #[arg(long, value_enum, default_value = "markdown")]
530        format: SessionFormat,
531        /// Fichier de sortie (défaut : stdout).
532        #[arg(long, value_name = "FICHIER")]
533        output: Option<PathBuf>,
534        /// Autorise l'écrasement d'un fichier de sortie existant.
535        #[arg(long)]
536        force: bool,
537        /// Nombre maximal de commandes détaillées dans le rapport.
538        #[arg(long, value_name = "N")]
539        limit: Option<usize>,
540    },
541}
542
543#[derive(Subcommand, Debug)]
544pub enum MaintenanceCommand {
545    /// Affiche l'état de la maintenance et ce qui serait nettoyé.
546    Status,
547    /// Exécute le nettoyage configuré.
548    Run {
549        /// Montre ce qui serait supprimé sans rien modifier.
550        #[arg(long = "dry-run")]
551        dry_run: bool,
552        /// Confirme le nettoyage sans question interactive.
553        #[arg(long)]
554        yes: bool,
555    },
556}
557
558#[derive(Subcommand, Debug)]
559pub enum StatsIgnoreCommand {
560    /// Ajoute une commande à la liste ignorée du Top commandes.
561    Add {
562        /// Nom de commande (ex: `create_dir`).
563        name: String,
564    },
565    /// Retire une commande de la liste ignorée.
566    Remove {
567        /// Nom de commande (ex: `create_dir`).
568        name: String,
569    },
570    /// Affiche les commandes actuellement ignorées.
571    List,
572}
573
574#[derive(Subcommand, Debug)]
575pub enum ShellCommand {
576    /// Met à niveau l'intégration Bash installée dans ~/.bashrc.
577    ///
578    /// Remplace un bloc obsolète par la version courante (capture de
579    /// `MNEMO_SESSION_ID` pour `mnemo session`), après sauvegarde et sans
580    /// toucher au reste du fichier. Sans bloc installé, propose `mnemo init`.
581    Upgrade,
582}
583
584/// Format d'export d'une session (`mnemo session export`).
585#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
586pub enum SessionFormat {
587    Markdown,
588    Json,
589}
590
591#[derive(Subcommand, Debug)]
592pub enum SessionCommand {
593    /// Liste les sessions connues, de la plus récente à la plus ancienne.
594    List {
595        /// Nombre maximal de sessions affichées.
596        #[arg(long, value_name = "N")]
597        limit: Option<usize>,
598    },
599    /// Affiche les commandes d'une session, dans l'ordre chronologique.
600    Show {
601        /// Identifiant de session (voir `mnemo session list`).
602        session_id: String,
603        /// Nombre maximal de commandes affichées.
604        #[arg(long, value_name = "N")]
605        limit: Option<usize>,
606    },
607    /// Exporte une session en Markdown (défaut) ou JSON.
608    Export {
609        /// Identifiant de session à exporter (incompatible avec `--last`).
610        #[arg(value_name = "SESSION_ID", conflicts_with = "last")]
611        session_id: Option<String>,
612        /// Cible la session la plus récente au lieu d'un identifiant explicite.
613        #[arg(long)]
614        last: bool,
615        /// Format de sortie (`markdown` par défaut).
616        #[arg(long, value_enum, default_value = "markdown")]
617        format: SessionFormat,
618        /// Fichier de sortie (défaut : stdout).
619        #[arg(long, value_name = "FICHIER")]
620        output: Option<PathBuf>,
621        /// Autorise l'écrasement d'un fichier de sortie existant.
622        #[arg(long)]
623        force: bool,
624    },
625}
626
627#[derive(Subcommand, Debug)]
628pub enum SecretsCommand {
629    /// Repère les commandes potentiellement sensibles (lecture seule).
630    ///
631    /// Les commandes sont toujours affichées sous forme redactée ; aucun secret
632    /// n'apparaît en clair. N'effectue aucune modification.
633    Scan {
634        /// Nombre maximal de résultats affichés.
635        #[arg(long, value_name = "N")]
636        limit: Option<usize>,
637        /// Sortie JSON (sans valeurs sensibles).
638        #[arg(long)]
639        json: bool,
640    },
641    /// Redacte en place les commandes sensibles déjà stockées.
642    ///
643    /// Dry-run par défaut : sans `--apply`, rien n'est modifié. Avec `--apply`,
644    /// une sauvegarde est créée avant toute écriture et seule la colonne
645    /// `command` est mise à jour.
646    Redact {
647        /// Montre ce qui serait redacté sans rien modifier (comportement par
648        /// défaut, accepté explicitement).
649        #[arg(long = "dry-run")]
650        dry_run: bool,
651        /// Applique réellement la redaction (sinon dry-run).
652        #[arg(long)]
653        apply: bool,
654        /// Confirme la redaction sans question interactive.
655        #[arg(long)]
656        yes: bool,
657        /// Force une sauvegarde avant redaction (toujours effectuée avec
658        /// `--apply`, ce drapeau le rend explicite).
659        #[arg(long)]
660        backup: bool,
661    },
662}