mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-15 03:08:56 +02:00
- mode agent : les outils write/edit/bash résolvent les chemins relatifs dans JEAN_HOME/workspace et le shell y démarre, au lieu du répertoire courant du processus (Bureau, ProgramData\jean\bin). Les chemins absolus demandés explicitement restent honorés. - Windows : le double-clic n'installe plus en silence. Boîte de dialogue unique, raccourcis menu Démarrer + Bureau, puis relance depuis la copie installée — un seul binaire fait foi. Désinstallation : retrait des raccourcis. - update : contrôle des droits AVANT le téléchargement (l'échec tombait sur le fichier temporaire, avec un message brut), message donnant la commande exacte, timeout UI porté de 30 s à 10 min. - jean where + bouton « Où sont mes fichiers ? » : emplacements, et alerte quand le binaire lancé n'est pas celui installé. - icônes : source unique (sys_brand_icon.go) pour la zone de notification, la barre de menus macOS et l'icône du .exe, alignée sur le favicon noir. macOS en icône template (une icône noire figée disparaît sur une barre sombre). - CI : le bundle macOS part de icon.png au lieu de convertir le .ico via sips, dont l'échec était masqué par « || true » ; notes de release versionnées.
173 lines
6.6 KiB
Go
173 lines
6.6 KiB
Go
//go:build windows
|
|
|
|
package jean
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"syscall"
|
|
"unsafe"
|
|
)
|
|
|
|
// Premier lancement sous Windows.
|
|
//
|
|
// Avant : double-cliquer jean-windows-amd64.exe déclenchait une installation
|
|
// SILENCIEUSE (copie du binaire dans %ProgramData%\jean\bin, ajout au PATH) dont
|
|
// l'utilisateur ne voyait rien — cmdApp appelait cmdInstall sans console pour en
|
|
// afficher la sortie. D'où la confusion légitime : est-ce que ça installe ou est-ce
|
|
// que ça lance ? Et surtout, deux copies du binaire coexistaient (celle du Bureau,
|
|
// qui tourne, et celle installée) : « mettre à jour » ne touchait que celle lancée,
|
|
// tandis que le raccourci et le PATH pointaient sur l'autre, restée en arrière.
|
|
//
|
|
// Maintenant : on DEMANDE, une bonne fois. Si l'utilisateur accepte, on installe
|
|
// pour de bon (copie + PATH + raccourcis menu Démarrer/Bureau) puis on relance
|
|
// depuis la copie installée et on quitte — il n'existe alors qu'UN seul binaire qui
|
|
// compte, à un emplacement connu, et le bouton de mise à jour agit dessus. S'il
|
|
// refuse, on lance l'application telle quelle, sans rien écrire ailleurs que dans
|
|
// le dossier de données.
|
|
|
|
const (
|
|
mbYesNo = 0x00000004
|
|
mbIconQuestion = 0x00000020
|
|
mbIconInfo = 0x00000040
|
|
mbTopmost = 0x00040000
|
|
idYes = 6
|
|
)
|
|
|
|
var pMessageBoxW = u32s.NewProc("MessageBoxW")
|
|
|
|
func messageBox(text, title string, flags uintptr) int {
|
|
t, _ := syscall.UTF16PtrFromString(text)
|
|
c, _ := syscall.UTF16PtrFromString(title)
|
|
r, _, _ := pMessageBoxW.Call(0, uintptr(unsafe.Pointer(t)), uintptr(unsafe.Pointer(c)), flags|mbTopmost)
|
|
return int(r)
|
|
}
|
|
|
|
// appFirstRun prépare le dossier de données et, si le binaire lancé n'est pas
|
|
// celui installé, propose l'installation. Renvoie true si l'app a été relancée
|
|
// depuis la copie installée (l'appelant doit alors rendre la main immédiatement).
|
|
func appFirstRun() bool {
|
|
// Le dossier de données et config.env sont indispensables pour que l'UI
|
|
// s'ouvre : on les crée sans rien demander, ils ne surprennent personne.
|
|
_ = provisionDataDir()
|
|
|
|
exe, err := os.Executable()
|
|
if err != nil {
|
|
return false
|
|
}
|
|
if resolved, err := filepath.EvalSymlinks(exe); err == nil {
|
|
exe = resolved
|
|
}
|
|
target := installedExePath()
|
|
if strings.EqualFold(exe, target) {
|
|
return false // on EST déjà l'application installée
|
|
}
|
|
if _, err := os.Stat(target); err == nil {
|
|
// Une copie est déjà installée — typiquement par l'installation silencieuse
|
|
// des versions ≤ 0.6.9. On le signale UNE SEULE FOIS : répéter la boîte à
|
|
// chaque double-clic du fichier téléchargé serait un harcèlement, pas une
|
|
// information.
|
|
notice := filepath.Join(JeanHome(), ".install_notice")
|
|
if _, seen := os.Stat(notice); seen != nil {
|
|
messageBox(
|
|
"AJEAN est déjà installé sur cet ordinateur :\n\n"+target+
|
|
"\n\nCe fichier-ci n'est qu'une copie téléchargée : vous pouvez la supprimer et lancer AJEAN depuis le menu Démarrer.\n\nCette copie va démarrer normalement.",
|
|
"AJEAN", mbIconInfo)
|
|
_ = os.WriteFile(notice, []byte(target+"\n"), 0o644)
|
|
}
|
|
return false
|
|
}
|
|
|
|
if messageBox(
|
|
"Installer AJEAN sur cet ordinateur ?\n\n"+
|
|
"• le programme sera copié dans :\n "+target+"\n"+
|
|
"• un raccourci sera ajouté au menu Démarrer et au Bureau\n"+
|
|
"• vos réglages et modèles seront rangés dans :\n "+JeanHome()+"\n\n"+
|
|
"Vous pourrez ensuite supprimer le fichier que vous venez de télécharger.\n\n"+
|
|
"Répondre Non lance AJEAN sans rien installer.",
|
|
"Installation d'AJEAN", mbYesNo|mbIconQuestion) != idYes {
|
|
return false
|
|
}
|
|
|
|
if _, err := installSelf(filepath.Dir(target)); err != nil {
|
|
messageBox("Installation impossible :\n\n"+err.Error()+"\n\nAJEAN va démarrer depuis l'emplacement actuel.", "AJEAN", mbIconInfo)
|
|
return false
|
|
}
|
|
_, _ = addToUserPath(filepath.Dir(target))
|
|
shortcuts := createShortcuts(target)
|
|
|
|
messageBox("AJEAN est installé.\n\n"+target+"\n\n"+shortcuts+
|
|
"\n\nL'application va démarrer. Vous pouvez supprimer le fichier téléchargé.",
|
|
"AJEAN", mbIconInfo)
|
|
|
|
// Relance depuis la copie installée : à partir de maintenant, une seule et
|
|
// même image du programme se met à jour, se relance et apparaît dans le tray.
|
|
cmd := exec.Command(target)
|
|
cmd.Dir = filepath.Dir(target)
|
|
if err := cmd.Start(); err != nil {
|
|
return false // échec du relancement : on continue avec la copie courante
|
|
}
|
|
return true
|
|
}
|
|
|
|
// provisionDataDir crée le dossier de données et un config.env de départ.
|
|
// Extrait de cmdInstall pour être réutilisable au premier lancement, sans
|
|
// embarquer l'installation du binaire ni les messages de console.
|
|
func provisionDataDir() error {
|
|
jeanHome := JeanHome()
|
|
for _, d := range []string{jeanHome, filepath.Join(jeanHome, "configs"), filepath.Join(jeanHome, "SKILLS")} {
|
|
if err := os.MkdirAll(d, 0o755); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
conf := filepath.Join(jeanHome, "config.env")
|
|
if _, err := os.Stat(conf); os.IsNotExist(err) {
|
|
return os.WriteFile(conf, []byte(configTemplate), 0o644)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// createShortcuts pose un raccourci « AJEAN » dans le menu Démarrer et sur le
|
|
// Bureau. Renvoie une phrase décrivant ce qui a été créé (affichée à
|
|
// l'utilisateur). Best-effort : un échec n'empêche pas l'installation.
|
|
func createShortcuts(target string) string {
|
|
ps := fmt.Sprintf(`$t=%s
|
|
$w=New-Object -ComObject WScript.Shell
|
|
$done=@()
|
|
foreach ($d in @([Environment]::GetFolderPath('StartMenu'), [Environment]::GetFolderPath('Desktop'))) {
|
|
if (-not $d) { continue }
|
|
try {
|
|
$s=$w.CreateShortcut((Join-Path $d 'AJEAN.lnk'))
|
|
$s.TargetPath=$t
|
|
$s.WorkingDirectory=(Split-Path $t)
|
|
$s.Description='AJEAN — IA locale'
|
|
$s.Save()
|
|
$done+=$d
|
|
} catch {}
|
|
}
|
|
Write-Output ($done -join ';')`, psQuote(target))
|
|
cmd := hideCmd(exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", ps))
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil || strings.TrimSpace(string(out)) == "" {
|
|
return "(raccourcis non créés — lancez AJEAN depuis " + target + ")"
|
|
}
|
|
return "Raccourci « AJEAN » ajouté au menu Démarrer et au Bureau."
|
|
}
|
|
|
|
// removeShortcuts efface les raccourcis posés par createShortcuts. Renvoie true
|
|
// si au moins un a été supprimé.
|
|
func removeShortcuts() bool {
|
|
ps := `$n=0
|
|
foreach ($d in @([Environment]::GetFolderPath('StartMenu'), [Environment]::GetFolderPath('Desktop'))) {
|
|
if (-not $d) { continue }
|
|
$p=Join-Path $d 'AJEAN.lnk'
|
|
if (Test-Path $p) { try { Remove-Item $p -Force; $n++ } catch {} }
|
|
}
|
|
Write-Output $n`
|
|
out, err := hideCmd(exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", ps)).CombinedOutput()
|
|
return err == nil && strings.TrimSpace(string(out)) != "0"
|
|
}
|