mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-23 23:18:13 +02:00
v0.3.2 : commande 'jean update' (auto-mise a jour depuis les releases GitHub)
This commit is contained in:
1 parent
342cdd0690
commit
f58299632b
6 files changed
+231
-5
No files matched your search
@@ -6,6 +6,7 @@ require (
|
|||||||
github.com/caddyserver/certmagic v0.25.4
|
github.com/caddyserver/certmagic v0.25.4
|
||||||
github.com/coder/websocket v1.8.15
|
github.com/coder/websocket v1.8.15
|
||||||
github.com/hashicorp/yamux v0.1.2
|
github.com/hashicorp/yamux v0.1.2
|
||||||
|
golang.org/x/mod v0.35.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -19,7 +20,6 @@ require (
|
|||||||
go.uber.org/zap v1.27.1 // indirect
|
go.uber.org/zap v1.27.1 // indirect
|
||||||
go.uber.org/zap/exp v0.3.0 // indirect
|
go.uber.org/zap/exp v0.3.0 // indirect
|
||||||
golang.org/x/crypto v0.50.0 // indirect
|
golang.org/x/crypto v0.50.0 // indirect
|
||||||
golang.org/x/mod v0.35.0 // indirect
|
|
||||||
golang.org/x/net v0.53.0 // indirect
|
golang.org/x/net v0.53.0 // indirect
|
||||||
golang.org/x/sync v0.20.0 // indirect
|
golang.org/x/sync v0.20.0 // indirect
|
||||||
golang.org/x/sys v0.43.0 // indirect
|
golang.org/x/sys v0.43.0 // indirect
|
||||||
|
|||||||
@@ -16,12 +16,14 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const Version = "0.3.1"
|
const Version = "0.3.2"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Migration one-shot des anciens skills (SKILLS/<nom>/SKILL.md) vers la
|
// Migration one-shot des anciens skills (SKILLS/<nom>/SKILL.md) vers la
|
||||||
// nouvelle mémoire (MEMORY/<nom>.md). Idempotente, silencieuse si rien à faire.
|
// nouvelle mémoire (MEMORY/<nom>.md). Idempotente, silencieuse si rien à faire.
|
||||||
migrateSkillsToMemory()
|
migrateSkillsToMemory()
|
||||||
|
// Nettoie un éventuel binaire .old laissé par une mise à jour Windows.
|
||||||
|
cleanupOldBinary()
|
||||||
|
|
||||||
args := os.Args[1:]
|
args := os.Args[1:]
|
||||||
cmd := "help"
|
cmd := "help"
|
||||||
@@ -71,6 +73,8 @@ func main() {
|
|||||||
mustExit(cmdBench(args))
|
mustExit(cmdBench(args))
|
||||||
case "llamacpp", "llama":
|
case "llamacpp", "llama":
|
||||||
mustExit(cmdLlamacpp(args))
|
mustExit(cmdLlamacpp(args))
|
||||||
|
case "update", "upgrade", "self-update":
|
||||||
|
mustExit(cmdUpdate(args))
|
||||||
case "install":
|
case "install":
|
||||||
mustExit(cmdInstall(args))
|
mustExit(cmdInstall(args))
|
||||||
case "uninstall":
|
case "uninstall":
|
||||||
@@ -134,6 +138,7 @@ Entrypoint (utilisé par jean.service) :
|
|||||||
Installation:
|
Installation:
|
||||||
install installer (Linux: unité systemd, sudoers, dossiers ; Windows: dossiers + config)
|
install installer (Linux: unité systemd, sudoers, dossiers ; Windows: dossiers + config)
|
||||||
uninstall désinstaller
|
uninstall désinstaller
|
||||||
|
update [--check] mettre à jour jean depuis les releases GitHub (--check = signale sans installer)
|
||||||
|
|
||||||
Env:
|
Env:
|
||||||
JEAN_HOME racine (défaut: /etc/jean sur Linux/macOS, %%ProgramData%%\jean sur Windows)
|
JEAN_HOME racine (défaut: /etc/jean sur Linux/macOS, %%ProgramData%%\jean sur Windows)
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,221 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/mod/semver"
|
||||||
|
)
|
||||||
|
|
||||||
|
// jean update — met à jour le binaire depuis les releases GitHub du projet.
|
||||||
|
// Périmètre minimal : compare la version, télécharge l'asset correspondant à
|
||||||
|
// l'OS/arch courant, remplace le binaire en place, puis affiche quoi redémarrer.
|
||||||
|
// AUCUN redémarrage de service automatique (choix volontaire, plus sûr).
|
||||||
|
|
||||||
|
const updateRepo = "nathaninline/jean"
|
||||||
|
|
||||||
|
type ghRelease struct {
|
||||||
|
TagName string `json:"tag_name"`
|
||||||
|
HTMLURL string `json:"html_url"`
|
||||||
|
Assets []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
BrowserDownloadURL string `json:"browser_download_url"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
} `json:"assets"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateAssetName reconstruit le nom d'asset attendu pour la plateforme courante,
|
||||||
|
// suivant la convention des releases : jean-<os>-<arch>[.exe].
|
||||||
|
func updateAssetName() string {
|
||||||
|
n := "jean-" + runtime.GOOS + "-" + runtime.GOARCH
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
n += ".exe"
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureV préfixe "v" si absent, pour comparer via golang.org/x/mod/semver.
|
||||||
|
func ensureV(s string) string {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if s != "" && !strings.HasPrefix(s, "v") {
|
||||||
|
s = "v" + s
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchLatestRelease() (*ghRelease, error) {
|
||||||
|
req, _ := http.NewRequest("GET", "https://api.github.com/repos/"+updateRepo+"/releases/latest", nil)
|
||||||
|
req.Header.Set("Accept", "application/vnd.github+json")
|
||||||
|
req.Header.Set("User-Agent", "jean-update")
|
||||||
|
resp, err := (&http.Client{Timeout: 30 * time.Second}).Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
return nil, fmt.Errorf("GitHub a répondu %s", resp.Status)
|
||||||
|
}
|
||||||
|
var rel ghRelease
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&rel); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
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…")
|
||||||
|
rel, err := fetchLatestRelease()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("impossible de contacter GitHub : %w", err)
|
||||||
|
}
|
||||||
|
latest := ensureV(rel.TagName)
|
||||||
|
cur := ensureV(Version)
|
||||||
|
if !semver.IsValid(latest) {
|
||||||
|
return 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
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
want := updateAssetName()
|
||||||
|
var url string
|
||||||
|
var size int64
|
||||||
|
for _, a := range rel.Assets {
|
||||||
|
if a.Name == want {
|
||||||
|
url, size = a.BrowserDownloadURL, a.Size
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if url == "" {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
if resolved, err := filepath.EvalSymlinks(exe); err == nil {
|
||||||
|
exe = resolved
|
||||||
|
}
|
||||||
|
dir := filepath.Dir(exe)
|
||||||
|
tmp := filepath.Join(dir, ".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)
|
||||||
|
}
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 err
|
||||||
|
}
|
||||||
|
fmt.Printf("✓ jean mis à jour en %s\n", strings.TrimPrefix(latest, "v"))
|
||||||
|
printRestartHint()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func downloadTo(url, dst string) error {
|
||||||
|
req, _ := http.NewRequest("GET", url, nil)
|
||||||
|
req.Header.Set("User-Agent", "jean-update")
|
||||||
|
req.Header.Set("Accept", "application/octet-stream")
|
||||||
|
resp, err := (&http.Client{Timeout: 5 * time.Minute}).Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
return fmt.Errorf("GitHub a répondu %s", resp.Status)
|
||||||
|
}
|
||||||
|
f, err := os.Create(dst)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, cErr := io.Copy(f, resp.Body)
|
||||||
|
if closeErr := f.Close(); cErr == nil {
|
||||||
|
cErr = closeErr
|
||||||
|
}
|
||||||
|
return cErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func fileSize(p string) int64 {
|
||||||
|
if fi, err := os.Stat(p); err == nil {
|
||||||
|
return fi.Size()
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// replaceBinary échange le binaire en place. Sous Unix, rename() dans le même
|
||||||
|
// dossier est atomique et fonctionne même si l'ancien binaire tourne encore
|
||||||
|
// (l'inode ouvert reste valide). Sous Windows on ne peut pas écraser un .exe en
|
||||||
|
// cours : on renomme l'ancien en .old (supprimé au prochain lancement).
|
||||||
|
func replaceBinary(exe, tmp string) error {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
old := exe + ".old"
|
||||||
|
_ = os.Remove(old) // nettoyage d'une éventuelle MAJ précédente
|
||||||
|
if err := os.Rename(exe, old); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.Rename(tmp, exe); err != nil {
|
||||||
|
_ = os.Rename(old, exe) // rollback
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_ = os.Remove(old) // échoue si l'exe tourne encore → nettoyé au prochain run
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return os.Rename(tmp, exe)
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanupOldBinary supprime silencieusement le .old laissé par une MAJ Windows
|
||||||
|
// précédente (le fichier n'était pas supprimable tant que l'exe tournait).
|
||||||
|
func cleanupOldBinary() {
|
||||||
|
if runtime.GOOS != "windows" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if exe, err := os.Executable(); err == nil {
|
||||||
|
_ = os.Remove(exe + ".old")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printRestartHint() {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
fmt.Println("redémarre les process jean en cours (jean web, etc.) pour appliquer la mise à jour.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println("pense à redémarrer les services : sudo systemctl restart jean jean-link (+ relancer 'jean web' si utilisé).")
|
||||||
|
}
|
||||||
+3
-3
@@ -3,13 +3,13 @@
|
|||||||
"FileVersion": {
|
"FileVersion": {
|
||||||
"Major": 0,
|
"Major": 0,
|
||||||
"Minor": 3,
|
"Minor": 3,
|
||||||
"Patch": 1,
|
"Patch": 2,
|
||||||
"Build": 0
|
"Build": 0
|
||||||
},
|
},
|
||||||
"ProductVersion": {
|
"ProductVersion": {
|
||||||
"Major": 0,
|
"Major": 0,
|
||||||
"Minor": 3,
|
"Minor": 3,
|
||||||
"Patch": 1,
|
"Patch": 2,
|
||||||
"Build": 0
|
"Build": 0
|
||||||
},
|
},
|
||||||
"FileFlagsMask": "3f",
|
"FileFlagsMask": "3f",
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
"LegalCopyright": "Copyright (c) 2026 Jean contributors. MIT License.",
|
"LegalCopyright": "Copyright (c) 2026 Jean contributors. MIT License.",
|
||||||
"OriginalFilename": "jean.exe",
|
"OriginalFilename": "jean.exe",
|
||||||
"ProductName": "Jean",
|
"ProductName": "Jean",
|
||||||
"ProductVersion": "0.3.1",
|
"ProductVersion": "0.3.2",
|
||||||
"Comments": "https://github.com/nathaninline/jean — projet open source (MIT)"
|
"Comments": "https://github.com/nathaninline/jean — projet open source (MIT)"
|
||||||
},
|
},
|
||||||
"VarFileInfo": {
|
"VarFileInfo": {
|
||||||
|
|||||||
Reference in new issue
Block a user