creation du CLI update project
EBO <eric.bouhana@softalys.com> committé le 2026-03-20 16:06
6b44246c5f163691924bd7908d468dfc73125f52
1 parent(s)
9 fichiers modifiés
+923
-57
M
Cargo.lock
+1
-0
@@ -3559,6 +3559,7 @@ dependencies = [
| 3559 | 3559 | name = "rustwarden-cli" |
| 3560 | 3560 | version = "0.1.0" |
| 3561 | 3561 | dependencies = [ |
| 3562 | + "chrono", | |
| 3562 | 3563 | "clap", |
| 3563 | 3564 | "console", |
| 3564 | 3565 | "dialoguer", |
M
README.md
+20
-1
@@ -158,7 +158,26 @@ RustwardenApp::builder()
| 158 | 158 | .build().await? |
| 159 | 159 | .run().await |
| 160 | 160 | ``` |
| 161 | - | |
| 161 | +### Procédure manuelle actuelle | |
| 162 | + | |
| 163 | + 1. Remplacer les crates du framework dans le projet consommateur : | |
| 164 | + # Depuis le dossier du projet consommateur | |
| 165 | + rm -rf crates/rustwarden-core crates/rustwarden-ui | |
| 166 | + cp -r /chemin/vers/Rustwarden/crates/rustwarden-core crates/ | |
| 167 | + cp -r /chemin/vers/Rustwarden/crates/rustwarden-ui crates/ | |
| 168 | + 2. Copier les fichiers partagés (si modifiés) : | |
| 169 | + cp -r /chemin/vers/Rustwarden/locales ./ | |
| 170 | + cp -r /chemin/vers/Rustwarden/static ./ | |
| 171 | + 3. Vérifier les breaking changes : | |
| 172 | + - Les exports du prelude (signatures de RustwardenApp, AuthUser, Claims, etc.) | |
| 173 | + - Les migrations : s'assurer que core_migrations() dans src/migrations.rs est à jour | |
| 174 | + - Le contrat des templates (LayoutContext variables) | |
| 175 | + - Les dépendances dans Cargo.toml (les versions récentes ont changé : thiserror 2.0, askama 0.15, jsonwebtoken 10, rand 0.9, etc.) | |
| 176 | + 4. Compiler et corriger : | |
| 177 | + cargo check | |
| 178 | + cargo clippy -- -D warnings | |
| 179 | + | |
| 180 | + | |
| 162 | 181 | ### Sequencement interne du builder |
| 163 | 182 | |
| 164 | 183 | 1. Connexion a la base PostgreSQL |
M
crates/rustwarden-ui/src/handlers/login_handler.rs
+2
-0
@@ -68,6 +68,7 @@ struct ResendVerificationTemplate {
| 68 | 68 | app_domain: String, |
| 69 | 69 | app_description: String, |
| 70 | 70 | page_title: String, |
| 71 | + #[allow(dead_code)] // utilise par base.html (template parent) | |
| 71 | 72 | csrf_token: String, |
| 72 | 73 | title: String, |
| 73 | 74 | subtitle: String, |
@@ -90,6 +91,7 @@ struct ResetPasswordTemplate {
| 90 | 91 | app_domain: String, |
| 91 | 92 | app_description: String, |
| 92 | 93 | page_title: String, |
| 94 | + #[allow(dead_code)] // utilise par base.html (template parent) | |
| 93 | 95 | csrf_token: String, |
| 94 | 96 | reset_password_title: String, |
| 95 | 97 | subtitle: String, |
M
tools/rustwarden-cli/Cargo.toml
+1
-0
@@ -13,6 +13,7 @@ clap = { version = "4", features = ["derive"] }
| 13 | 13 | dialoguer = { version = "0.11", features = ["fuzzy-select"] } |
| 14 | 14 | console = "0.15" |
| 15 | 15 | rand = "0.9" |
| 16 | +chrono = "0.4" | |
| 16 | 17 | |
| 17 | 18 | [lints.clippy] |
| 18 | 19 | empty_line_after_doc_comments = "allow" |
M
tools/rustwarden-cli/README.md
+94
-9
@@ -1,6 +1,6 @@
| 1 | 1 | # rustwarden-cli |
| 2 | 2 | |
| 3 | -Wizard interactif en ligne de commande pour creer de nouveaux projets applicatifs bases sur le framework Rustwarden. | |
| 3 | +Wizard interactif en ligne de commande pour creer et maintenir des projets applicatifs bases sur le framework Rustwarden. | |
| 4 | 4 | |
| 5 | 5 | ## Installation |
| 6 | 6 |
@@ -12,18 +12,30 @@ cargo build -p rustwarden-cli
| 12 | 12 | |
| 13 | 13 | Le binaire est genere dans `target/debug/rustwarden-cli`. |
| 14 | 14 | |
| 15 | -## Utilisation | |
| 15 | +Pour un binaire optimise reutilisable : | |
| 16 | 16 | |
| 17 | 17 | ```bash |
| 18 | -cargo run -p rustwarden-cli -- start-project | |
| 18 | +cargo build -p rustwarden-cli --release | |
| 19 | +# => target/release/rustwarden-cli | |
| 19 | 20 | ``` |
| 20 | 21 | |
| 21 | -Ou directement : | |
| 22 | +## Utilisation | |
| 22 | 23 | |
| 23 | 24 | ```bash |
| 24 | -./target/debug/rustwarden-cli start-project | |
| 25 | +# Lancement direct (compile + execute) | |
| 26 | +cargo run -p rustwarden-cli -- <commande> | |
| 27 | + | |
| 28 | +# Ou via le binaire | |
| 29 | +./target/release/rustwarden-cli <commande> | |
| 25 | 30 | ``` |
| 26 | 31 | |
| 32 | +Commandes disponibles : | |
| 33 | + | |
| 34 | +| Commande | Description | | |
| 35 | +|----------|-------------| | |
| 36 | +| `start-project` | Creer un nouveau projet applicatif Rustwarden | | |
| 37 | +| `update-project` | Mettre a jour les crates du framework dans un projet existant | | |
| 38 | + | |
| 27 | 39 | ## Commande `start-project` |
| 28 | 40 | |
| 29 | 41 | Le wizard pose une serie de questions pour configurer le projet, puis genere un dossier complet pret a l'emploi. |
@@ -46,12 +58,17 @@ Le wizard pose une serie de questions pour configurer le projet, puis genere un
| 46 | 58 | | 12 | Email admin | `admin@localhost` | |
| 47 | 59 | | 13 | Mot de passe admin | *(genere aleatoirement)* | |
| 48 | 60 | | 14 | Configurer SMTP ? (optionnel) | Non | |
| 61 | +| 15 | Activer OAuth / SSO ? (optionnel) | Non | | |
| 49 | 62 | |
| 50 | 63 | Si SMTP est active, le wizard demande egalement : |
| 51 | 64 | - Hote, port, identifiants SMTP |
| 52 | 65 | - Adresse et nom d'expediteur |
| 53 | 66 | - TLS / STARTTLS |
| 54 | 67 | |
| 68 | +Si OAuth est active : | |
| 69 | +- URL de base pour les callbacks | |
| 70 | +- Configuration par provider (Google, GitHub, Discord, Microsoft) | |
| 71 | + | |
| 55 | 72 | ### Structure generee |
| 56 | 73 | |
| 57 | 74 | ``` |
@@ -69,6 +86,7 @@ mon-app/
| 69 | 86 | │ └── .env # Identifiants DB pour Docker |
| 70 | 87 | ├── .env # Configuration complete de l'application |
| 71 | 88 | ├── .env.example # Reference de toutes les variables d'environnement |
| 89 | +├── .rustwarden-version # Version du framework (pour update-project) | |
| 72 | 90 | ├── README.md # Guide de demarrage rapide |
| 73 | 91 | └── DEVELOPER.md # Guide complet pour ajouter de la logique metier |
| 74 | 92 | ``` |
@@ -82,6 +100,7 @@ mon-app/
| 82 | 100 | | `.env` | Variables d'environnement remplies avec les reponses du wizard | |
| 83 | 101 | | `.env.example` | Reference avec toutes les variables et valeurs d'exemple | |
| 84 | 102 | | `database/.env` | Credentials PostgreSQL pour Docker Compose | |
| 103 | +| `.rustwarden-version` | Hash git + date du framework au moment de la generation | | |
| 85 | 104 | | `README.md` | Demarrage rapide, routes, commandes | |
| 86 | 105 | | `DEVELOPER.md` | Architecture, ajout de routes/modeles, hooks, i18n, configuration | |
| 87 | 106 |
@@ -109,15 +128,78 @@ cargo run
| 109 | 128 | |
| 110 | 129 | L'admin initial est deja configure avec les identifiants fournis pendant le wizard. |
| 111 | 130 | |
| 131 | +## Commande `update-project` | |
| 132 | + | |
| 133 | +Met a jour les crates du framework (`rustwarden-core`, `rustwarden-ui`) et les ressources partagees (`locales`, `static`, `database`) dans un projet existant, sans toucher aux fichiers specifiques du projet. | |
| 134 | + | |
| 135 | +```bash | |
| 136 | +cargo run -p rustwarden-cli -- update-project | |
| 137 | +``` | |
| 138 | + | |
| 139 | +### Fonctionnement | |
| 140 | + | |
| 141 | +1. **Detection** : demande le chemin du projet et verifie qu'il s'agit bien d'un projet Rustwarden | |
| 142 | +2. **Versions** : affiche la version actuelle (depuis `.rustwarden-version`) et la nouvelle (git hash du framework) | |
| 143 | +3. **Detection des templates personnalises** : compare les templates HTML du projet avec ceux du framework pour identifier les modifications | |
| 144 | +4. **Plan de mise a jour** : affiche ce qui sera mis a jour et ce qui sera preserve | |
| 145 | +5. **Confirmation** : demande validation avant de proceder | |
| 146 | +6. **Backup** : cree un dossier `.rustwarden-backup-YYYYMMDD_HHMMSS/` avec une copie complete des fichiers qui vont etre remplaces | |
| 147 | +7. **Mise a jour** : remplace les crates et ressources par les versions du framework source | |
| 148 | +8. **Version** : met a jour `.rustwarden-version` | |
| 149 | + | |
| 150 | +### Fichiers mis a jour | |
| 151 | + | |
| 152 | +| Element | Action | | |
| 153 | +|---------|--------| | |
| 154 | +| `crates/rustwarden-core/` | Supprime et recopie depuis le framework | | |
| 155 | +| `crates/rustwarden-ui/` | Supprime et recopie (si present dans le projet) | | |
| 156 | +| `locales/` | Remplace | | |
| 157 | +| `static/` | Remplace (si le projet a une UI) | | |
| 158 | +| `database/` | Remplace en preservant `database/.env` | | |
| 159 | + | |
| 160 | +### Fichiers jamais touches | |
| 161 | + | |
| 162 | +| Fichier | Raison | | |
| 163 | +|---------|--------| | |
| 164 | +| `Cargo.toml` | Configuration workspace du projet | | |
| 165 | +| `src/main.rs` | Logique metier du projet | | |
| 166 | +| `.env` | Credentials et configuration specifiques | | |
| 167 | +| `.env.example` | Peut avoir ete personnalise | | |
| 168 | +| `database/.env` | Credentials PostgreSQL du projet | | |
| 169 | +| `README.md` | Documentation specifique au projet | | |
| 170 | +| `DEVELOPER.md` | Guide personnalise | | |
| 171 | + | |
| 172 | +### Apres la mise a jour | |
| 173 | + | |
| 174 | +Le CLI affiche les etapes a suivre : | |
| 175 | + | |
| 176 | +```bash | |
| 177 | +# 1. Verifier la compilation | |
| 178 | +cargo check | |
| 179 | + | |
| 180 | +# 2. Corriger les eventuels breaking changes | |
| 181 | +cargo clippy -- -D warnings | |
| 182 | + | |
| 183 | +# 3. Verifier les migrations | |
| 184 | +cargo run --bin migrate | |
| 185 | + | |
| 186 | +# 4. Lancer les tests | |
| 187 | +cargo test | |
| 188 | +``` | |
| 189 | + | |
| 190 | +Si des templates personnalises ont ete ecrases, ils sont recuperables dans le dossier de backup. | |
| 191 | + | |
| 112 | 192 | ## Architecture du CLI |
| 113 | 193 | |
| 114 | 194 | ``` |
| 115 | 195 | tools/rustwarden-cli/ |
| 116 | -├── Cargo.toml # Dependencies : clap, dialoguer, console, rand | |
| 196 | +├── Cargo.toml # Dependencies : clap, dialoguer, console, rand, chrono | |
| 117 | 197 | └── src/ |
| 118 | - ├── main.rs # Point d'entree, parsing des sous-commandes | |
| 119 | - ├── start_project.rs # Logique du wizard (questions, copie, generation) | |
| 120 | - └── templates.rs # Templates des fichiers generes (Cargo.toml, main.rs, .env, etc.) | |
| 198 | + ├── main.rs # Point d'entree, parsing des sous-commandes | |
| 199 | + ├── start_project.rs # Wizard de creation (questions, copie, generation) | |
| 200 | + ├── update_project.rs # Logique de mise a jour (backup, remplacement, detection) | |
| 201 | + ├── templates.rs # Templates des fichiers generes (Cargo.toml, main.rs, .env, etc.) | |
| 202 | + └── version.rs # Gestion du fichier .rustwarden-version (lecture, ecriture, calcul) | |
| 121 | 203 | ``` |
| 122 | 204 | |
| 123 | 205 | ### Dependencies |
@@ -128,6 +210,7 @@ tools/rustwarden-cli/
| 128 | 210 | | `dialoguer` | Prompts interactifs (Input, Select, Confirm) | |
| 129 | 211 | | `console` | Styles et couleurs dans le terminal | |
| 130 | 212 | | `rand` | Generation de mots de passe et secrets JWT aleatoires | |
| 213 | +| `chrono` | Timestamp pour les noms de dossiers de backup | | |
| 131 | 214 | |
| 132 | 215 | ## Etendre le CLI |
| 133 | 216 |
@@ -138,6 +221,8 @@ Pour ajouter une nouvelle sous-commande, ajoutez une variante dans l'enum `Comma
| 138 | 221 | enum Commands { |
| 139 | 222 | /// Creer un nouveau projet applicatif Rustwarden |
| 140 | 223 | StartProject, |
| 224 | + /// Mettre a jour les crates du framework dans un projet existant | |
| 225 | + UpdateProject, | |
| 141 | 226 | /// Votre nouvelle commande |
| 142 | 227 | MaCommande, |
| 143 | 228 | } |
M
tools/rustwarden-cli/src/main.rs
+15
-1
@@ -1,10 +1,16 @@
| 1 | 1 | mod start_project; |
| 2 | 2 | mod templates; |
| 3 | +mod update_project; | |
| 4 | +mod version; | |
| 3 | 5 | |
| 4 | 6 | use clap::{Parser, Subcommand}; |
| 5 | 7 | |
| 6 | 8 | #[derive(Parser)] |
| 7 | -#[command(name = "rustwarden-cli", version, about = "CLI wizard pour scaffolder des applications Rustwarden")] | |
| 9 | +#[command( | |
| 10 | + name = "rustwarden-cli", | |
| 11 | + version, | |
| 12 | + about = "CLI wizard pour scaffolder des applications Rustwarden" | |
| 13 | +)] | |
| 8 | 14 | struct Cli { |
| 9 | 15 | #[command(subcommand)] |
| 10 | 16 | command: Commands, |
@@ -14,6 +20,8 @@ struct Cli {
| 14 | 20 | enum Commands { |
| 15 | 21 | /// Creer un nouveau projet applicatif Rustwarden |
| 16 | 22 | StartProject, |
| 23 | + /// Mettre a jour les crates du framework dans un projet existant | |
| 24 | + UpdateProject, | |
| 17 | 25 | } |
| 18 | 26 | |
| 19 | 27 | fn main() { |
@@ -26,5 +34,11 @@ fn main() {
| 26 | 34 | std::process::exit(1); |
| 27 | 35 | } |
| 28 | 36 | } |
| 37 | + Commands::UpdateProject => { | |
| 38 | + if let Err(e) = update_project::run() { | |
| 39 | + eprintln!("{}", console::style(format!("Erreur: {e}")).red().bold()); | |
| 40 | + std::process::exit(1); | |
| 41 | + } | |
| 42 | + } | |
| 29 | 43 | } |
| 30 | 44 | } |
M
tools/rustwarden-cli/src/start_project.rs
+24
-46
@@ -6,6 +6,7 @@ use dialoguer::{Confirm, Input, Select};
| 6 | 6 | use rand::Rng; |
| 7 | 7 | |
| 8 | 8 | use crate::templates::{self, OAuthProviderConfig, OAuthWizardConfig, ProjectConfig, SmtpConfig}; |
| 9 | +use crate::version; | |
| 9 | 10 | |
| 10 | 11 | pub fn run() -> Result<(), Box<dyn std::error::Error>> { |
| 11 | 12 | println!( |
@@ -31,28 +32,16 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
| 31 | 32 | let auto_migrate = ask_auto_migrate()?; |
| 32 | 33 | let port = ask_port()?; |
| 33 | 34 | |
| 34 | - println!( | |
| 35 | - "\n{}", | |
| 36 | - style("── Base de donnees ──").cyan().bold() | |
| 37 | - ); | |
| 35 | + println!("\n{}", style("── Base de donnees ──").cyan().bold()); | |
| 38 | 36 | let (db_name, db_user, db_password) = ask_database_config(&name)?; |
| 39 | 37 | |
| 40 | - println!( | |
| 41 | - "\n{}", | |
| 42 | - style("── Administrateur ──").cyan().bold() | |
| 43 | - ); | |
| 38 | + println!("\n{}", style("── Administrateur ──").cyan().bold()); | |
| 44 | 39 | let (admin_username, admin_email, admin_password) = ask_admin_config()?; |
| 45 | 40 | |
| 46 | - println!( | |
| 47 | - "\n{}", | |
| 48 | - style("── Email SMTP ──").cyan().bold() | |
| 49 | - ); | |
| 41 | + println!("\n{}", style("── Email SMTP ──").cyan().bold()); | |
| 50 | 42 | let smtp = ask_smtp_config(&app_display_name)?; |
| 51 | 43 | |
| 52 | - println!( | |
| 53 | - "\n{}", | |
| 54 | - style("── OAuth / SSO ──").cyan().bold() | |
| 55 | - ); | |
| 44 | + println!("\n{}", style("── OAuth / SSO ──").cyan().bold()); | |
| 56 | 45 | let oauth = ask_oauth_config(port)?; |
| 57 | 46 | |
| 58 | 47 | let jwt_secret = generate_jwt_secret(); |
@@ -75,10 +64,7 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
| 75 | 64 | oauth, |
| 76 | 65 | }; |
| 77 | 66 | |
| 78 | - println!( | |
| 79 | - "\n{}", | |
| 80 | - style("Generation du projet...").cyan().bold() | |
| 81 | - ); | |
| 67 | + println!("\n{}", style("Generation du projet...").cyan().bold()); | |
| 82 | 68 | |
| 83 | 69 | fs::create_dir_all(project_dir.join("src"))?; |
| 84 | 70 | fs::create_dir_all(project_dir.join("crates"))?; |
@@ -92,6 +78,11 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
| 92 | 78 | generate_files(&project_dir, &cfg)?; |
| 93 | 79 | rebrand_templates(&project_dir, &app_display_name)?; |
| 94 | 80 | |
| 81 | + // Ecrire le fichier de version du framework | |
| 82 | + if let Ok(v) = version::compute_version(&rustwarden_root) { | |
| 83 | + let _ = version::write_version(&project_dir, &v); | |
| 84 | + } | |
| 85 | + | |
| 95 | 86 | print_summary(&cfg, &project_dir); |
| 96 | 87 | |
| 97 | 88 | Ok(()) |
@@ -210,10 +201,7 @@ fn handle_existing_dir(project_dir: &Path) -> Result<(), Box<dyn std::error::Err
| 210 | 201 | } |
| 211 | 202 | |
| 212 | 203 | fn ask_ui_mode() -> Result<bool, Box<dyn std::error::Error>> { |
| 213 | - let options = vec![ | |
| 214 | - "Avec interface web (rustwarden-ui)", | |
| 215 | - "API seule (headless)", | |
| 216 | - ]; | |
| 204 | + let options = vec!["Avec interface web (rustwarden-ui)", "API seule (headless)"]; | |
| 217 | 205 | let selection = Select::new() |
| 218 | 206 | .with_prompt(format!("{}", style("Mode").cyan())) |
| 219 | 207 | .items(&options) |
@@ -262,10 +250,7 @@ fn ask_database_config(
| 262 | 250 | |
| 263 | 251 | let default_pw = generate_random_password(16); |
| 264 | 252 | let db_password: String = Input::new() |
| 265 | - .with_prompt(format!( | |
| 266 | - "{}", | |
| 267 | - style("Mot de passe PostgreSQL").cyan() | |
| 268 | - )) | |
| 253 | + .with_prompt(format!("{}", style("Mot de passe PostgreSQL").cyan())) | |
| 269 | 254 | .default(default_pw) |
| 270 | 255 | .interact_text()?; |
| 271 | 256 |
@@ -292,7 +277,9 @@ fn ask_admin_config() -> Result<(String, String, String), Box<dyn std::error::Er
| 292 | 277 | Ok((username, email, password)) |
| 293 | 278 | } |
| 294 | 279 | |
| 295 | -fn ask_smtp_config(app_display_name: &str) -> Result<Option<SmtpConfig>, Box<dyn std::error::Error>> { | |
| 280 | +fn ask_smtp_config( | |
| 281 | + app_display_name: &str, | |
| 282 | +) -> Result<Option<SmtpConfig>, Box<dyn std::error::Error>> { | |
| 296 | 283 | let configure = Confirm::new() |
| 297 | 284 | .with_prompt(format!( |
| 298 | 285 | "{}", |
@@ -424,7 +411,10 @@ fn ask_oauth_config(port: u16) -> Result<Option<OAuthWizardConfig>, Box<dyn std:
| 424 | 411 | .interact_text()?; |
| 425 | 412 | |
| 426 | 413 | let client_secret: String = Input::new() |
| 427 | - .with_prompt(format!(" {} Client Secret", style(display_names[i]).cyan())) | |
| 414 | + .with_prompt(format!( | |
| 415 | + " {} Client Secret", | |
| 416 | + style(display_names[i]).cyan() | |
| 417 | + )) | |
| 428 | 418 | .interact_text()?; |
| 429 | 419 | |
| 430 | 420 | providers.push(OAuthProviderConfig { |
@@ -473,9 +463,7 @@ fn find_rustwarden_root() -> Result<PathBuf, Box<dyn std::error::Error>> {
| 473 | 463 | let cargo_toml = dir.join("Cargo.toml"); |
| 474 | 464 | if cargo_toml.exists() { |
| 475 | 465 | let content = fs::read_to_string(&cargo_toml)?; |
| 476 | - if content.contains("[workspace]") | |
| 477 | - && dir.join("crates/rustwarden-core").exists() | |
| 478 | - { | |
| 466 | + if content.contains("[workspace]") && dir.join("crates/rustwarden-core").exists() { | |
| 479 | 467 | return Ok(dir); |
| 480 | 468 | } |
| 481 | 469 | } |
@@ -647,11 +635,7 @@ fn generate_files(
| 647 | 635 | if let Some(parent) = full_path.parent() { |
| 648 | 636 | fs::create_dir_all(parent)?; |
| 649 | 637 | } |
| 650 | - println!( | |
| 651 | - " {} {}", | |
| 652 | - style("Genere").dim(), | |
| 653 | - style(path).white() | |
| 654 | - ); | |
| 638 | + println!(" {} {}", style("Genere").dim(), style(path).white()); | |
| 655 | 639 | fs::write(full_path, content)?; |
| 656 | 640 | } |
| 657 | 641 |
@@ -665,10 +649,7 @@ fn generate_files(
| 665 | 649 | fn print_summary(cfg: &ProjectConfig, project_dir: &Path) { |
| 666 | 650 | let divider = style("─".repeat(55)).dim(); |
| 667 | 651 | println!("\n{divider}"); |
| 668 | - println!( | |
| 669 | - " {}", | |
| 670 | - style("Projet cree avec succes !").green().bold() | |
| 671 | - ); | |
| 652 | + println!(" {}", style("Projet cree avec succes !").green().bold()); | |
| 672 | 653 | println!("{divider}\n"); |
| 673 | 654 | |
| 674 | 655 | println!(" {} {}", style("Nom").bold(), cfg.name); |
@@ -730,10 +711,7 @@ fn print_summary(cfg: &ProjectConfig, project_dir: &Path) {
| 730 | 711 | " {}", |
| 731 | 712 | style("cd database && docker compose up -d && cd ..").green() |
| 732 | 713 | ); |
| 733 | - println!( | |
| 734 | - " {} Lancer l'application :", | |
| 735 | - style("3.").cyan().bold() | |
| 736 | - ); | |
| 714 | + println!(" {} Lancer l'application :", style("3.").cyan().bold()); | |
| 737 | 715 | println!(" {}", style("cargo run").green()); |
| 738 | 716 | println!( |
| 739 | 717 | " {} Ouvrir dans le navigateur :", |
A
tools/rustwarden-cli/src/update_project.rs
+711
-0
@@ -0,0 +1,711 @@
| 1 | +use std::fs; | |
| 2 | +use std::path::{Path, PathBuf}; | |
| 3 | + | |
| 4 | +use console::style; | |
| 5 | +use dialoguer::{Confirm, Input}; | |
| 6 | + | |
| 7 | +use crate::version; | |
| 8 | + | |
| 9 | +/// Fichiers et dossiers du framework qui sont remplaces lors de la mise a jour. | |
| 10 | +const FRAMEWORK_CRATES: &[&str] = &["rustwarden-core", "rustwarden-ui"]; | |
| 11 | +/// Ressources partagees mises a jour. Le dossier database/ est exclu car il | |
| 12 | +/// contient les volumes Docker et le .env avec les credentials du projet. | |
| 13 | +const FRAMEWORK_RESOURCES: &[&str] = &["locales", "static"]; | |
| 14 | + | |
| 15 | +/// Fichiers du projet consommateur qu'on ne touche JAMAIS. | |
| 16 | +const PROTECTED_FILES: &[&str] = &[ | |
| 17 | + "Cargo.toml", | |
| 18 | + "src/main.rs", | |
| 19 | + ".env", | |
| 20 | + ".env.example", | |
| 21 | + "README.md", | |
| 22 | + "DEVELOPER.md", | |
| 23 | +]; | |
| 24 | + | |
| 25 | +/// Lance la commande update-project. | |
| 26 | +pub fn run() -> Result<(), Box<dyn std::error::Error>> { | |
| 27 | + println!( | |
| 28 | + "\n{}", | |
| 29 | + style(" Rustwarden - Mise a jour du framework ") | |
| 30 | + .bold() | |
| 31 | + .on_cyan() | |
| 32 | + .black(), | |
| 33 | + ); | |
| 34 | + println!(); | |
| 35 | + | |
| 36 | + let rustwarden_root = find_rustwarden_root()?; | |
| 37 | + let project_dir = ask_project_dir()?; | |
| 38 | + | |
| 39 | + validate_consumer_project(&project_dir)?; | |
| 40 | + | |
| 41 | + let current_version = version::read_version(&project_dir); | |
| 42 | + let new_version = version::compute_version(&rustwarden_root)?; | |
| 43 | + | |
| 44 | + print_version_info(¤t_version, &new_version); | |
| 45 | + | |
| 46 | + let has_ui = project_dir.join("crates/rustwarden-ui").exists(); | |
| 47 | + let custom_templates = detect_custom_templates(&project_dir, &rustwarden_root, has_ui); | |
| 48 | + | |
| 49 | + print_update_plan(&project_dir, has_ui, &custom_templates); | |
| 50 | + | |
| 51 | + if !Confirm::new() | |
| 52 | + .with_prompt(format!("{}", style("Proceder a la mise a jour ?").cyan())) | |
| 53 | + .default(false) | |
| 54 | + .interact()? | |
| 55 | + { | |
| 56 | + println!(" {} Operation annulee.", style("✗").red()); | |
| 57 | + return Ok(()); | |
| 58 | + } | |
| 59 | + | |
| 60 | + // --- Backup --- | |
| 61 | + let backup_dir = create_backup(&project_dir, has_ui)?; | |
| 62 | + | |
| 63 | + // --- Mise a jour des crates --- | |
| 64 | + let stats = update_crates(&rustwarden_root, &project_dir, has_ui, &custom_templates)?; | |
| 65 | + | |
| 66 | + // --- Mise a jour des ressources --- | |
| 67 | + update_resources(&rustwarden_root, &project_dir, has_ui)?; | |
| 68 | + | |
| 69 | + // --- Reecrire le fichier de version --- | |
| 70 | + version::write_version(&project_dir, &new_version)?; | |
| 71 | + | |
| 72 | + // --- Resume --- | |
| 73 | + print_summary(&project_dir, &backup_dir, &custom_templates, &stats); | |
| 74 | + | |
| 75 | + Ok(()) | |
| 76 | +} | |
| 77 | + | |
| 78 | +// --------------------------------------------------------------------------- | |
| 79 | +// Detection du projet consommateur | |
| 80 | +// --------------------------------------------------------------------------- | |
| 81 | + | |
| 82 | +/// Demande le chemin du projet consommateur. | |
| 83 | +fn ask_project_dir() -> Result<PathBuf, Box<dyn std::error::Error>> { | |
| 84 | + let dir: String = Input::new() | |
| 85 | + .with_prompt(format!( | |
| 86 | + "{}", | |
| 87 | + style("Chemin du projet consommateur a mettre a jour").cyan() | |
| 88 | + )) | |
| 89 | + .default(".".into()) | |
| 90 | + .interact_text()?; | |
| 91 | + | |
| 92 | + let path = PathBuf::from(&dir); | |
| 93 | + let path = if path.is_absolute() { | |
| 94 | + path | |
| 95 | + } else { | |
| 96 | + std::env::current_dir()?.join(path) | |
| 97 | + }; | |
| 98 | + | |
| 99 | + Ok(path.canonicalize().unwrap_or(path)) | |
| 100 | +} | |
| 101 | + | |
| 102 | +/// Verifie que le dossier est bien un projet consommateur Rustwarden. | |
| 103 | +fn validate_consumer_project(project_dir: &Path) -> Result<(), Box<dyn std::error::Error>> { | |
| 104 | + if !project_dir.join("crates/rustwarden-core").exists() { | |
| 105 | + return Err(format!( | |
| 106 | + "Le dossier {} ne semble pas etre un projet Rustwarden.\n\ | |
| 107 | + (crates/rustwarden-core/ introuvable)", | |
| 108 | + project_dir.display() | |
| 109 | + ) | |
| 110 | + .into()); | |
| 111 | + } | |
| 112 | + | |
| 113 | + if !project_dir.join("Cargo.toml").exists() { | |
| 114 | + return Err(format!( | |
| 115 | + "Pas de Cargo.toml dans {}. Est-ce bien la racine du projet ?", | |
| 116 | + project_dir.display() | |
| 117 | + ) | |
| 118 | + .into()); | |
| 119 | + } | |
| 120 | + | |
| 121 | + Ok(()) | |
| 122 | +} | |
| 123 | + | |
| 124 | +// --------------------------------------------------------------------------- | |
| 125 | +// Detection des templates personnalises | |
| 126 | +// --------------------------------------------------------------------------- | |
| 127 | + | |
| 128 | +/// Compare les templates du projet avec ceux du framework pour detecter les | |
| 129 | +/// fichiers modifies par le consommateur. | |
| 130 | +fn detect_custom_templates( | |
| 131 | + project_dir: &Path, | |
| 132 | + rustwarden_root: &Path, | |
| 133 | + has_ui: bool, | |
| 134 | +) -> Vec<String> { | |
| 135 | + let mut custom = Vec::new(); | |
| 136 | + | |
| 137 | + if !has_ui { | |
| 138 | + return custom; | |
| 139 | + } | |
| 140 | + | |
| 141 | + let project_templates = project_dir.join("crates/rustwarden-ui/templates"); | |
| 142 | + let framework_templates = rustwarden_root.join("crates/rustwarden-ui/templates"); | |
| 143 | + | |
| 144 | + if !project_templates.exists() || !framework_templates.exists() { | |
| 145 | + return custom; | |
| 146 | + } | |
| 147 | + | |
| 148 | + compare_dirs_recursive( | |
| 149 | + &project_templates, | |
| 150 | + &framework_templates, | |
| 151 | + &project_templates, | |
| 152 | + &mut custom, | |
| 153 | + ); | |
| 154 | + custom | |
| 155 | +} | |
| 156 | + | |
| 157 | +/// Compare recursivement deux dossiers et enregistre les fichiers differents. | |
| 158 | +fn compare_dirs_recursive( | |
| 159 | + project_dir: &Path, | |
| 160 | + framework_dir: &Path, | |
| 161 | + base: &Path, | |
| 162 | + custom: &mut Vec<String>, | |
| 163 | +) { | |
| 164 | + let entries = match fs::read_dir(project_dir) { | |
| 165 | + Ok(e) => e, | |
| 166 | + Err(_) => return, | |
| 167 | + }; | |
| 168 | + | |
| 169 | + for entry in entries.flatten() { | |
| 170 | + let path = entry.path(); | |
| 171 | + let relative = path.strip_prefix(base).unwrap_or(&path); | |
| 172 | + let framework_path = framework_dir.join(relative); | |
| 173 | + | |
| 174 | + if path.is_dir() { | |
| 175 | + compare_dirs_recursive(&path, &framework_dir.join(entry.file_name()), base, custom); | |
| 176 | + } else if path.extension().and_then(|e| e.to_str()) == Some("html") { | |
| 177 | + if !framework_path.exists() { | |
| 178 | + // Fichier qui n'existe pas dans le framework = fichier custom | |
| 179 | + custom.push(relative.to_string_lossy().to_string()); | |
| 180 | + } else { | |
| 181 | + // Comparer le contenu | |
| 182 | + let Ok(project_content) = fs::read(&path) else { | |
| 183 | + continue; | |
| 184 | + }; | |
| 185 | + let Ok(framework_content) = fs::read(&framework_path) else { | |
| 186 | + continue; | |
| 187 | + }; | |
| 188 | + if project_content != framework_content { | |
| 189 | + custom.push(relative.to_string_lossy().to_string()); | |
| 190 | + } | |
| 191 | + } | |
| 192 | + } | |
| 193 | + } | |
| 194 | +} | |
| 195 | + | |
| 196 | +// --------------------------------------------------------------------------- | |
| 197 | +// Affichage | |
| 198 | +// --------------------------------------------------------------------------- | |
| 199 | + | |
| 200 | +/// Affiche les informations de version. | |
| 201 | +fn print_version_info(current: &Option<String>, new: &str) { | |
| 202 | + println!("\n{}", style("── Versions ──").cyan().bold()); | |
| 203 | + | |
| 204 | + match current { | |
| 205 | + Some(v) => println!(" {} {}", style("Actuelle :").dim(), style(v).white()), | |
| 206 | + None => println!( | |
| 207 | + " {} {}", | |
| 208 | + style("Actuelle :").dim(), | |
| 209 | + style("inconnue (pas de .rustwarden-version)").yellow() | |
| 210 | + ), | |
| 211 | + } | |
| 212 | + println!(" {} {}", style("Nouvelle :").dim(), style(new).green()); | |
| 213 | +} | |
| 214 | + | |
| 215 | +/// Affiche le plan de mise a jour. | |
| 216 | +fn print_update_plan(project_dir: &Path, has_ui: bool, custom_templates: &[String]) { | |
| 217 | + println!("\n{}", style("── Plan de mise a jour ──").cyan().bold()); | |
| 218 | + | |
| 219 | + println!(" {} {}", style("Projet :").dim(), project_dir.display()); | |
| 220 | + | |
| 221 | + println!("\n {} :", style("Sera mis a jour").green().bold()); | |
| 222 | + println!(" - crates/rustwarden-core/"); | |
| 223 | + if has_ui { | |
| 224 | + println!(" - crates/rustwarden-ui/"); | |
| 225 | + } | |
| 226 | + for res in FRAMEWORK_RESOURCES { | |
| 227 | + let p = project_dir.join(res); | |
| 228 | + if p.exists() || (has_ui || *res != "static") { | |
| 229 | + println!(" - {res}/"); | |
| 230 | + } | |
| 231 | + } | |
| 232 | + | |
| 233 | + println!( | |
| 234 | + "\n {} :", | |
| 235 | + style("Preserve (jamais touche)").yellow().bold() | |
| 236 | + ); | |
| 237 | + for f in PROTECTED_FILES { | |
| 238 | + if project_dir.join(f).exists() { | |
| 239 | + println!(" - {f}"); | |
| 240 | + } | |
| 241 | + } | |
| 242 | + | |
| 243 | + if !custom_templates.is_empty() { | |
| 244 | + println!( | |
| 245 | + "\n {} {} template(s) personnalise(s) detecte(s) :", | |
| 246 | + style("⚠").yellow().bold(), | |
| 247 | + custom_templates.len() | |
| 248 | + ); | |
| 249 | + for t in custom_templates { | |
| 250 | + println!(" - templates/{t}"); | |
| 251 | + } | |
| 252 | + println!( | |
| 253 | + " {}", | |
| 254 | + style(" Ces fichiers seront preserves (non ecrases).").green() | |
| 255 | + ); | |
| 256 | + } | |
| 257 | + | |
| 258 | + println!( | |
| 259 | + "\n {}", | |
| 260 | + style("Seuls les fichiers modifies dans le framework seront mis a jour.").dim() | |
| 261 | + ); | |
| 262 | + println!( | |
| 263 | + " {}", | |
| 264 | + style("Un backup complet des crates sera cree avant la mise a jour.").dim() | |
| 265 | + ); | |
| 266 | +} | |
| 267 | + | |
| 268 | +/// Statistiques de synchronisation. | |
| 269 | +#[derive(Default)] | |
| 270 | +struct SyncStats { | |
| 271 | + updated: Vec<String>, | |
| 272 | + added: Vec<String>, | |
| 273 | + skipped_custom: Vec<String>, | |
| 274 | + unchanged: u32, | |
| 275 | + removed: Vec<String>, | |
| 276 | +} | |
| 277 | + | |
| 278 | +/// Affiche le resume final. | |
| 279 | +fn print_summary( | |
| 280 | + project_dir: &Path, | |
| 281 | + backup_dir: &Path, | |
| 282 | + custom_templates: &[String], | |
| 283 | + stats: &SyncStats, | |
| 284 | +) { | |
| 285 | + let divider = style("─".repeat(55)).dim(); | |
| 286 | + println!("\n{divider}"); | |
| 287 | + println!(" {}", style("Mise a jour terminee !").green().bold()); | |
| 288 | + println!("{divider}\n"); | |
| 289 | + | |
| 290 | + println!(" {} {}", style("Backup :").bold(), backup_dir.display()); | |
| 291 | + | |
| 292 | + if !stats.added.is_empty() { | |
| 293 | + println!( | |
| 294 | + "\n {} {} fichier(s) ajoute(s) :", | |
| 295 | + style("+").green().bold(), | |
| 296 | + stats.added.len() | |
| 297 | + ); | |
| 298 | + for f in &stats.added { | |
| 299 | + println!(" {}", style(f).green()); | |
| 300 | + } | |
| 301 | + } | |
| 302 | + | |
| 303 | + if !stats.updated.is_empty() { | |
| 304 | + println!( | |
| 305 | + "\n {} {} fichier(s) mis a jour :", | |
| 306 | + style("~").cyan().bold(), | |
| 307 | + stats.updated.len() | |
| 308 | + ); | |
| 309 | + for f in &stats.updated { | |
| 310 | + println!(" {}", style(f).cyan()); | |
| 311 | + } | |
| 312 | + } | |
| 313 | + | |
| 314 | + if !stats.removed.is_empty() { | |
| 315 | + println!( | |
| 316 | + "\n {} {} fichier(s) supprime(s) (n'existent plus dans le framework) :", | |
| 317 | + style("-").red().bold(), | |
| 318 | + stats.removed.len() | |
| 319 | + ); | |
| 320 | + for f in &stats.removed { | |
| 321 | + println!(" {}", style(f).red()); | |
| 322 | + } | |
| 323 | + } | |
| 324 | + | |
| 325 | + if !stats.skipped_custom.is_empty() { | |
| 326 | + println!( | |
| 327 | + "\n {} {} template(s) personnalise(s) preserve(s) :", | |
| 328 | + style("⚠").yellow().bold(), | |
| 329 | + stats.skipped_custom.len() | |
| 330 | + ); | |
| 331 | + for f in &stats.skipped_custom { | |
| 332 | + println!(" {}", style(f).yellow()); | |
| 333 | + } | |
| 334 | + } | |
| 335 | + | |
| 336 | + if stats.unchanged > 0 { | |
| 337 | + println!( | |
| 338 | + "\n {} {} fichier(s) identiques (non modifies)", | |
| 339 | + style("=").dim(), | |
| 340 | + stats.unchanged | |
| 341 | + ); | |
| 342 | + } | |
| 343 | + | |
| 344 | + let _ = (project_dir, custom_templates); // utilises dans le plan | |
| 345 | + | |
| 346 | + println!("\n{divider}"); | |
| 347 | + println!(" {}", style("Prochaines etapes :").bold()); | |
| 348 | + println!("{divider}\n"); | |
| 349 | + | |
| 350 | + println!(" {} Verifier la compilation :", style("1.").cyan().bold()); | |
| 351 | + println!( | |
| 352 | + " {}", | |
| 353 | + style(format!("cd {} && cargo check", project_dir.display())).green() | |
| 354 | + ); | |
| 355 | + println!( | |
| 356 | + " {} Corriger les eventuels breaking changes :", | |
| 357 | + style("2.").cyan().bold() | |
| 358 | + ); | |
| 359 | + println!(" {}", style("cargo clippy -- -D warnings").green()); | |
| 360 | + println!(" {} Verifier les migrations :", style("3.").cyan().bold()); | |
| 361 | + println!(" {}", style("cargo run --bin migrate").green()); | |
| 362 | + println!(" {} Lancer les tests :", style("4.").cyan().bold()); | |
| 363 | + println!(" {}", style("cargo test").green()); | |
| 364 | + println!(); | |
| 365 | +} | |
| 366 | + | |
| 367 | +// --------------------------------------------------------------------------- | |
| 368 | +// Backup | |
| 369 | +// --------------------------------------------------------------------------- | |
| 370 | + | |
| 371 | +/// Cree un backup des crates et ressources du framework dans le projet. | |
| 372 | +fn create_backup(project_dir: &Path, has_ui: bool) -> Result<PathBuf, Box<dyn std::error::Error>> { | |
| 373 | + let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S"); | |
| 374 | + let backup_dir = project_dir.join(format!(".rustwarden-backup-{timestamp}")); | |
| 375 | + fs::create_dir_all(&backup_dir)?; | |
| 376 | + | |
| 377 | + println!( | |
| 378 | + "\n {} {}", | |
| 379 | + style("Backup").dim(), | |
| 380 | + style(backup_dir.display().to_string()).white() | |
| 381 | + ); | |
| 382 | + | |
| 383 | + for crate_name in FRAMEWORK_CRATES { | |
| 384 | + let src = project_dir.join("crates").join(crate_name); | |
| 385 | + if src.exists() { | |
| 386 | + let dst = backup_dir.join("crates").join(crate_name); | |
| 387 | + copy_dir_recursive(&src, &dst)?; | |
| 388 | + println!(" {} crates/{crate_name}/", style("→").dim(),); | |
| 389 | + } | |
| 390 | + } | |
| 391 | + | |
| 392 | + for res in FRAMEWORK_RESOURCES { | |
| 393 | + let src = project_dir.join(res); | |
| 394 | + if src.exists() && (has_ui || *res != "static") { | |
| 395 | + let dst = backup_dir.join(res); | |
| 396 | + copy_dir_recursive(&src, &dst)?; | |
| 397 | + println!(" {} {res}/", style("→").dim(),); | |
| 398 | + } | |
| 399 | + } | |
| 400 | + | |
| 401 | + Ok(backup_dir) | |
| 402 | +} | |
| 403 | + | |
| 404 | +// --------------------------------------------------------------------------- | |
| 405 | +// Mise a jour | |
| 406 | +// --------------------------------------------------------------------------- | |
| 407 | + | |
| 408 | +/// Met a jour les crates du framework via sync fichier par fichier. | |
| 409 | +fn update_crates( | |
| 410 | + rustwarden_root: &Path, | |
| 411 | + project_dir: &Path, | |
| 412 | + has_ui: bool, | |
| 413 | + custom_templates: &[String], | |
| 414 | +) -> Result<SyncStats, Box<dyn std::error::Error>> { | |
| 415 | + println!("\n{}", style("── Mise a jour des crates ──").cyan().bold()); | |
| 416 | + | |
| 417 | + let mut stats = SyncStats::default(); | |
| 418 | + | |
| 419 | + // rustwarden-core : toujours mis a jour | |
| 420 | + sync_crate( | |
| 421 | + rustwarden_root, | |
| 422 | + project_dir, | |
| 423 | + "rustwarden-core", | |
| 424 | + &[], | |
| 425 | + &mut stats, | |
| 426 | + )?; | |
| 427 | + | |
| 428 | + // rustwarden-ui : uniquement si le projet l'utilise | |
| 429 | + if has_ui { | |
| 430 | + sync_crate( | |
| 431 | + rustwarden_root, | |
| 432 | + project_dir, | |
| 433 | + "rustwarden-ui", | |
| 434 | + custom_templates, | |
| 435 | + &mut stats, | |
| 436 | + )?; | |
| 437 | + } | |
| 438 | + | |
| 439 | + Ok(stats) | |
| 440 | +} | |
| 441 | + | |
| 442 | +/// Synchronise un crate du framework fichier par fichier. | |
| 443 | +/// - Fichiers identiques : ignores | |
| 444 | +/// - Fichiers modifies dans le framework : mis a jour | |
| 445 | +/// - Fichiers nouveaux dans le framework : ajoutes | |
| 446 | +/// - Fichiers supprimes du framework : supprimes (sauf custom) | |
| 447 | +/// - Templates personnalises : preserves | |
| 448 | +fn sync_crate( | |
| 449 | + rustwarden_root: &Path, | |
| 450 | + project_dir: &Path, | |
| 451 | + crate_name: &str, | |
| 452 | + custom_templates: &[String], | |
| 453 | + stats: &mut SyncStats, | |
| 454 | +) -> Result<(), Box<dyn std::error::Error>> { | |
| 455 | + let src = rustwarden_root.join("crates").join(crate_name); | |
| 456 | + let dst = project_dir.join("crates").join(crate_name); | |
| 457 | + | |
| 458 | + if !src.exists() { | |
| 459 | + return Err(format!("Crate source introuvable : {}", src.display()).into()); | |
| 460 | + } | |
| 461 | + | |
| 462 | + let prefix = format!("crates/{crate_name}"); | |
| 463 | + | |
| 464 | + // 1. Copier/mettre a jour depuis la source | |
| 465 | + sync_dir_recursive(&src, &dst, &prefix, crate_name, custom_templates, stats)?; | |
| 466 | + | |
| 467 | + // 2. Supprimer les fichiers du projet qui n'existent plus dans le framework | |
| 468 | + cleanup_removed_files(&src, &dst, &prefix, crate_name, custom_templates, stats)?; | |
| 469 | + | |
| 470 | + println!(" {} crates/{crate_name}/", style("✓").green().bold()); | |
| 471 | + | |
| 472 | + Ok(()) | |
| 473 | +} | |
| 474 | + | |
| 475 | +/// Synchronise recursivement un dossier source vers un dossier destination. | |
| 476 | +fn sync_dir_recursive( | |
| 477 | + src: &Path, | |
| 478 | + dst: &Path, | |
| 479 | + prefix: &str, | |
| 480 | + crate_name: &str, | |
| 481 | + custom_templates: &[String], | |
| 482 | + stats: &mut SyncStats, | |
| 483 | +) -> Result<(), Box<dyn std::error::Error>> { | |
| 484 | + fs::create_dir_all(dst)?; | |
| 485 | + | |
| 486 | + for entry in fs::read_dir(src)? { | |
| 487 | + let entry = entry?; | |
| 488 | + let file_type = entry.file_type()?; | |
| 489 | + let src_path = entry.path(); | |
| 490 | + let dst_path = dst.join(entry.file_name()); | |
| 491 | + let name = entry.file_name().to_string_lossy().to_string(); | |
| 492 | + | |
| 493 | + // Exclure target/, .git/, data/ | |
| 494 | + if name == "target" || name == ".git" || name == "data" { | |
| 495 | + continue; | |
| 496 | + } | |
| 497 | + | |
| 498 | + let relative = format!("{prefix}/{name}"); | |
| 499 | + | |
| 500 | + if file_type.is_dir() { | |
| 501 | + sync_dir_recursive( | |
| 502 | + &src_path, | |
| 503 | + &dst_path, | |
| 504 | + &relative, | |
| 505 | + crate_name, | |
| 506 | + custom_templates, | |
| 507 | + stats, | |
| 508 | + )?; | |
| 509 | + } else { | |
| 510 | + // Verifier si c'est un template personnalise a preserver | |
| 511 | + if is_custom_template(&dst_path, crate_name, custom_templates) { | |
| 512 | + stats.skipped_custom.push(relative.clone()); | |
| 513 | + continue; | |
| 514 | + } | |
| 515 | + | |
| 516 | + if dst_path.exists() { | |
| 517 | + // Comparer le contenu | |
| 518 | + let src_content = fs::read(&src_path)?; | |
| 519 | + let dst_content = fs::read(&dst_path)?; | |
| 520 | + | |
| 521 | + if src_content == dst_content { | |
| 522 | + stats.unchanged += 1; | |
| 523 | + } else { | |
| 524 | + fs::copy(&src_path, &dst_path)?; | |
| 525 | + stats.updated.push(relative); | |
| 526 | + } | |
| 527 | + } else { | |
| 528 | + // Nouveau fichier | |
| 529 | + if let Some(parent) = dst_path.parent() { | |
| 530 | + fs::create_dir_all(parent)?; | |
| 531 | + } | |
| 532 | + fs::copy(&src_path, &dst_path)?; | |
| 533 | + stats.added.push(relative); | |
| 534 | + } | |
| 535 | + } | |
| 536 | + } | |
| 537 | + | |
| 538 | + Ok(()) | |
| 539 | +} | |
| 540 | + | |
| 541 | +/// Supprime les fichiers du projet qui n'existent plus dans le framework. | |
| 542 | +fn cleanup_removed_files( | |
| 543 | + src: &Path, | |
| 544 | + dst: &Path, | |
| 545 | + prefix: &str, | |
| 546 | + crate_name: &str, | |
| 547 | + custom_templates: &[String], | |
| 548 | + stats: &mut SyncStats, | |
| 549 | +) -> Result<(), Box<dyn std::error::Error>> { | |
| 550 | + if !dst.exists() { | |
| 551 | + return Ok(()); | |
| 552 | + } | |
| 553 | + | |
| 554 | + for entry in fs::read_dir(dst)? { | |
| 555 | + let entry = entry?; | |
| 556 | + let file_type = entry.file_type()?; | |
| 557 | + let src_path = src.join(entry.file_name()); | |
| 558 | + let dst_path = entry.path(); | |
| 559 | + let name = entry.file_name().to_string_lossy().to_string(); | |
| 560 | + | |
| 561 | + if name == "target" || name == ".git" || name == "data" { | |
| 562 | + continue; | |
| 563 | + } | |
| 564 | + | |
| 565 | + let relative = format!("{prefix}/{name}"); | |
| 566 | + | |
| 567 | + if file_type.is_dir() { | |
| 568 | + if src_path.exists() { | |
| 569 | + // Le dossier existe dans le framework, verifier son contenu | |
| 570 | + cleanup_removed_files( | |
| 571 | + &src_path, | |
| 572 | + &dst_path, | |
| 573 | + &relative, | |
| 574 | + crate_name, | |
| 575 | + custom_templates, | |
| 576 | + stats, | |
| 577 | + )?; | |
| 578 | + } | |
| 579 | + // On ne supprime pas les dossiers entiers qui n'existent pas | |
| 580 | + // dans le framework — ils peuvent contenir des fichiers custom | |
| 581 | + } else if !src_path.exists() { | |
| 582 | + // Le fichier n'existe plus dans le framework | |
| 583 | + if is_custom_template(&dst_path, crate_name, custom_templates) { | |
| 584 | + // Template custom, on le garde | |
| 585 | + continue; | |
| 586 | + } | |
| 587 | + fs::remove_file(&dst_path)?; | |
| 588 | + stats.removed.push(relative); | |
| 589 | + } | |
| 590 | + } | |
| 591 | + | |
| 592 | + Ok(()) | |
| 593 | +} | |
| 594 | + | |
| 595 | +/// Verifie si un fichier du projet est un template personnalise. | |
| 596 | +fn is_custom_template(file_path: &Path, crate_name: &str, custom_templates: &[String]) -> bool { | |
| 597 | + if crate_name != "rustwarden-ui" || custom_templates.is_empty() { | |
| 598 | + return false; | |
| 599 | + } | |
| 600 | + | |
| 601 | + // Extraire le chemin relatif depuis templates/ | |
| 602 | + let path_str = file_path.to_string_lossy(); | |
| 603 | + let Some(idx) = path_str.find("templates/") else { | |
| 604 | + return false; | |
| 605 | + }; | |
| 606 | + let relative = &path_str[idx + "templates/".len()..]; | |
| 607 | + | |
| 608 | + custom_templates.iter().any(|t| t == relative) | |
| 609 | +} | |
| 610 | + | |
| 611 | +/// Met a jour les ressources partagees (locales, static). | |
| 612 | +fn update_resources( | |
| 613 | + rustwarden_root: &Path, | |
| 614 | + project_dir: &Path, | |
| 615 | + has_ui: bool, | |
| 616 | +) -> Result<(), Box<dyn std::error::Error>> { | |
| 617 | + println!( | |
| 618 | + "\n{}", | |
| 619 | + style("── Mise a jour des ressources ──").cyan().bold() | |
| 620 | + ); | |
| 621 | + | |
| 622 | + for res in FRAMEWORK_RESOURCES { | |
| 623 | + // Ne pas copier static/ si le projet n'a pas d'UI | |
| 624 | + if *res == "static" && !has_ui { | |
| 625 | + continue; | |
| 626 | + } | |
| 627 | + | |
| 628 | + let src = rustwarden_root.join(res); | |
| 629 | + let dst = project_dir.join(res); | |
| 630 | + | |
| 631 | + if !src.exists() { | |
| 632 | + continue; | |
| 633 | + } | |
| 634 | + | |
| 635 | + if dst.exists() { | |
| 636 | + fs::remove_dir_all(&dst)?; | |
| 637 | + } | |
| 638 | + copy_dir_recursive(&src, &dst)?; | |
| 639 | + | |
| 640 | + println!(" {} {res}/", style("✓").green().bold(),); | |
| 641 | + } | |
| 642 | + | |
| 643 | + Ok(()) | |
| 644 | +} | |
| 645 | + | |
| 646 | +// --------------------------------------------------------------------------- | |
| 647 | +// Decouverte du workspace Rustwarden | |
| 648 | +// --------------------------------------------------------------------------- | |
| 649 | + | |
| 650 | +/// Cherche la racine du workspace Rustwarden (le framework source). | |
| 651 | +fn find_rustwarden_root() -> Result<PathBuf, Box<dyn std::error::Error>> { | |
| 652 | + let start_dirs = [ | |
| 653 | + std::env::current_exe() | |
| 654 | + .ok() | |
| 655 | + .and_then(|p| p.parent().map(|p| p.to_path_buf())), | |
| 656 | + std::env::current_dir().ok(), | |
| 657 | + ]; | |
| 658 | + | |
| 659 | + for start in start_dirs.into_iter().flatten() { | |
| 660 | + let mut dir = start; | |
| 661 | + loop { | |
| 662 | + let cargo_toml = dir.join("Cargo.toml"); | |
| 663 | + if cargo_toml.exists() { | |
| 664 | + let content = fs::read_to_string(&cargo_toml)?; | |
| 665 | + if content.contains("[workspace]") | |
| 666 | + && dir.join("crates/rustwarden-core").exists() | |
| 667 | + && dir.join("tools/rustwarden-cli").exists() | |
| 668 | + { | |
| 669 | + return Ok(dir); | |
| 670 | + } | |
| 671 | + } | |
| 672 | + if !dir.pop() { | |
| 673 | + break; | |
| 674 | + } | |
| 675 | + } | |
| 676 | + } | |
| 677 | + | |
| 678 | + Err("Impossible de trouver la racine du framework Rustwarden.\n\ | |
| 679 | + Lancez cette commande depuis le repertoire Rustwarden \ | |
| 680 | + ou son sous-dossier." | |
| 681 | + .into()) | |
| 682 | +} | |
| 683 | + | |
| 684 | +// --------------------------------------------------------------------------- | |
| 685 | +// Copie recursive | |
| 686 | +// --------------------------------------------------------------------------- | |
| 687 | + | |
| 688 | +/// Copie un dossier recursivement en excluant target/, .git/, data/. | |
| 689 | +fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<(), Box<dyn std::error::Error>> { | |
| 690 | + fs::create_dir_all(dst)?; | |
| 691 | + | |
| 692 | + for entry in fs::read_dir(src)? { | |
| 693 | + let entry = entry?; | |
| 694 | + let file_type = entry.file_type()?; | |
| 695 | + let src_path = entry.path(); | |
| 696 | + let dst_path = dst.join(entry.file_name()); | |
| 697 | + | |
| 698 | + let name = entry.file_name().to_string_lossy().to_string(); | |
| 699 | + if name == "target" || name == ".git" || name == "data" { | |
| 700 | + continue; | |
| 701 | + } | |
| 702 | + | |
| 703 | + if file_type.is_dir() { | |
| 704 | + copy_dir_recursive(&src_path, &dst_path)?; | |
| 705 | + } else { | |
| 706 | + fs::copy(&src_path, &dst_path)?; | |
| 707 | + } | |
| 708 | + } | |
| 709 | + | |
| 710 | + Ok(()) | |
| 711 | +} |
A
tools/rustwarden-cli/src/version.rs
+55
-0
@@ -0,0 +1,55 @@
| 1 | +use std::fs; | |
| 2 | +use std::path::Path; | |
| 3 | +use std::process::Command; | |
| 4 | + | |
| 5 | +/// Nom du fichier de version dans les projets consommateurs. | |
| 6 | +const VERSION_FILE: &str = ".rustwarden-version"; | |
| 7 | + | |
| 8 | +/// Lit la version actuelle du framework dans un projet consommateur. | |
| 9 | +pub fn read_version(project_dir: &Path) -> Option<String> { | |
| 10 | + let path = project_dir.join(VERSION_FILE); | |
| 11 | + fs::read_to_string(path).ok().map(|s| s.trim().to_string()) | |
| 12 | +} | |
| 13 | + | |
| 14 | +/// Ecrit la version du framework dans un projet consommateur. | |
| 15 | +pub fn write_version(project_dir: &Path, version: &str) -> Result<(), Box<dyn std::error::Error>> { | |
| 16 | + let path = project_dir.join(VERSION_FILE); | |
| 17 | + fs::write(path, format!("{version}\n"))?; | |
| 18 | + Ok(()) | |
| 19 | +} | |
| 20 | + | |
| 21 | +/// Calcule la version actuelle du framework a partir du git hash et de la | |
| 22 | +/// date du dernier commit, ou a defaut du Cargo.toml. | |
| 23 | +pub fn compute_version(rustwarden_root: &Path) -> Result<String, Box<dyn std::error::Error>> { | |
| 24 | + // Essayer git describe ou git log | |
| 25 | + if let Ok(output) = Command::new("git") | |
| 26 | + .args(["log", "-1", "--format=%h %ci"]) | |
| 27 | + .current_dir(rustwarden_root) | |
| 28 | + .output() | |
| 29 | + { | |
| 30 | + if output.status.success() { | |
| 31 | + let info = String::from_utf8_lossy(&output.stdout).trim().to_string(); | |
| 32 | + if !info.is_empty() { | |
| 33 | + // Format: "d5ab015 2026-03-19 14:30:00 +0100" -> "d5ab015 (2026-03-19)" | |
| 34 | + let parts: Vec<&str> = info.splitn(3, ' ').collect(); | |
| 35 | + if parts.len() >= 2 { | |
| 36 | + return Ok(format!("{} ({})", parts[0], parts[1])); | |
| 37 | + } | |
| 38 | + return Ok(info); | |
| 39 | + } | |
| 40 | + } | |
| 41 | + } | |
| 42 | + | |
| 43 | + // Fallback : version du Cargo.toml de rustwarden-core | |
| 44 | + let cargo_toml = rustwarden_root.join("crates/rustwarden-core/Cargo.toml"); | |
| 45 | + let content = fs::read_to_string(cargo_toml)?; | |
| 46 | + for line in content.lines() { | |
| 47 | + if let Some(version) = line.strip_prefix("version = \"") { | |
| 48 | + if let Some(version) = version.strip_suffix('"') { | |
| 49 | + return Ok(format!("v{version}")); | |
| 50 | + } | |
| 51 | + } | |
| 52 | + } | |
| 53 | + | |
| 54 | + Ok("inconnue".to_string()) | |
| 55 | +} |
GitRust