# Migration electron-updater + Gitea — Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Remplacer le système de mise à jour custom (download HTTP + script batch) par `electron-updater`, en gardant Gitea comme source de releases. **Architecture:** Approche hybride — appel API Gitea pour découvrir la dernière version (car Gitea n'a pas d'URL fixe "latest"), puis `electron-updater` (generic provider) pour le cycle download → vérification sha512 → installation NSIS → restart. Les IPC channels restent identiques pour que `UpdateBanner.ts` et `preload.ts` n'aient aucun changement. **Tech Stack:** electron-updater 6.x, Electron 32.x, TypeScript --- ### Task 1: Installer electron-updater et configurer package.json **Files:** - Modify: `package.json` **Step 1: Installer la dépendance** ```bash npm install electron-updater ``` > `electron-updater` doit être dans `dependencies` (pas `devDependencies`) car il tourne dans l'app packagée. **Step 2: Ajouter la config `publish` dans la section `build`** Dans `package.json`, ajouter dans `"build"` : ```json "publish": { "provider": "generic", "url": "https://gitea.mickael-pol.fr/mickael/dd-timer/releases/download/latest" } ``` > Cette URL est un placeholder — au runtime, on la remplace dynamiquement via `setFeedURL()`. > Son seul rôle est de déclencher la génération de `latest.yml` par electron-builder. **Step 3: Commit** ```bash git add package.json package-lock.json git commit -m "chore: add electron-updater dependency + publish config" ``` --- ### Task 2: Réécrire la section mise à jour dans main.ts **Files:** - Modify: `src/infrastructure/electron/main.ts:1-16` (imports) - Modify: `src/infrastructure/electron/main.ts:37-48` (UpdateInfo + state vars) - Delete: `src/infrastructure/electron/main.ts:254-450` (tout le bloc update custom) - Add: nouveau bloc update avec electron-updater (~60 lignes) **Step 1: Ajouter l'import electron-updater, supprimer les imports inutiles** Remplacer les imports en haut du fichier : ```typescript import { app, BrowserWindow, Tray, Menu, nativeImage, ipcMain, Notification, dialog, } from 'electron'; import path from 'path'; import fs from 'fs'; import { autoUpdater } from 'electron-updater'; ``` > Supprimer : `import https from 'https'`, `import http from 'http'`, `import os from 'os'`, `import { spawn } from 'child_process'` — ils ne sont plus nécessaires pour les mises à jour. > > **Attention** : `https` et `http` sont encore utilisés par le bloc ntfy (lignes 214-243). Vérifier si d'autres usages existent avant de supprimer. Si ntfy les utilise → garder `https` et `http`. Supprimer seulement `os` et `spawn` si plus aucun usage. **Step 2: Simplifier les variables d'état** Remplacer l'interface `UpdateInfo` et les variables `updateInfo` / `downloading` : ```typescript interface UpdateInfo { version: string; } let updateInfo: UpdateInfo | null = null; ``` > On supprime `downloadUrl`, `assetName`, `releaseNotes` — electron-updater gère tout ça. > On supprime `downloading` — electron-updater gère l'état. **Step 3: Supprimer tout le bloc update custom** Supprimer entièrement (lignes 254-450) : - `compareVersions()` - Interfaces `GiteaAsset`, `GiteaRelease` - `checkForUpdates()` - `startDownload()` - `sendUpdateError()` - `launchUpdater()` **Step 4: Écrire le nouveau système de mise à jour** Ajouter à la place du code supprimé : ```typescript // ─── COMPARAISON DE VERSIONS ──────────────────────────────────────────────── function compareVersions(a: string, b: string): number { const pa = a.replace(/^v/, '').split('.').map(Number); const pb = b.replace(/^v/, '').split('.').map(Number); for (let i = 0; i < 3; i++) { if ((pb[i] || 0) > (pa[i] || 0)) return 1; if ((pb[i] || 0) < (pa[i] || 0)) return -1; } return 0; } // ─── ELECTRON-UPDATER : CONFIGURATION ─────────────────────────────────────── autoUpdater.autoDownload = true; autoUpdater.autoInstallOnAppQuit = false; // ─── ELECTRON-UPDATER : EVENTS ────────────────────────────────────────────── autoUpdater.on('update-available', (info) => { updateInfo = { version: info.version }; if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send('update-available', updateInfo); } fireNotification( `Mise a jour v${info.version} disponible !`, 'Téléchargement en cours...' ); rebuildTrayMenu(); }); autoUpdater.on('download-progress', (progress) => { if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send('update-progress', { percent: Math.round(progress.percent) }); } }); autoUpdater.on('update-downloaded', () => { if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send('update-ready', updateInfo ?? {}); } }); autoUpdater.on('error', (err) => { console.error('Update error:', err.message); if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send('update-error', { message: err.message }); } }); // ─── VÉRIFICATION VIA API GITEA ───────────────────────────────────────────── function checkForUpdates(silent = false): void { const https = require('https') as typeof import('https'); const options: import('https').RequestOptions = { hostname: GITEA_HOST, port: 443, path: `/api/v1/repos/${GITEA_USER}/${GITEA_REPO}/releases?limit=1`, method: 'GET', headers: { 'User-Agent': `MinuteurDragodinde/${CURRENT_VERSION}`, 'Accept': 'application/json', }, }; const req = https.request(options, (res) => { let data = ''; res.on('data', (chunk: string) => { data += chunk; }); res.on('end', () => { try { const releases = JSON.parse(data); if (!Array.isArray(releases) || releases.length === 0) return; const release = releases[0]; const latestVersion = release.tag_name; if (!latestVersion || compareVersions(CURRENT_VERSION, latestVersion) <= 0) { if (!silent && mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send('update-not-available'); } return; } // Pointer electron-updater vers le tag de la release const tag = latestVersion.startsWith('v') ? latestVersion : `v${latestVersion}`; autoUpdater.setFeedURL({ provider: 'generic', url: `https://${GITEA_HOST}/${GITEA_USER}/${GITEA_REPO}/releases/download/${tag}`, }); // electron-updater prend le relais : lit latest.yml, télécharge, vérifie autoUpdater.checkForUpdates(); } catch (e: unknown) { console.error('Update check parse error:', (e as Error).message); } }); }); req.on('error', (e: Error) => console.error('Update check error:', e.message)); req.end(); } ``` **Step 5: Mettre à jour le handler `install-update`** Remplacer : ```typescript ipcMain.on('install-update', () => startDownload()); ``` Par : ```typescript ipcMain.on('install-update', () => { autoUpdater.quitAndInstall(true, true); }); ``` > `quitAndInstall(isSilent, isForceRunAfter)` : installe en silencieux et relance l'app. **Step 6: Mettre à jour le tray menu** Dans `rebuildTrayMenu()`, remplacer le click du menu update : ```typescript if (updateInfo) { items.push({ type: 'separator' }); items.push({ label: `⬆ Mise a jour v${updateInfo.version} disponible !`, click: () => autoUpdater.quitAndInstall(true, true), }); } ``` **Step 7: Simplifier le bloc did-finish-load** Le bloc `did-finish-load` (ligne 115-133) reste identique — il envoie déjà `update-available` si `updateInfo` existe. **Step 8: Commit** ```bash git add src/infrastructure/electron/main.ts git commit -m "feat: migrate update system to electron-updater" ``` --- ### Task 3: Vérifier que preload.ts et UpdateBanner.ts n'ont pas besoin de changement **Files:** - Read: `src/infrastructure/electron/preload.ts` - Read: `src/presentation/components/UpdateBanner.ts` **Step 1: Vérifier les IPC channels** Les channels IPC n'ont pas changé : - `update-available` → `{ version }` ✓ - `update-downloading` → **ATTENTION** : l'ancien code envoyait `update-downloading`, mais electron-updater n'a pas cet event. Il passe directement de `update-available` à `download-progress`. Le banner state `downloading` est activé par `onUpdateDownloading` OU `onUpdateProgress`. Vérifier que `UpdateBanner` gère bien la transition. **Step 2: Envoyer `update-downloading` explicitement** Dans les events autoUpdater, ajouter après `update-available` : ```typescript autoUpdater.on('update-available', (info) => { updateInfo = { version: info.version }; if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send('update-available', updateInfo); // Envoyer aussi downloading car autoDownload = true mainWindow.webContents.send('update-downloading', { version: info.version }); } fireNotification( `Mise a jour v${info.version} disponible !`, 'Téléchargement en cours...' ); rebuildTrayMenu(); }); ``` > Cela garantit que `UpdateBanner` passe bien en état `downloading` comme avant. **Step 3: Commit (si changement)** ```bash git add src/infrastructure/electron/main.ts git commit -m "fix: send update-downloading event for banner compatibility" ``` --- ### Task 4: Vérifier la génération de latest.yml **Step 1: Lancer un build** ```bash npm run build ``` **Step 2: Vérifier que `dist/latest.yml` existe** ```bash ls dist/latest.yml cat dist/latest.yml ``` Contenu attendu (exemple) : ```yaml version: 1.1.5 files: - url: Minuteur-Dragodinde-Setup-1.1.5.exe sha512: size: path: Minuteur-Dragodinde-Setup-1.1.5.exe sha512: releaseDate: '2026-04-04T...' ``` **Step 3: Vérifier que le .exe est aussi généré** ```bash ls dist/Minuteur-Dragodinde-Setup-*.exe ``` --- ### Task 5: Mettre à jour le CHANGELOG **Files:** - Modify: `CHANGELOG.md` **Step 1: Ajouter l'entrée** Ajouter sous `## v1.1.6` (ou créer `## v1.1.7` si nouvelle version) : ```markdown ### Mise à jour automatique - **Migration electron-updater** : remplacement du système custom (download HTTP + script batch) par `electron-updater` (generic provider) - Vérification sha512 automatique des mises à jour - Installation NSIS native (plus de script batch hack) - Restart automatique après installation - Code simplifié (~40 lignes vs ~200) - Compatible Gitea : découverte via API + `latest.yml` uploadé en asset de release ``` **Step 2: Commit** ```bash git add CHANGELOG.md git commit -m "docs: add electron-updater migration to CHANGELOG" ``` --- ### Task 6: Mettre à jour le workflow de release **Step 1: Documenter le nouveau process** Le nouveau workflow de release est : 1. Mettre à jour `version` dans `package.json` 2. `npm run build` 3. Deux fichiers sont générés dans `dist/` : - `Minuteur-Dragodinde-Setup-X.Y.Z.exe` - `latest.yml` 4. Commit + tag + push : ```bash git add -A && git commit -m "vX.Y.Z" git tag vX.Y.Z && git push && git push --tags ``` 5. Sur Gitea → Releases → Créer release avec le tag 6. **Uploader les 2 fichiers** : le `.exe` ET `latest.yml` > **IMPORTANT** : `latest.yml` DOIT être uploadé à chaque release, sinon electron-updater ne pourra pas vérifier l'intégrité du fichier. --- ## Récapitulatif des fichiers modifiés | Fichier | Action | |---------|--------| | `package.json` | Ajout dep `electron-updater` + config `publish` | | `package-lock.json` | Mis à jour automatiquement | | `src/infrastructure/electron/main.ts` | Réécriture section update (~200 lignes → ~60 lignes) | | `src/infrastructure/electron/preload.ts` | **Aucun changement** | | `src/presentation/components/UpdateBanner.ts` | **Aucun changement** | | `CHANGELOG.md` | Entrée migration |