Files
Loki/internal/ajean/cli_app.go
T
nathaninline 110458c133 Renommage AJEAN (1/3) : module, chemins de donnees et auto-MAJ
Socle du renommage jean -> AJEAN, sans aucun impact visible pour le parc
installe. Trois briques :

- module github.com/nathaninline/ajean, cmd/ajean, internal/ajean,
  package ajean. Purement interne.

- AjeanHome() remplace JeanHome(). Les defauts deviennent
  %ProgramData%\ajean et /etc/ajean, avec migration du dossier existant
  au premier lancement. La migration est un os.Rename dans le dossier
  parent de l'ancien chemin : intra-volume, donc atomique et instantane
  meme avec des .gguf de plusieurs dizaines de Go. Si le rename echoue
  (handle ouvert sous Windows, droits, volume en lecture seule) on
  CONTINUE sur l'ancien chemin et on retentera : jamais de copie, jamais
  de suppression, le pire cas est "rien n'a bouge".
  $AJEAN_HOME et $JEAN_HOME restent tous deux honores, ainsi que
  /etc/default/ajean et /etc/default/jean : un chemin impose a la main
  est un ordre, on ne le migre pas.

- l'auto-MAJ accepte desormais ajean-<os>-<arch> ET jean-<os>-<arch>,
  dans cet ordre. Indispensable dans les deux sens : les binaires deja
  installes ne connaissent que jean-*, et ce binaire-ci doit pouvoir
  s'installer depuis une release anterieure au renommage.

Couvert par des tests, dont le repli quand un handle ouvert bloque le
rename sous Windows.
2026-08-04 13:59:25 +02:00

85 lines
2.8 KiB
Go

package ajean
// cli_app.go — expérience « application » de Jean.
//
// Quand on double-clique sur le binaire (aucun argument, console fraîche), au
// lieu d'afficher l'aide dans une console qui se ferme, Jean démarre son UI web,
// l'ouvre dans le navigateur ET pose une icône dans la zone de notification
// Windows (voir sys_tray_windows.go) : on voit que Jean tourne et on le pilote
// (« Ouvrir Jean » / « Quitter »).
import (
"fmt"
"net"
"net/http"
"time"
)
const appPort = 8090
// cmdApp lance l'UI web, l'ouvre dans le navigateur et fait tourner l'icône de
// la zone de notification (Windows). Sur une machine vierge, il crée d'abord le
// dossier de données + config.env pour que l'interface s'ouvre quand même
// (l'écran d'accueil prendra le relais pour le choix du modèle).
func cmdApp(args []string) error {
url := fmt.Sprintf("http://localhost:%d", appPort)
// Premier lancement : prépare le dossier de données et, sous Windows, propose
// explicitement l'installation (voir sys_firstrun_windows.go). Si l'app a été
// relancée depuis la copie installée, ce process n'a plus rien à faire.
if appFirstRun() {
return nil
}
addr := fmt.Sprintf("0.0.0.0:%d", appPort)
ln, err := net.Listen("tcp", addr)
if err != nil {
// Jean tourne déjà sur ce port : on ouvre juste l'UI sur l'instance
// existante plutôt que d'échouer.
fmt.Printf("AJEAN est déjà lancé — ouverture de %s\n", url)
return openBrowser(url)
}
// UN SEUL process propriétaire de la conversation, comme le service jean-link
// sous Linux : l'app sert l'UI locale ET le tunnel avec le MÊME mux. Deux
// process qui servent la conversation (objet en mémoire persisté dans
// conversation.json) donnent deux fils divergents entre le local et
// app.ajean.link — le bug qu'on a vécu avec le worker détaché.
appOwnsLink = true
appWebMux = newWebMux()
go func() { _ = http.Serve(ln, appWebMux) }()
go func() {
if readLinkToken() == "" {
return
}
// Worker détaché rescapé (version antérieure, autre copie de l'app) : il
// possède SA propre conversation et écrase la nôtre. On le supprime avant
// de prendre la main.
killForeignLinkWorker()
startAppLink(appWebMux)
}()
sp := showSplash("Lancement d'AJEAN en cours…")
waitServerReady(url)
_ = openBrowser(url)
time.Sleep(900 * time.Millisecond) // laisse le navigateur s'afficher par-dessus le splash
sp.close()
runTray(url) // icône zone de notification ; bloque jusqu'à « Quitter »
return nil
}
// waitServerReady attend que l'UI réponde (jusqu'à ~4 s) pour n'ouvrir le
// navigateur qu'une fois le serveur prêt.
func waitServerReady(url string) {
c := &http.Client{Timeout: 400 * time.Millisecond}
for i := 0; i < 20; i++ {
if resp, err := c.Get(url); err == nil {
resp.Body.Close()
return
}
time.Sleep(150 * time.Millisecond)
}
}