diff --git a/main.go b/main.go index c3228e5..ac80035 100644 --- a/main.go +++ b/main.go @@ -16,7 +16,7 @@ import ( "strings" ) -const Version = "0.3.3" +const Version = "0.3.4" func main() { // Migration one-shot des anciens skills (SKILLS//SKILL.md) vers la diff --git a/resource_windows_amd64.syso b/resource_windows_amd64.syso index d41d113..9fd69c3 100644 Binary files a/resource_windows_amd64.syso and b/resource_windows_amd64.syso differ diff --git a/resource_windows_arm64.syso b/resource_windows_arm64.syso index 8cf1576..1af7cf5 100644 Binary files a/resource_windows_arm64.syso and b/resource_windows_arm64.syso differ diff --git a/ui/index.html b/ui/index.html index ce004a3..7fc195b 100644 --- a/ui/index.html +++ b/ui/index.html @@ -395,6 +395,11 @@ button:disabled{opacity:.5;cursor:not-allowed} +
Mise à jour +
Version installée : …
+
+
+
@@ -640,7 +645,34 @@ async function loadStatus(){ el.innerHTML=''+txt+' :'+s.port+''; MODEL_READY = !!(s.active && s.health); if(s.ctx){ CTX_MAX=s.ctx; updateCtxMeter(); } - if(s.version){ document.getElementById('ver').textContent='v'+s.version; } + if(s.version){ + document.getElementById('ver').textContent='v'+s.version; + const uc=document.getElementById('upd-cur'); if(uc) uc.textContent='v'+s.version; + } +} +async function checkUpdate(){ + const b=document.getElementById('upd-check'), msg=document.getElementById('upd-msg'); + b.disabled=true; msg.textContent='Vérification…'; + try{ + const r=await jget('/api/update'); + if(r.current){ document.getElementById('upd-cur').textContent='v'+r.current; } + if(r.error){ msg.textContent='Erreur : '+r.error; } + else if(r.available){ + msg.innerHTML='Nouvelle version v'+r.latest+' disponible. '; + const btn=document.createElement('button'); btn.textContent='Mettre à jour'; btn.onclick=applyUpdate; + msg.appendChild(btn); + } else { msg.textContent='Jean est à jour ✓'; } + }catch(e){ msg.textContent='Erreur réseau'; } + b.disabled=false; +} +async function applyUpdate(){ + const msg=document.getElementById('upd-msg'); + msg.textContent='Téléchargement et installation…'; + try{ + const r=await jpost('/api/update/apply',{}); + if(r.ok){ msg.innerHTML='✓ Installé en v'+r.version+'.
'+(r.restart||''); } + else { msg.textContent='Échec : '+(r.error||'inconnu'); } + }catch(e){ msg.textContent='Erreur pendant la mise à jour (réessaie).'; } } // Compteur de contexte : CTX_USED estimé via les stats serveur (prefill+decode // du dernier tour ≈ taille du prochain prompt). À 90% on propose de compacter. diff --git a/update.go b/update.go index e96f078..3391e30 100644 --- a/update.go +++ b/update.go @@ -69,32 +69,46 @@ func fetchLatestRelease() (*ghRelease, error) { return &rel, nil } -func cmdUpdate(args []string) error { - checkOnly := false - for _, a := range args { - if a == "--check" || a == "-check" || a == "check" { - checkOnly = true - } - } - fmt.Println("recherche de la dernière version…") +// updateInfo décrit l'état de mise à jour (partagé CLI + UI web). +type updateInfo struct { + Current string `json:"current"` + Latest string `json:"latest"` + Available bool `json:"available"` + URL string `json:"url"` +} + +// checkForUpdate interroge GitHub et compare à la version courante. Réutilisé +// par `jean update` (CLI) et par l'endpoint web /api/update. +func checkForUpdate() (updateInfo, error) { + info := updateInfo{Current: Version} rel, err := fetchLatestRelease() if err != nil { - return fmt.Errorf("impossible de contacter GitHub : %w", err) + return info, err } latest := ensureV(rel.TagName) - cur := ensureV(Version) if !semver.IsValid(latest) { - return fmt.Errorf("tag de release inattendu : %q", rel.TagName) + return info, fmt.Errorf("tag de release inattendu : %q", rel.TagName) } - if semver.Compare(latest, cur) <= 0 { - fmt.Printf("jean est déjà à jour (%s).\n", Version) - return nil + info.Latest = strings.TrimPrefix(latest, "v") + info.URL = rel.HTMLURL + info.Available = semver.Compare(latest, ensureV(Version)) > 0 + return info, nil +} + +// applyUpdate télécharge et installe le binaire de la dernière release pour +// l'OS/arch courant. Renvoie la nouvelle version. Ne redémarre AUCUN service +// (voir printRestartHint / le message renvoyé à l'UI). +func applyUpdate() (string, error) { + rel, err := fetchLatestRelease() + if err != nil { + return "", fmt.Errorf("impossible de contacter GitHub : %w", err) } - fmt.Printf("nouvelle version disponible : %s (actuelle : %s)\n", strings.TrimPrefix(latest, "v"), Version) - fmt.Printf(" %s\n", rel.HTMLURL) - if checkOnly { - fmt.Println("lance 'jean update' pour l'installer.") - return nil + latest := ensureV(rel.TagName) + if !semver.IsValid(latest) { + return "", fmt.Errorf("tag de release inattendu : %q", rel.TagName) + } + if semver.Compare(latest, ensureV(Version)) <= 0 { + return Version, fmt.Errorf("déjà à jour (%s)", Version) } want := updateAssetName() @@ -107,45 +121,71 @@ func cmdUpdate(args []string) error { } } if url == "" { - return fmt.Errorf("aucun binaire %q dans la release %s (os/arch %s/%s)", want, latest, runtime.GOOS, runtime.GOARCH) + return "", fmt.Errorf("aucun binaire %q dans la release %s (os/arch %s/%s)", want, latest, runtime.GOOS, runtime.GOARCH) } exe, err := os.Executable() if err != nil { - return err + return "", err } if resolved, err := filepath.EvalSymlinks(exe); err == nil { exe = resolved } - dir := filepath.Dir(exe) - tmp := filepath.Join(dir, ".jean-update.tmp") + tmp := filepath.Join(filepath.Dir(exe), ".jean-update.tmp") - fmt.Printf("téléchargement de %s (%.1f Mo)…\n", want, float64(size)/1e6) if err := downloadTo(url, tmp); err != nil { - return fmt.Errorf("téléchargement : %w", err) + return "", fmt.Errorf("téléchargement : %w", err) } - // Conserver les permissions du binaire existant (sinon 0755 par défaut). mode := os.FileMode(0o755) if fi, err := os.Stat(exe); err == nil { mode = fi.Mode() } if err := os.Chmod(tmp, mode); err != nil { os.Remove(tmp) - return err + return "", err } if got := fileSize(tmp); size > 0 && got != size { os.Remove(tmp) - return fmt.Errorf("taille inattendue (%d o reçus, %d attendus) — mise à jour annulée", got, size) + return "", fmt.Errorf("taille inattendue (%d o reçus, %d attendus) — mise à jour annulée", got, size) } - if err := replaceBinary(exe, tmp); err != nil { os.Remove(tmp) if os.IsPermission(err) { - return fmt.Errorf("droits insuffisants pour écrire %s — relance avec privilèges (ex : sudo jean update)", exe) + return "", fmt.Errorf("droits insuffisants pour écrire %s — relance avec privilèges (ex : sudo jean update)", exe) } + return "", err + } + return strings.TrimPrefix(latest, "v"), nil +} + +func cmdUpdate(args []string) error { + checkOnly := false + for _, a := range args { + if a == "--check" || a == "-check" || a == "check" { + checkOnly = true + } + } + fmt.Println("recherche de la dernière version…") + info, err := checkForUpdate() + if err != nil { + return fmt.Errorf("impossible de contacter GitHub : %w", err) + } + if !info.Available { + fmt.Printf("jean est déjà à jour (%s).\n", Version) + return nil + } + fmt.Printf("nouvelle version disponible : %s (actuelle : %s)\n", info.Latest, Version) + fmt.Printf(" %s\n", info.URL) + if checkOnly { + fmt.Println("lance 'jean update' pour l'installer.") + return nil + } + fmt.Printf("téléchargement de %s…\n", updateAssetName()) + newVer, err := applyUpdate() + if err != nil { return err } - fmt.Printf("✓ jean mis à jour en %s\n", strings.TrimPrefix(latest, "v")) + fmt.Printf("✓ jean mis à jour en %s\n", newVer) printRestartHint() return nil } @@ -212,10 +252,32 @@ func cleanupOldBinary() { } } -func printRestartHint() { - if runtime.GOOS == "windows" { - fmt.Println("redémarre les process jean en cours (jean web, etc.) pour appliquer la mise à jour.") +// handleUpdateCheck (GET /api/update) : renvoie l'état de mise à jour pour l'UI. +func handleUpdateCheck(w http.ResponseWriter, r *http.Request) { + info, err := checkForUpdate() + if err != nil { + sendJSON(w, 200, map[string]any{"current": Version, "available": false, "error": err.Error()}) return } - fmt.Println("pense à redémarrer les services : sudo systemctl restart jean jean-link (+ relancer 'jean web' si utilisé).") + sendJSON(w, 200, info) } + +// handleUpdateApply (POST /api/update/apply) : télécharge et installe la dernière +// version, puis renvoie la version installée + l'indice de redémarrage. +func handleUpdateApply(w http.ResponseWriter, r *http.Request) { + newVer, err := applyUpdate() + if err != nil { + sendJSON(w, 200, map[string]any{"ok": false, "error": err.Error()}) + return + } + sendJSON(w, 200, map[string]any{"ok": true, "version": newVer, "restart": restartHintText()}) +} + +func restartHintText() string { + if runtime.GOOS == "windows" { + return "Redémarre Jean (quitte puis relance) pour appliquer la mise à jour." + } + return "Redémarre les services pour appliquer : sudo systemctl restart jean jean-link" +} + +func printRestartHint() { fmt.Println(restartHintText()) } diff --git a/versioninfo.json b/versioninfo.json index 496dcbd..a20087c 100644 --- a/versioninfo.json +++ b/versioninfo.json @@ -3,13 +3,13 @@ "FileVersion": { "Major": 0, "Minor": 3, - "Patch": 3, + "Patch": 4, "Build": 0 }, "ProductVersion": { "Major": 0, "Minor": 3, - "Patch": 3, + "Patch": 4, "Build": 0 }, "FileFlagsMask": "3f", @@ -25,7 +25,7 @@ "LegalCopyright": "Copyright (c) 2026 Jean contributors. MIT License.", "OriginalFilename": "jean.exe", "ProductName": "Jean", - "ProductVersion": "0.3.3", + "ProductVersion": "0.3.4", "Comments": "https://github.com/nathaninline/jean — projet open source (MIT)" }, "VarFileInfo": { diff --git a/web.go b/web.go index 42a3b0d..433b6dc 100644 --- a/web.go +++ b/web.go @@ -75,6 +75,8 @@ func newWebMux() *http.ServeMux { api("/api/vram", handleVram) api("/api/config", handleConfigEnv) api("/api/catalog", handleCatalog) + api("/api/update", handleUpdateCheck) + api("/api/update/apply", handleUpdateApply) api("/api/models", handleModels) api("/api/models/delete", handleModelDelete) api("/api/models/download", handleModelDownload)