diff --git a/README.md b/README.md index 4b184c6..ed52c0b 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Aucune dépendance à l'exécution, aucun flag CMake à retenir, aucun conteneur ### 1. Le binaire ```bash -curl -L -o jean https://github.com/nathaninline/jean/releases/latest/download/jean-linux-amd64 +curl -L -o jean https://github.com/nathaninline/ajean/releases/latest/download/jean-linux-amd64 chmod +x jean sudo mv jean /usr/local/bin/jean ``` @@ -264,7 +264,7 @@ Chaque appel `/api/*` présente alors `Authorization: Bearer ` : Go 1.25+. AJEAN est écrit à 100 % en Go, l'interface est embarquée via `go:embed` : ```bash -git clone https://github.com/nathaninline/jean.git +git clone https://github.com/nathaninline/ajean.git cd jean CGO_ENABLED=0 go build -o jean ./cmd/jean ``` diff --git a/cmd/jean/icon.ico b/cmd/ajean/icon.ico similarity index 100% rename from cmd/jean/icon.ico rename to cmd/ajean/icon.ico diff --git a/cmd/jean/icon.png b/cmd/ajean/icon.png similarity index 100% rename from cmd/jean/icon.png rename to cmd/ajean/icon.png diff --git a/cmd/jean/main.go b/cmd/ajean/main.go similarity index 74% rename from cmd/jean/main.go rename to cmd/ajean/main.go index b7559cd..51b0427 100644 --- a/cmd/jean/main.go +++ b/cmd/ajean/main.go @@ -1,4 +1,4 @@ -// Point d'entrée du binaire jean. Tout le code vit dans internal/jean ; ce +// Point d'entrée du binaire ajean. Tout le code vit dans internal/jean ; ce // dossier ne porte que le main() et les ressources Windows (.syso, icône, // versioninfo) qui doivent résider dans le dossier du package main. // @@ -7,11 +7,11 @@ // les .syso après avoir bumpé la version : `go generate ./...` // (nécessite : go install github.com/josephspurrier/goversioninfo/cmd/goversioninfo@latest) // -//go:generate go run github.com/nathaninline/jean/tools/gen-icon icon.ico +//go:generate go run github.com/nathaninline/ajean/tools/gen-icon icon.ico //go:generate goversioninfo -64 -icon=icon.ico -o resource_windows_amd64.syso versioninfo.json //go:generate goversioninfo -64 -arm -icon=icon.ico -o resource_windows_arm64.syso versioninfo.json package main -import "github.com/nathaninline/jean/internal/jean" +import "github.com/nathaninline/ajean/internal/ajean" -func main() { jean.Main() } +func main() { ajean.Main() } diff --git a/cmd/jean/resource_windows_amd64.syso b/cmd/ajean/resource_windows_amd64.syso similarity index 100% rename from cmd/jean/resource_windows_amd64.syso rename to cmd/ajean/resource_windows_amd64.syso diff --git a/cmd/jean/resource_windows_arm64.syso b/cmd/ajean/resource_windows_arm64.syso similarity index 100% rename from cmd/jean/resource_windows_arm64.syso rename to cmd/ajean/resource_windows_arm64.syso diff --git a/cmd/jean/versioninfo.json b/cmd/ajean/versioninfo.json similarity index 90% rename from cmd/jean/versioninfo.json rename to cmd/ajean/versioninfo.json index 507563d..3b3f5dd 100644 --- a/cmd/jean/versioninfo.json +++ b/cmd/ajean/versioninfo.json @@ -26,7 +26,7 @@ "OriginalFilename": "jean.exe", "ProductName": "AJEAN", "ProductVersion": "0.6.15", - "Comments": "https://github.com/nathaninline/jean — projet open source (MIT)" + "Comments": "https://github.com/nathaninline/ajean — projet open source (MIT)" }, "VarFileInfo": { "Translation": { diff --git a/go.mod b/go.mod index 5e588df..4d7ff1d 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/nathaninline/jean +module github.com/nathaninline/ajean go 1.25.0 diff --git a/internal/jean/backend_build.go b/internal/ajean/backend_build.go similarity index 99% rename from internal/jean/backend_build.go rename to internal/ajean/backend_build.go index 42c7639..f796c83 100644 --- a/internal/jean/backend_build.go +++ b/internal/ajean/backend_build.go @@ -1,6 +1,6 @@ // backend_build.go — machinerie de compilation de llama.cpp : détection du // plan de build (CUDA/ROCm/Metal/Vulkan/CPU), cmake, suivi de progression, logs. -package jean +package ajean import ( "bufio" diff --git a/internal/jean/backend_buildfilter_test.go b/internal/ajean/backend_buildfilter_test.go similarity index 99% rename from internal/jean/backend_buildfilter_test.go rename to internal/ajean/backend_buildfilter_test.go index f1d93b1..a1dd2ab 100644 --- a/internal/jean/backend_buildfilter_test.go +++ b/internal/ajean/backend_buildfilter_test.go @@ -1,4 +1,4 @@ -package jean +package ajean import "testing" diff --git a/internal/jean/backend_catalog.go b/internal/ajean/backend_catalog.go similarity index 99% rename from internal/jean/backend_catalog.go rename to internal/ajean/backend_catalog.go index eb7b13b..d63feb3 100644 --- a/internal/jean/backend_catalog.go +++ b/internal/ajean/backend_catalog.go @@ -1,4 +1,4 @@ -package jean +package ajean // backend_catalog.go — récupère la liste de modèles curatée servie par ajean.link et la // combine avec les infos matérielles locales, pour que l'écran d'accueil (à diff --git a/internal/jean/backend_config.go b/internal/ajean/backend_config.go similarity index 99% rename from internal/jean/backend_config.go rename to internal/ajean/backend_config.go index 75b6f7a..c8df85c 100644 --- a/internal/jean/backend_config.go +++ b/internal/ajean/backend_config.go @@ -1,4 +1,4 @@ -package jean +package ajean import ( "bufio" diff --git a/internal/jean/backend_gpu.go b/internal/ajean/backend_gpu.go similarity index 99% rename from internal/jean/backend_gpu.go rename to internal/ajean/backend_gpu.go index 9a16692..c70b6c6 100644 --- a/internal/jean/backend_gpu.go +++ b/internal/ajean/backend_gpu.go @@ -1,4 +1,4 @@ -package jean +package ajean import ( "bufio" diff --git a/internal/jean/backend_llamacpp.go b/internal/ajean/backend_llamacpp.go similarity index 99% rename from internal/jean/backend_llamacpp.go rename to internal/ajean/backend_llamacpp.go index 4ea1805..3748b93 100644 --- a/internal/jean/backend_llamacpp.go +++ b/internal/ajean/backend_llamacpp.go @@ -1,4 +1,4 @@ -package jean +package ajean import ( "fmt" @@ -82,7 +82,7 @@ func llamacppRepoDir() string { } func defaultRepoDir() string { - return filepath.Join(JeanHome(), "backends", "llama.cpp") + return filepath.Join(AjeanHome(), "backends", "llama.cpp") } // findRepoRoot walks up from a binary path (…/build/bin/llama-server) looking @@ -510,7 +510,7 @@ func sanitizeBackendName(name string) string { // dossier backends (../, chemin absolu…). func backendDir(name string) (string, error) { name = sanitizeBackendName(name) - root, err := filepath.Abs(filepath.Join(JeanHome(), "backends")) + root, err := filepath.Abs(filepath.Join(AjeanHome(), "backends")) if err != nil { return "", err } diff --git a/internal/jean/backend_model_dirs.go b/internal/ajean/backend_model_dirs.go similarity index 92% rename from internal/jean/backend_model_dirs.go rename to internal/ajean/backend_model_dirs.go index 3bef6f8..c9e843c 100644 --- a/internal/jean/backend_model_dirs.go +++ b/internal/ajean/backend_model_dirs.go @@ -1,4 +1,4 @@ -package jean +package ajean import ( "encoding/json" @@ -15,7 +15,7 @@ import ( // (disque dur externe, second SSD, montage réseau…). La liste est persistée // dans $JEAN_HOME/model_dirs.json — PAS dans config.env, que le changement de // preset réécrit — et peut aussi venir de $JEAN_MODEL_DIRS. -func modelDirsPath() string { return filepath.Join(JeanHome(), "model_dirs.json") } +func modelDirsPath() string { return filepath.Join(AjeanHome(), "model_dirs.json") } // modelDirsListSep sépare les dossiers dans $JEAN_MODEL_DIRS (':' sous Unix, // ';' sous Windows, comme le PATH). @@ -39,7 +39,7 @@ func extraModelDirs() []string { if v := strings.TrimSpace(os.Getenv("JEAN_MODEL_DIRS")); v != "" { raw = append(raw, strings.Split(v, modelDirsListSep())...) } - return dedupDirs(raw, JeanHome()) + return dedupDirs(raw, AjeanHome()) } // dedupDirs nettoie une liste de chemins : vides retirés, chemins absolus @@ -82,17 +82,17 @@ func normDir(p string) string { // modelDirs renvoie tous les dossiers où chercher un .gguf : JEAN_HOME d'abord // (il reste la destination des téléchargements), puis les dossiers ajoutés. func modelDirs() []string { - return append([]string{JeanHome()}, extraModelDirs()...) + return append([]string{AjeanHome()}, extraModelDirs()...) } // saveExtraModelDirs écrit la liste des dossiers supplémentaires sur disque. func saveExtraModelDirs(dirs []string) error { - clean := dedupDirs(dirs, JeanHome()) + clean := dedupDirs(dirs, AjeanHome()) b, err := json.MarshalIndent(clean, "", " ") if err != nil { return err } - if err := os.MkdirAll(JeanHome(), 0o755); err != nil { + if err := os.MkdirAll(AjeanHome(), 0o755); err != nil { return err } return os.WriteFile(modelDirsPath(), append(b, '\n'), 0o644) @@ -147,7 +147,7 @@ func resolveModelPath(name string) (string, error) { return p, nil } } - return filepath.Join(JeanHome(), base), nil // introuvable : l'appelant décidera + return filepath.Join(AjeanHome(), base), nil // introuvable : l'appelant décidera } // modelDirCount compte les .gguf lisibles d'un dossier (-1 si illisible). @@ -211,7 +211,7 @@ func handleModelDirs(w http.ResponseWriter, r *http.Request) { sendJSON(w, 200, map[string]any{"ok": true}) return } - out := []map[string]any{{"path": JeanHome(), "home": true, "count": modelDirCount(JeanHome()), "exists": true}} + out := []map[string]any{{"path": AjeanHome(), "home": true, "count": modelDirCount(AjeanHome()), "exists": true}} for _, d := range extraModelDirs() { n := modelDirCount(d) out = append(out, map[string]any{"path": d, "home": false, "count": n, "exists": n >= 0}) diff --git a/internal/jean/backend_model_dirs_test.go b/internal/ajean/backend_model_dirs_test.go similarity index 99% rename from internal/jean/backend_model_dirs_test.go rename to internal/ajean/backend_model_dirs_test.go index f2dc780..f7700a8 100644 --- a/internal/jean/backend_model_dirs_test.go +++ b/internal/ajean/backend_model_dirs_test.go @@ -1,4 +1,4 @@ -package jean +package ajean import ( "os" diff --git a/internal/jean/backend_models.go b/internal/ajean/backend_models.go similarity index 99% rename from internal/jean/backend_models.go rename to internal/ajean/backend_models.go index 379b34d..4d27b18 100644 --- a/internal/jean/backend_models.go +++ b/internal/ajean/backend_models.go @@ -1,4 +1,4 @@ -package jean +package ajean import ( "context" @@ -97,7 +97,7 @@ func downloadDestPath(name string) (string, error) { if !strings.HasSuffix(strings.ToLower(base), ".gguf") { return "", fmt.Errorf("seuls les fichiers .gguf sont acceptés") } - return filepath.Join(JeanHome(), base), nil + return filepath.Join(AjeanHome(), base), nil } // modelFromPresetContent extracts the MODEL= value from a preset's config.env @@ -600,7 +600,7 @@ func handleModelDownloadCancel(w http.ResponseWriter, r *http.Request) { // (its state lived in memory), so the partial file would otherwise sit there // forever eating disk. func cleanStalePartFiles() { - matches, err := filepath.Glob(filepath.Join(JeanHome(), "*.gguf.part")) + matches, err := filepath.Glob(filepath.Join(AjeanHome(), "*.gguf.part")) if err != nil { return } diff --git a/internal/jean/backend_models_dl_test.go b/internal/ajean/backend_models_dl_test.go similarity index 99% rename from internal/jean/backend_models_dl_test.go rename to internal/ajean/backend_models_dl_test.go index b295072..ec8a09f 100644 --- a/internal/jean/backend_models_dl_test.go +++ b/internal/ajean/backend_models_dl_test.go @@ -1,4 +1,4 @@ -package jean +package ajean import ( "bytes" diff --git a/internal/jean/backend_prebuilt.go b/internal/ajean/backend_prebuilt.go similarity index 99% rename from internal/jean/backend_prebuilt.go rename to internal/ajean/backend_prebuilt.go index 97aefe1..13cea55 100644 --- a/internal/jean/backend_prebuilt.go +++ b/internal/ajean/backend_prebuilt.go @@ -8,7 +8,7 @@ // CUDA officiel pour Linux (on retombe sur Vulkan — la compilation locale // reste la voie CUDA sous Linux). La compilation locale reste indispensable // pour les forks (ex. PrismML). -package jean +package ajean import ( "archive/tar" @@ -37,7 +37,7 @@ type ghAsset struct { } func prebuiltDir() string { - return filepath.Join(JeanHome(), "backends", "llama.cpp-prebuilt") + return filepath.Join(AjeanHome(), "backends", "llama.cpp-prebuilt") } // prebuiltVersion lit le marqueur VERSION du dossier prebuilt : "tag cudaVer" diff --git a/internal/jean/backend_prebuilt_test.go b/internal/ajean/backend_prebuilt_test.go similarity index 99% rename from internal/jean/backend_prebuilt_test.go rename to internal/ajean/backend_prebuilt_test.go index e513cd6..98719b0 100644 --- a/internal/jean/backend_prebuilt_test.go +++ b/internal/ajean/backend_prebuilt_test.go @@ -1,4 +1,4 @@ -package jean +package ajean import ( "archive/tar" diff --git a/internal/jean/backend_presets.go b/internal/ajean/backend_presets.go similarity index 99% rename from internal/jean/backend_presets.go rename to internal/ajean/backend_presets.go index 4a9794e..cddec24 100644 --- a/internal/jean/backend_presets.go +++ b/internal/ajean/backend_presets.go @@ -1,4 +1,4 @@ -package jean +package ajean import ( "bufio" diff --git a/internal/jean/backend_presets_test.go b/internal/ajean/backend_presets_test.go similarity index 98% rename from internal/jean/backend_presets_test.go rename to internal/ajean/backend_presets_test.go index d6d3dc0..1d896e9 100644 --- a/internal/jean/backend_presets_test.go +++ b/internal/ajean/backend_presets_test.go @@ -1,4 +1,4 @@ -package jean +package ajean import "testing" diff --git a/internal/jean/backend_serve.go b/internal/ajean/backend_serve.go similarity index 98% rename from internal/jean/backend_serve.go rename to internal/ajean/backend_serve.go index d706298..d6bf09c 100644 --- a/internal/jean/backend_serve.go +++ b/internal/ajean/backend_serve.go @@ -1,4 +1,4 @@ -package jean +package ajean import ( "fmt" @@ -34,7 +34,7 @@ func cmdServe(args []string) error { return fmt.Errorf("modèle introuvable : %s", model) } if !filepath.IsAbs(bin) { - bin = filepath.Join(JeanHome(), bin) + bin = filepath.Join(AjeanHome(), bin) } // Make sure llama-server can find its bundled shared libraries (the .so/.dll @@ -102,7 +102,7 @@ func cmdServe(args []string) error { // Working dir = JEAN_HOME so relative paths in EXTRA_ARGS (e.g. --mmproj // mmproj-F16.gguf) still resolve. - _ = os.Chdir(JeanHome()) + _ = os.Chdir(AjeanHome()) fmt.Fprintf(os.Stderr, "[jean serve] %s model=%s port=%s\n", bin, filepath.Base(model), get("PORT", "8080")) diff --git a/internal/jean/chat_agent.go b/internal/ajean/chat_agent.go similarity index 97% rename from internal/jean/chat_agent.go rename to internal/ajean/chat_agent.go index fdbaccd..7ed8921 100644 --- a/internal/jean/chat_agent.go +++ b/internal/ajean/chat_agent.go @@ -1,4 +1,4 @@ -package jean +package ajean import ( "fmt" @@ -27,7 +27,7 @@ func agentEnabled() bool { } func setAgentEnabled(on bool) error { - _ = os.MkdirAll(JeanHome(), 0o755) + _ = os.MkdirAll(AjeanHome(), 0o755) // On nettoie systématiquement les anciens drapeaux pour ne pas garder un // état fantôme « à moitié activé » hérité de l'ancien modèle à deux toggles. _ = os.Remove(legacyToolsFlag()) diff --git a/internal/jean/chat_cmd.go b/internal/ajean/chat_cmd.go similarity index 99% rename from internal/jean/chat_cmd.go rename to internal/ajean/chat_cmd.go index a6cb6dd..f58ab3a 100644 --- a/internal/jean/chat_cmd.go +++ b/internal/ajean/chat_cmd.go @@ -1,4 +1,4 @@ -package jean +package ajean import ( "bufio" diff --git a/internal/jean/chat_compact.go b/internal/ajean/chat_compact.go similarity index 99% rename from internal/jean/chat_compact.go rename to internal/ajean/chat_compact.go index 23be98e..b9509ee 100644 --- a/internal/jean/chat_compact.go +++ b/internal/ajean/chat_compact.go @@ -1,4 +1,4 @@ -package jean +package ajean import ( "bytes" diff --git a/internal/jean/chat_compact_test.go b/internal/ajean/chat_compact_test.go similarity index 99% rename from internal/jean/chat_compact_test.go rename to internal/ajean/chat_compact_test.go index 91f6dfa..1bf05ae 100644 --- a/internal/jean/chat_compact_test.go +++ b/internal/ajean/chat_compact_test.go @@ -1,4 +1,4 @@ -package jean +package ajean import ( "strings" diff --git a/internal/jean/chat_conversation.go b/internal/ajean/chat_conversation.go similarity index 99% rename from internal/jean/chat_conversation.go rename to internal/ajean/chat_conversation.go index 9a2420b..1656121 100644 --- a/internal/jean/chat_conversation.go +++ b/internal/ajean/chat_conversation.go @@ -1,4 +1,4 @@ -package jean +package ajean import ( "context" @@ -65,10 +65,10 @@ var conv = func() *Conversation { return c }() -// convPath = fichier de persistance sur la box jean (JeanHome = /etc/jean en +// convPath = fichier de persistance sur la box jean (AjeanHome = /etc/jean en // prod). En clair : la box déchiffre déjà pour lancer le modèle, le relais reste // aveugle — le persister ici ne change rien à la posture E2E. -func convPath() string { return filepath.Join(JeanHome(), "conversation.json") } +func convPath() string { return filepath.Join(AjeanHome(), "conversation.json") } // LoadConversation recharge l'état persisté au démarrage du process. Sans fichier // (première fois) on part d'une conversation vide. @@ -94,7 +94,7 @@ func (c *Conversation) persist() { if err != nil { return } - _ = os.MkdirAll(JeanHome(), 0o755) + _ = os.MkdirAll(AjeanHome(), 0o755) tmp := convPath() + ".tmp" if err := os.WriteFile(tmp, b, 0o600); err != nil { return diff --git a/internal/jean/chat_conversation_test.go b/internal/ajean/chat_conversation_test.go similarity index 99% rename from internal/jean/chat_conversation_test.go rename to internal/ajean/chat_conversation_test.go index 585b3b8..a46969d 100644 --- a/internal/jean/chat_conversation_test.go +++ b/internal/ajean/chat_conversation_test.go @@ -1,4 +1,4 @@ -package jean +package ajean import ( "context" diff --git a/internal/jean/chat_diff.go b/internal/ajean/chat_diff.go similarity index 99% rename from internal/jean/chat_diff.go rename to internal/ajean/chat_diff.go index 11e852c..bd355a0 100644 --- a/internal/jean/chat_diff.go +++ b/internal/ajean/chat_diff.go @@ -1,4 +1,4 @@ -package jean +package ajean // chat_diff.go — petit diff ligne à ligne pour l'interface : quand l'IA modifie // un fichier (outil `edit`) ou une page de mémoire (`mem_edit`, `mem_add`), on diff --git a/internal/jean/chat_internet.go b/internal/ajean/chat_internet.go similarity index 99% rename from internal/jean/chat_internet.go rename to internal/ajean/chat_internet.go index 3547cb9..0d9c947 100644 --- a/internal/jean/chat_internet.go +++ b/internal/ajean/chat_internet.go @@ -1,4 +1,4 @@ -package jean +package ajean import ( "bytes" @@ -74,7 +74,7 @@ func writeCrawlKey(key string) error { } return nil } - _ = os.MkdirAll(JeanHome(), 0o755) + _ = os.MkdirAll(AjeanHome(), 0o755) return os.WriteFile(crawlKeyPath(), []byte(key+"\n"), 0o600) } @@ -97,7 +97,7 @@ func internetEnabled() bool { } func setInternetEnabled(on bool) error { - _ = os.MkdirAll(JeanHome(), 0o755) + _ = os.MkdirAll(AjeanHome(), 0o755) if on { f, err := os.Create(internetFlag()) if err != nil { diff --git a/internal/jean/chat_internet_tools.go b/internal/ajean/chat_internet_tools.go similarity index 99% rename from internal/jean/chat_internet_tools.go rename to internal/ajean/chat_internet_tools.go index 9a6c632..a0f6378 100644 --- a/internal/jean/chat_internet_tools.go +++ b/internal/ajean/chat_internet_tools.go @@ -1,6 +1,6 @@ // chat_internet_tools.go — les 4 outils web exposés au modèle (web_search, // web_open, web_read, web_grep) + la sous-commande `jean internet`. -package jean +package ajean import ( "fmt" diff --git a/internal/jean/chat_memory.go b/internal/ajean/chat_memory.go similarity index 99% rename from internal/jean/chat_memory.go rename to internal/ajean/chat_memory.go index c85824e..03be346 100644 --- a/internal/jean/chat_memory.go +++ b/internal/ajean/chat_memory.go @@ -1,4 +1,4 @@ -package jean +package ajean import ( "errors" diff --git a/internal/jean/chat_tools.go b/internal/ajean/chat_tools.go similarity index 99% rename from internal/jean/chat_tools.go rename to internal/ajean/chat_tools.go index a3b6d24..4cb81a5 100644 --- a/internal/jean/chat_tools.go +++ b/internal/ajean/chat_tools.go @@ -1,4 +1,4 @@ -package jean +package ajean import ( "context" diff --git a/internal/jean/chat_tools_write_test.go b/internal/ajean/chat_tools_write_test.go similarity index 98% rename from internal/jean/chat_tools_write_test.go rename to internal/ajean/chat_tools_write_test.go index cab2195..ab07b4d 100644 --- a/internal/jean/chat_tools_write_test.go +++ b/internal/ajean/chat_tools_write_test.go @@ -1,4 +1,4 @@ -package jean +package ajean import ( "os" diff --git a/internal/jean/chat_workspace.go b/internal/ajean/chat_workspace.go similarity index 94% rename from internal/jean/chat_workspace.go rename to internal/ajean/chat_workspace.go index c845175..3b5f652 100644 --- a/internal/jean/chat_workspace.go +++ b/internal/ajean/chat_workspace.go @@ -1,4 +1,4 @@ -package jean +package ajean import ( "os" @@ -26,7 +26,7 @@ var ( ) // agentWorkspace renvoie le dossier de travail de l'agent, créé au besoin. Il -// essaie plusieurs emplacements car JeanHome() vaut %ProgramData%\jean sous +// essaie plusieurs emplacements car AjeanHome() vaut %ProgramData%\jean sous // Windows, qui n'est pas inscriptible par un utilisateur non administrateur. func agentWorkspace() string { workspaceOnce.Do(func() { @@ -60,7 +60,7 @@ func workspaceCandidates() []string { if v := strings.TrimSpace(os.Getenv(workspaceEnv)); v != "" { c = append(c, v) } - c = append(c, filepath.Join(JeanHome(), "workspace")) + c = append(c, filepath.Join(AjeanHome(), "workspace")) if home, err := os.UserHomeDir(); err == nil && home != "" { c = append(c, filepath.Join(home, "jean", "workspace")) } diff --git a/internal/jean/chat_workspace_test.go b/internal/ajean/chat_workspace_test.go similarity index 98% rename from internal/jean/chat_workspace_test.go rename to internal/ajean/chat_workspace_test.go index 9ce7481..7ee8955 100644 --- a/internal/jean/chat_workspace_test.go +++ b/internal/ajean/chat_workspace_test.go @@ -1,4 +1,4 @@ -package jean +package ajean import ( "path/filepath" diff --git a/internal/jean/cli_app.go b/internal/ajean/cli_app.go similarity index 99% rename from internal/jean/cli_app.go rename to internal/ajean/cli_app.go index 7900a7a..32e1314 100644 --- a/internal/jean/cli_app.go +++ b/internal/ajean/cli_app.go @@ -1,4 +1,4 @@ -package jean +package ajean // cli_app.go — expérience « application » de Jean. // diff --git a/internal/jean/doc.go b/internal/ajean/doc.go similarity index 98% rename from internal/jean/doc.go rename to internal/ajean/doc.go index 87090ec..2ca1c5b 100644 --- a/internal/jean/doc.go +++ b/internal/ajean/doc.go @@ -15,4 +15,4 @@ // // Les suffixes _windows/_linux/_darwin/_unix/_other portent les contraintes de // compilation par OS. ui/index.html est GÉNÉRÉ depuis ui/src/ (voir ui/assemble.ps1). -package jean +package ajean diff --git a/internal/jean/llm_bench.go b/internal/ajean/llm_bench.go similarity index 98% rename from internal/jean/llm_bench.go rename to internal/ajean/llm_bench.go index dfe647e..ad9365c 100644 --- a/internal/jean/llm_bench.go +++ b/internal/ajean/llm_bench.go @@ -1,4 +1,4 @@ -package jean +package ajean import ( "bytes" @@ -138,7 +138,7 @@ func runBench(nPrompt, nPredict int) (*benchResult, error) { // lastBenchPath stores the most recent benchmark result so the web UI can show // it without re-running. Lives in JEAN_HOME so it survives restarts. -func lastBenchPath() string { return filepath.Join(JeanHome(), ".last_bench.json") } +func lastBenchPath() string { return filepath.Join(AjeanHome(), ".last_bench.json") } // savedBench is a benchResult plus the model it was run against and a timestamp. type savedBench struct { @@ -170,7 +170,7 @@ func loadLastBench() *savedBench { // benchStorePath holds per-preset benchmark results so the UI can show each // preset's measured performance. Keyed by preset name. -func benchStorePath() string { return filepath.Join(JeanHome(), ".bench_presets.json") } +func benchStorePath() string { return filepath.Join(AjeanHome(), ".bench_presets.json") } // loadBenchStore returns the per-preset bench map (empty if none/unreadable). func loadBenchStore() map[string]savedBench { diff --git a/internal/jean/llm_client.go b/internal/ajean/llm_client.go similarity index 99% rename from internal/jean/llm_client.go rename to internal/ajean/llm_client.go index c4880cb..ad800a9 100644 --- a/internal/jean/llm_client.go +++ b/internal/ajean/llm_client.go @@ -1,4 +1,4 @@ -package jean +package ajean import ( "bufio" diff --git a/internal/jean/llm_oai.go b/internal/ajean/llm_oai.go similarity index 98% rename from internal/jean/llm_oai.go rename to internal/ajean/llm_oai.go index bc57a7c..ff31642 100644 --- a/internal/jean/llm_oai.go +++ b/internal/ajean/llm_oai.go @@ -1,4 +1,4 @@ -package jean +package ajean import ( "bufio" @@ -74,7 +74,7 @@ func runOAIFront(rawLn net.Listener, tlsCfg *tls.Config) error { // oaiPublicPath est le drapeau qui active l'accès OpenAI public (piloté par l'UI, // lu en direct → activable/coupable sans redémarrer le service de lien). -func oaiPublicPath() string { return filepath.Join(JeanHome(), ".oai_public") } +func oaiPublicPath() string { return filepath.Join(AjeanHome(), ".oai_public") } // oaiPublicEnabled indique si l'accès OpenAI public est activé pour cette machine. func oaiPublicEnabled() bool { @@ -101,7 +101,7 @@ func setOAIPublic(on bool) error { // $JEAN_HOME/certs et ne quitte jamais la machine. Toujours construite ; c'est le // démux (oaiPublicEnabled, lu en direct) qui décide de router ou non le trafic. func oaiTLSConfig() *tls.Config { - certmagic.Default.Storage = &certmagic.FileStorage{Path: filepath.Join(JeanHome(), "certs")} + certmagic.Default.Storage = &certmagic.FileStorage{Path: filepath.Join(AjeanHome(), "certs")} certmagic.DefaultACME.Agreed = true certmagic.DefaultACME.Email = strings.TrimSpace(os.Getenv("JEAN_ACME_EMAIL")) certmagic.DefaultACME.DisableHTTPChallenge = true // pas de :80 accessible (CGNAT) → TLS-ALPN uniquement diff --git a/internal/jean/llm_testcmd.go b/internal/ajean/llm_testcmd.go similarity index 99% rename from internal/jean/llm_testcmd.go rename to internal/ajean/llm_testcmd.go index 17eb092..5a4dae2 100644 --- a/internal/jean/llm_testcmd.go +++ b/internal/ajean/llm_testcmd.go @@ -1,4 +1,4 @@ -package jean +package ajean import ( "context" diff --git a/internal/jean/mcp_client.go b/internal/ajean/mcp_client.go similarity index 99% rename from internal/jean/mcp_client.go rename to internal/ajean/mcp_client.go index c1e271d..a82d63f 100644 --- a/internal/jean/mcp_client.go +++ b/internal/ajean/mcp_client.go @@ -1,4 +1,4 @@ -package jean +package ajean import ( "context" diff --git a/internal/jean/mcp_client_test.go b/internal/ajean/mcp_client_test.go similarity index 99% rename from internal/jean/mcp_client_test.go rename to internal/ajean/mcp_client_test.go index 152c968..10bd944 100644 --- a/internal/jean/mcp_client_test.go +++ b/internal/ajean/mcp_client_test.go @@ -1,4 +1,4 @@ -package jean +package ajean import ( "os/exec" diff --git a/internal/jean/mcp_config.go b/internal/ajean/mcp_config.go similarity index 99% rename from internal/jean/mcp_config.go rename to internal/ajean/mcp_config.go index 0dca871..308d3ce 100644 --- a/internal/jean/mcp_config.go +++ b/internal/ajean/mcp_config.go @@ -1,4 +1,4 @@ -package jean +package ajean import ( "encoding/json" @@ -149,7 +149,7 @@ func loadMCPConfigLocked() (map[string]MCPServerConfig, error) { // saveMCPConfigLocked écrit atomiquement mcp.json (write temp + rename). func saveMCPConfigLocked(servers map[string]MCPServerConfig) error { - _ = os.MkdirAll(JeanHome(), 0o755) + _ = os.MkdirAll(AjeanHome(), 0o755) f := mcpConfigFile{MCPServers: servers} b, err := json.MarshalIndent(f, "", " ") if err != nil { diff --git a/internal/jean/relay_e2e.go b/internal/ajean/relay_e2e.go similarity index 98% rename from internal/jean/relay_e2e.go rename to internal/ajean/relay_e2e.go index 8e79017..9e52578 100644 --- a/internal/jean/relay_e2e.go +++ b/internal/ajean/relay_e2e.go @@ -1,4 +1,4 @@ -package jean +package ajean // relay_e2e.go — chiffrement bout-en-bout du chat entre le NAVIGATEUR et CET agent, de // sorte que le relais (ajean.link) ne voie que de l'opaque : « boîte noire ». @@ -32,7 +32,7 @@ import ( "sync" ) -func e2eKeyPath() string { return filepath.Join(JeanHome(), ".e2e_key") } +func e2eKeyPath() string { return filepath.Join(AjeanHome(), ".e2e_key") } var ( e2eOnce sync.Once @@ -55,7 +55,7 @@ func e2ePrivateKey() (*ecdh.PrivateKey, error) { e2eErr = err return } - _ = os.MkdirAll(JeanHome(), 0o755) + _ = os.MkdirAll(AjeanHome(), 0o755) if err := os.WriteFile(e2eKeyPath(), []byte(hex.EncodeToString(k.Bytes())+"\n"), 0o600); err != nil { e2eErr = err return diff --git a/internal/jean/relay_e2eauth.go b/internal/ajean/relay_e2eauth.go similarity index 96% rename from internal/jean/relay_e2eauth.go rename to internal/ajean/relay_e2eauth.go index b55b341..41d007a 100644 --- a/internal/jean/relay_e2eauth.go +++ b/internal/ajean/relay_e2eauth.go @@ -1,4 +1,4 @@ -package jean +package ajean // relay_e2eauth.go — authentification MUTUELLE + anti-rejeu du canal chiffré. // @@ -44,7 +44,7 @@ var ( authSet = map[string]bool{} ) -func authUsersPath() string { return filepath.Join(JeanHome(), ".authorized_users") } +func authUsersPath() string { return filepath.Join(AjeanHome(), ".authorized_users") } func loadAuthUsers() { authOnce.Do(func() { @@ -88,7 +88,7 @@ func authorizeUser(uPubHex string) error { return nil } authSet[uPubHex] = true - _ = os.MkdirAll(JeanHome(), 0o755) + _ = os.MkdirAll(AjeanHome(), 0o755) f, err := os.OpenFile(authUsersPath(), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600) if err != nil { return err @@ -102,7 +102,7 @@ func authorizeUser(uPubHex string) error { // // Générés par « jean link code » (ou affichés par « jean link »), ils sont // partagés avec le worker « jean link --foreground » (systemd, autre process) via -// un fichier JeanHome/.pair_codes — sinon les deux process ne s'accorderaient pas. +// un fichier AjeanHome/.pair_codes — sinon les deux process ne s'accorderaient pas. // Stockés HACHÉS (SHA-256) : le fichier ne révèle aucun code. Chaque code expire // au bout de 10 min et est consommé (retiré) au premier appairage réussi. @@ -113,7 +113,7 @@ type pairEntry struct { Exp int64 `json:"e"` // expiration (unix ms) } -func pairCodesPath() string { return filepath.Join(JeanHome(), ".pair_codes") } +func pairCodesPath() string { return filepath.Join(AjeanHome(), ".pair_codes") } func hashPairCode(code string) string { h := sha256.Sum256([]byte(strings.ToUpper(strings.TrimSpace(code)))) @@ -141,7 +141,7 @@ func loadPairEntries() []pairEntry { } func savePairEntries(es []pairEntry) error { - if err := os.MkdirAll(JeanHome(), 0o755); err != nil { + if err := os.MkdirAll(AjeanHome(), 0o755); err != nil { return err } b, _ := json.Marshal(es) diff --git a/internal/jean/relay_e2eauth_test.go b/internal/ajean/relay_e2eauth_test.go similarity index 99% rename from internal/jean/relay_e2eauth_test.go rename to internal/ajean/relay_e2eauth_test.go index 8489860..a75accf 100644 --- a/internal/jean/relay_e2eauth_test.go +++ b/internal/ajean/relay_e2eauth_test.go @@ -1,4 +1,4 @@ -package jean +package ajean import ( "strings" diff --git a/internal/jean/relay_link.go b/internal/ajean/relay_link.go similarity index 97% rename from internal/jean/relay_link.go rename to internal/ajean/relay_link.go index a62ab6e..2b02f0e 100644 --- a/internal/jean/relay_link.go +++ b/internal/ajean/relay_link.go @@ -1,4 +1,4 @@ -package jean +package ajean // relay_link.go — `jean link ` : connecte ce serveur Jean au relais public // (ajean.link) par une connexion SORTANTE persistante, pour qu'un utilisateur @@ -42,8 +42,8 @@ import ( // $JEAN_LINK_URL (utile pour tester contre un relais local). const defaultRelayURL = "wss://ajean.link/agent" -func linkTokenPath() string { return filepath.Join(JeanHome(), ".link_token") } -func linkMachinePath() string { return filepath.Join(JeanHome(), ".link_machine") } +func linkTokenPath() string { return filepath.Join(AjeanHome(), ".link_token") } +func linkMachinePath() string { return filepath.Join(AjeanHome(), ".link_machine") } // machineID returns a stable per-machine identifier, creating one on first use. // Permet au relais de regrouper les connexions d'une même machine sous le compte. @@ -56,7 +56,7 @@ func machineID() string { buf := make([]byte, 8) _, _ = rand.Read(buf) id := hex.EncodeToString(buf) - _ = os.MkdirAll(JeanHome(), 0o755) + _ = os.MkdirAll(AjeanHome(), 0o755) _ = os.WriteFile(linkMachinePath(), []byte(id+"\n"), 0o600) return id } @@ -71,7 +71,7 @@ func readLinkToken() string { } func saveLinkToken(tok string) error { - if err := os.MkdirAll(JeanHome(), 0o755); err != nil { + if err := os.MkdirAll(AjeanHome(), 0o755); err != nil { return err } return os.WriteFile(linkTokenPath(), []byte(tok+"\n"), 0o600) @@ -140,7 +140,7 @@ func cmdLink(args []string) error { case "code": code, err := newPairCode() if err != nil { - return fmt.Errorf("génération du code (droits sur %s ?): %w", JeanHome(), err) + return fmt.Errorf("génération du code (droits sur %s ?): %w", AjeanHome(), err) } fmt.Printf("%s code d'appairage (valable 10 min, à usage unique) :\n %s\n", green("[link]"), bold(code)) return nil @@ -209,7 +209,7 @@ func linkPrintIdentity() error { } code, err := newPairCode() if err != nil { - fmt.Printf("%s code d'appairage indisponible (droits sur %s ?): %v\n", yellow("[link]"), JeanHome(), err) + fmt.Printf("%s code d'appairage indisponible (droits sur %s ?): %v\n", yellow("[link]"), AjeanHome(), err) fmt.Printf(" Réessaie : sudo jean link code\n") return nil } @@ -375,8 +375,8 @@ func serveLocalWebMux(mux *http.ServeMux) { // linkPIDPath / linkLogPath : suivi du worker de lien hors systemd (macOS, // Windows), où Jean est une app de bureau lancée sans droits root. -func linkPIDPath() string { return filepath.Join(JeanHome(), linkServiceName+".pid") } -func linkLogPath() string { return filepath.Join(JeanHome(), linkServiceName+".log") } +func linkPIDPath() string { return filepath.Join(AjeanHome(), linkServiceName+".pid") } +func linkLogPath() string { return filepath.Join(AjeanHome(), linkServiceName+".log") } // linkServiceCtl pilote le worker de lien (start/stop/restart). Sous Linux c'est // l'unité systemd jean-link (avec sudo non interactif si on n'est pas root) ; @@ -486,7 +486,7 @@ func linkUserSvcCtl(action string) error { if p, err := filepath.EvalSymlinks(self); err == nil { self = p } - if err := os.MkdirAll(JeanHome(), 0o755); err != nil { + if err := os.MkdirAll(AjeanHome(), 0o755); err != nil { return err } logf, err := os.OpenFile(linkLogPath(), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644) diff --git a/internal/jean/run.go b/internal/ajean/run.go similarity index 79% rename from internal/jean/run.go rename to internal/ajean/run.go index a00ec6b..b526c94 100644 --- a/internal/jean/run.go +++ b/internal/ajean/run.go @@ -1,7 +1,7 @@ // jean — single-binary LLM server manager + web UI for llama.cpp deployments. // Point d'entrée réel : Main(), appelé par cmd/jean (les métadonnées Windows // .syso/go:generate vivent là-bas, dans le dossier du package main). -package jean +package ajean import ( "fmt" @@ -175,55 +175,65 @@ func mustExit(err error) { } } -// JeanHome resolves the JEAN data directory. -// Precedence: $JEAN_HOME → /etc/default/jean (unix only) → defaultJeanHome(). -// defaultJeanHome() is platform-specific (see sys_platform_unix.go / sys_platform_windows.go). -func JeanHome() string { +// AjeanHome resolves the AJEAN data directory. +// Precedence: $AJEAN_HOME → $JEAN_HOME (héritage) → /etc/default/ajean puis +// /etc/default/jean (unix only) → migratedDefaultHome(). +// +// Les deux noms de variable sont acceptés indéfiniment : ils sont posés À LA MAIN +// dans les config des utilisateurs et dans des scripts qu'on ne voit pas. Une +// variable explicite est un ordre — on ne migre RIEN quand il y en a une, on obéit. +func AjeanHome() string { + if h := os.Getenv("AJEAN_HOME"); h != "" { + return h + } if h := os.Getenv("JEAN_HOME"); h != "" { return h } if h := readEtcDefault(); h != "" { return h } - return defaultJeanHome() + return migratedDefaultHome() } -// readEtcDefault parses /etc/default/jean for JEAN_HOME=. Quiet on errors. +// readEtcDefault parses /etc/default/ajean (puis /etc/default/jean, hérité) for +// AJEAN_HOME= ou JEAN_HOME=. Quiet on errors. func readEtcDefault() string { - b, err := os.ReadFile("/etc/default/jean") - if err != nil { - return "" - } - for _, line := range strings.Split(string(b), "\n") { - s := strings.TrimSpace(line) - if s == "" || strings.HasPrefix(s, "#") { + for _, path := range []string{"/etc/default/ajean", "/etc/default/jean"} { + b, err := os.ReadFile(path) + if err != nil { continue } - s = strings.TrimPrefix(s, "export ") - if eq := strings.IndexByte(s, '='); eq > 0 { - k := strings.TrimSpace(s[:eq]) - v := strings.Trim(strings.TrimSpace(s[eq+1:]), "\"'") - if k == "JEAN_HOME" { - return v + for _, line := range strings.Split(string(b), "\n") { + s := strings.TrimSpace(line) + if s == "" || strings.HasPrefix(s, "#") { + continue + } + s = strings.TrimPrefix(s, "export ") + if eq := strings.IndexByte(s, '='); eq > 0 { + k := strings.TrimSpace(s[:eq]) + v := strings.Trim(strings.TrimSpace(s[eq+1:]), "\"'") + if k == "AJEAN_HOME" || k == "JEAN_HOME" { + return v + } } } } return "" } -func confPath() string { return filepath.Join(JeanHome(), "config.env") } -func presetsDir() string { return filepath.Join(JeanHome(), "configs") } -func skillsDir() string { return filepath.Join(JeanHome(), "SKILLS") } -func memoryDir() string { return filepath.Join(JeanHome(), "MEMORY") } -func agentFlag() string { return filepath.Join(JeanHome(), ".agent_enabled") } -func internetFlag() string { return filepath.Join(JeanHome(), ".internet_enabled") } -func mcpConfigPath() string { return filepath.Join(JeanHome(), "mcp.json") } +func confPath() string { return filepath.Join(AjeanHome(), "config.env") } +func presetsDir() string { return filepath.Join(AjeanHome(), "configs") } +func skillsDir() string { return filepath.Join(AjeanHome(), "SKILLS") } +func memoryDir() string { return filepath.Join(AjeanHome(), "MEMORY") } +func agentFlag() string { return filepath.Join(AjeanHome(), ".agent_enabled") } +func internetFlag() string { return filepath.Join(AjeanHome(), ".internet_enabled") } +func mcpConfigPath() string { return filepath.Join(AjeanHome(), "mcp.json") } // Anciens drapeaux séparés, conservés pour la migration vers le mode agent unifié. func legacySkillsFlag() string { return filepath.Join(skillsDir(), ".enabled") } -func legacyToolsFlag() string { return filepath.Join(JeanHome(), ".tools_enabled") } -func apiKeyPath() string { return filepath.Join(JeanHome(), ".api_key") } -func crawlKeyPath() string { return filepath.Join(JeanHome(), ".crawl4ai_key") } +func legacyToolsFlag() string { return filepath.Join(AjeanHome(), ".tools_enabled") } +func apiKeyPath() string { return filepath.Join(AjeanHome(), ".api_key") } +func crawlKeyPath() string { return filepath.Join(AjeanHome(), ".crawl4ai_key") } func serviceName() string { if n := os.Getenv("JEAN_SERVICE"); n != "" { return n diff --git a/internal/jean/sys_brand_icon.go b/internal/ajean/sys_brand_icon.go similarity index 99% rename from internal/jean/sys_brand_icon.go rename to internal/ajean/sys_brand_icon.go index 134de41..04b70bb 100644 --- a/internal/jean/sys_brand_icon.go +++ b/internal/ajean/sys_brand_icon.go @@ -1,4 +1,4 @@ -package jean +package ajean // sys_brand_icon.go — l'icône de la marque AJEAN, rendue à la volée. // diff --git a/internal/jean/sys_console_darwin.go b/internal/ajean/sys_console_darwin.go similarity index 99% rename from internal/jean/sys_console_darwin.go rename to internal/ajean/sys_console_darwin.go index 40b4409..2d02240 100644 --- a/internal/jean/sys_console_darwin.go +++ b/internal/ajean/sys_console_darwin.go @@ -1,6 +1,6 @@ //go:build darwin -package jean +package ajean // sys_console_darwin.go — détection du lancement « par clic » sous macOS. // diff --git a/internal/jean/sys_console_other.go b/internal/ajean/sys_console_other.go similarity index 97% rename from internal/jean/sys_console_other.go rename to internal/ajean/sys_console_other.go index 1dee73a..63f7a35 100644 --- a/internal/jean/sys_console_other.go +++ b/internal/ajean/sys_console_other.go @@ -1,6 +1,6 @@ //go:build !windows && !darwin -package jean +package ajean // setupConsole : sous Linux, le process a toujours une vraie console/tty // standard (stdout hérité). Rien à faire → on signale simplement « on a une diff --git a/internal/jean/sys_console_windows.go b/internal/ajean/sys_console_windows.go similarity index 99% rename from internal/jean/sys_console_windows.go rename to internal/ajean/sys_console_windows.go index d8e8b56..821c4e6 100644 --- a/internal/jean/sys_console_windows.go +++ b/internal/ajean/sys_console_windows.go @@ -1,6 +1,6 @@ //go:build windows -package jean +package ajean // sys_console_windows.go — gestion de la console sous Windows. // diff --git a/internal/jean/sys_firstrun_other.go b/internal/ajean/sys_firstrun_other.go similarity index 97% rename from internal/jean/sys_firstrun_other.go rename to internal/ajean/sys_firstrun_other.go index 5ca594a..b6eec3c 100644 --- a/internal/jean/sys_firstrun_other.go +++ b/internal/ajean/sys_firstrun_other.go @@ -1,6 +1,6 @@ //go:build !windows -package jean +package ajean import "os" diff --git a/internal/jean/sys_firstrun_windows.go b/internal/ajean/sys_firstrun_windows.go similarity index 99% rename from internal/jean/sys_firstrun_windows.go rename to internal/ajean/sys_firstrun_windows.go index 6d42128..f9ad683 100644 --- a/internal/jean/sys_firstrun_windows.go +++ b/internal/ajean/sys_firstrun_windows.go @@ -1,6 +1,6 @@ //go:build windows -package jean +package ajean import ( "fmt" @@ -113,7 +113,7 @@ func appFirstRun() bool { "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"+ + "• vos réglages et modèles seront rangés dans :\n "+AjeanHome()+"\n\n"+ "Vous pourrez ensuite supprimer le fichier que vous venez de télécharger.\n\n"+ "Répondre Non démarre AJEAN sans rien installer. La question reviendra\n"+ "au prochain lancement de ce fichier.", @@ -376,7 +376,7 @@ type vsFixedFileInfo struct { // 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() + jeanHome := AjeanHome() for _, d := range []string{jeanHome, filepath.Join(jeanHome, "configs"), filepath.Join(jeanHome, "SKILLS")} { if err := os.MkdirAll(d, 0o755); err != nil { return err diff --git a/internal/jean/sys_firstrun_windows_test.go b/internal/ajean/sys_firstrun_windows_test.go similarity index 99% rename from internal/jean/sys_firstrun_windows_test.go rename to internal/ajean/sys_firstrun_windows_test.go index 43449ec..51a651a 100644 --- a/internal/jean/sys_firstrun_windows_test.go +++ b/internal/ajean/sys_firstrun_windows_test.go @@ -1,6 +1,6 @@ //go:build windows -package jean +package ajean import ( "os" diff --git a/internal/jean/sys_install_darwin.go b/internal/ajean/sys_install_darwin.go similarity index 99% rename from internal/jean/sys_install_darwin.go rename to internal/ajean/sys_install_darwin.go index f74001d..56f55d8 100644 --- a/internal/jean/sys_install_darwin.go +++ b/internal/ajean/sys_install_darwin.go @@ -1,6 +1,6 @@ //go:build darwin -package jean +package ajean import ( "fmt" diff --git a/internal/jean/sys_install_linux.go b/internal/ajean/sys_install_linux.go similarity index 99% rename from internal/jean/sys_install_linux.go rename to internal/ajean/sys_install_linux.go index 154bdce..d53fedc 100644 --- a/internal/jean/sys_install_linux.go +++ b/internal/ajean/sys_install_linux.go @@ -1,6 +1,6 @@ //go:build linux -package jean +package ajean import ( "fmt" diff --git a/internal/jean/sys_install_windows.go b/internal/ajean/sys_install_windows.go similarity index 98% rename from internal/jean/sys_install_windows.go rename to internal/ajean/sys_install_windows.go index ccce2b6..9aae2dc 100644 --- a/internal/jean/sys_install_windows.go +++ b/internal/ajean/sys_install_windows.go @@ -1,6 +1,6 @@ //go:build windows -package jean +package ajean import ( "fmt" @@ -38,7 +38,7 @@ EXTRA_ARGS="" ` func cmdInstall(args []string) error { - jeanHome := JeanHome() + jeanHome := AjeanHome() fmt.Printf("Installation (Windows)\n") fmt.Printf(" JEAN_HOME = %s\n", jeanHome) @@ -203,7 +203,7 @@ func cmdUninstall(args []string) error { _ = svcStop(false) // Pull JEAN_HOME\bin off the user PATH (best-effort). - binDir := filepath.Join(JeanHome(), "bin") + binDir := filepath.Join(AjeanHome(), "bin") if removed, err := removeFromUserPath(binDir); err == nil && removed { fmt.Printf(" %s %s retiré du PATH utilisateur\n", green("✓"), binDir) } @@ -214,7 +214,7 @@ func cmdUninstall(args []string) error { } if !keepData { - jeanHome := JeanHome() + jeanHome := AjeanHome() if err := os.RemoveAll(jeanHome); err != nil { return fmt.Errorf("suppression de %s: %w", jeanHome, err) } diff --git a/internal/ajean/sys_migrate.go b/internal/ajean/sys_migrate.go new file mode 100644 index 0000000..9aa5141 --- /dev/null +++ b/internal/ajean/sys_migrate.go @@ -0,0 +1,80 @@ +package ajean + +import ( + "fmt" + "os" + "path/filepath" + "sync" +) + +// Migration du dossier de données jean → ajean. +// +// C'est le seul endroit du renommage qui peut faire PERDRE des données : le +// dossier contient la config, les clés, la mémoire, les conversations, et +// surtout des .gguf qui pèsent des dizaines de gigaoctets. Trois règles en +// découlent, et elles expliquent tout le code ci-dessous : +// +// 1. On déplace par os.Rename DANS LE DOSSIER PARENT de l'ancien. Un rename +// intra-volume est atomique et instantané — il ne recopie pas les 40 Go, et +// il n'existe aucun instant où les données seraient à moitié quelque part. +// Choisir le parent de l'ancien chemin (et pas le défaut de la plateforme) +// garantit qu'on reste sur le même volume, donc que le rename est bien un +// rename et non un copy+delete déguisé. +// +// 2. Si le rename échoue — service en cours qui tient des handles sous Windows, +// droits insuffisants, volume monté en lecture seule — on CONTINUE SUR +// L'ANCIEN CHEMIN. Une machine qui n'a pas migré marche exactement comme +// avant ; une machine qui a perdu ses modèles, non. La migration est donc +// retentée à chaque démarrage jusqu'à ce qu'elle passe. +// +// 3. Aucune suppression, jamais. Le pire cas est « rien n'a bougé ». + +var ( + homeOnce sync.Once + homePath string +) + +// migratedDefaultHome renvoie le dossier de données par défaut, en migrant +// l'ancien dossier « jean » vers « ajean » à la première résolution du process. +// Le résultat est mis en cache : deux appels ne doivent jamais désigner deux +// dossiers différents, sinon une moitié du programme écrirait à côté de l'autre. +// +// N'est PAS appelé quand $AJEAN_HOME/$JEAN_HOME ou /etc/default/* imposent un +// chemin : un choix explicite de l'utilisateur ne se migre pas. +func migratedDefaultHome() string { + homeOnce.Do(func() { homePath = resolveDefaultHome() }) + return homePath +} + +func resolveDefaultHome() string { + return migrateHome(defaultAjeanHome(), legacyDefaultHome()) +} + +// migrateHome contient toute la logique de migration, isolée des chemins réels +// de la plateforme pour être testable telle quelle. Renvoie le dossier à +// utiliser — le nouveau si la migration a réussi ou n'était pas nécessaire, +// l'ancien si elle a échoué. +func migrateHome(target, legacy string) string { + if isDir(target) { + return target // déjà migré (ou installation neuve déjà faite) + } + if !isDir(legacy) { + return target // installation neuve : rien à migrer + } + + // Même parent que l'ancien dossier ⇒ même volume ⇒ rename atomique. + sibling := filepath.Join(filepath.Dir(legacy), filepath.Base(target)) + if isDir(sibling) { + return sibling + } + if err := os.Rename(legacy, sibling); err != nil { + // Cas le plus courant sous Windows : un service AJEAN tourne encore et + // tient un handle dans le dossier. On reste sur l'ancien chemin — tout + // fonctionne — et on retentera au prochain démarrage. + fmt.Fprintf(os.Stderr, "[info] dossier de données pas encore migré vers %s (%v) — on continue sur %s\n", + sibling, err, legacy) + return legacy + } + fmt.Fprintf(os.Stderr, "[ok] dossier de données migré : %s → %s\n", legacy, sibling) + return sibling +} diff --git a/internal/ajean/sys_migrate_test.go b/internal/ajean/sys_migrate_test.go new file mode 100644 index 0000000..87f24f4 --- /dev/null +++ b/internal/ajean/sys_migrate_test.go @@ -0,0 +1,102 @@ +package ajean + +import ( + "os" + "path/filepath" + "runtime" + "testing" +) + +// mkHome crée un dossier de données factice contenant un fichier témoin, pour +// vérifier que la migration déplace bien le CONTENU et pas seulement le dossier. +func mkHome(t *testing.T, dir string) { + t.Helper() + if err := os.MkdirAll(dir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "config.env"), []byte("MODEL=x.gguf\n"), 0o644); err != nil { + t.Fatal(err) + } +} + +// Cas nominal : seul l'ancien dossier existe → il est migré, contenu compris. +func TestMigrateHomeMovesLegacy(t *testing.T) { + root := t.TempDir() + legacy := filepath.Join(root, "jean") + target := filepath.Join(root, "ajean") + mkHome(t, legacy) + + if got := migrateHome(target, legacy); got != target { + t.Fatalf("got %q, attendu le nouveau chemin %q", got, target) + } + if b, err := os.ReadFile(filepath.Join(target, "config.env")); err != nil || string(b) != "MODEL=x.gguf\n" { + t.Fatalf("contenu non migré: %q (%v)", b, err) + } + if isDir(legacy) { + t.Fatalf("l'ancien dossier existe encore après un rename réussi") + } +} + +// Installation neuve : rien à migrer, on renvoie le nouveau chemin sans rien créer. +func TestMigrateHomeFreshInstall(t *testing.T) { + root := t.TempDir() + target := filepath.Join(root, "ajean") + if got := migrateHome(target, filepath.Join(root, "jean")); got != target { + t.Fatalf("got %q, attendu %q", got, target) + } +} + +// Déjà migré : le nouveau dossier existe → on ne retouche à rien, même si un +// vieux dossier « jean » traîne encore (utilisateur qui l'a recréé, restauration +// de sauvegarde…). Écraser serait perdre les données courantes. +func TestMigrateHomeKeepsExistingTarget(t *testing.T) { + root := t.TempDir() + legacy := filepath.Join(root, "jean") + target := filepath.Join(root, "ajean") + mkHome(t, legacy) + mkHome(t, target) + if err := os.WriteFile(filepath.Join(target, "config.env"), []byte("MODEL=courant.gguf\n"), 0o644); err != nil { + t.Fatal(err) + } + + if got := migrateHome(target, legacy); got != target { + t.Fatalf("got %q, attendu %q", got, target) + } + b, _ := os.ReadFile(filepath.Join(target, "config.env")) + if string(b) != "MODEL=courant.gguf\n" { + t.Fatalf("le dossier courant a été écrasé par l'ancien: %q", b) + } + if !isDir(legacy) { + t.Fatalf("l'ancien dossier a été supprimé — la migration ne doit JAMAIS supprimer") + } +} + +// Rename impossible → on doit retomber sur l'ancien chemin, intact. C'est la +// garantie « au pire, rien n'a bougé », celle qui protège les .gguf. +// +// On reproduit le scénario réel : un service AJEAN tourne encore et tient un +// handle ouvert dans le dossier de données, ce qui fait échouer le rename du +// dossier sous Windows. Unix, lui, renomme sans broncher un dossier dont des +// fichiers sont ouverts — il n'y a donc rien à simuler là-bas. +func TestMigrateHomeFallsBackWhenRenameFails(t *testing.T) { + if runtime.GOOS != "windows" { + t.Skip("un rename de dossier réussit avec des fichiers ouverts hors Windows") + } + root := t.TempDir() + legacy := filepath.Join(root, "jean") + target := filepath.Join(root, "ajean") + mkHome(t, legacy) + + held, err := os.Open(filepath.Join(legacy, "config.env")) + if err != nil { + t.Fatal(err) + } + defer held.Close() + + if got := migrateHome(target, legacy); got != legacy { + t.Fatalf("got %q, attendu un repli sur l'ancien chemin %q", got, legacy) + } + if b, err := os.ReadFile(filepath.Join(legacy, "config.env")); err != nil || string(b) != "MODEL=x.gguf\n" { + t.Fatalf("l'ancien dossier a été abîmé par une migration ratée: %q (%v)", b, err) + } +} diff --git a/internal/jean/sys_paths.go b/internal/ajean/sys_paths.go similarity index 93% rename from internal/jean/sys_paths.go rename to internal/ajean/sys_paths.go index 5a05a00..a7ec1e8 100644 --- a/internal/jean/sys_paths.go +++ b/internal/ajean/sys_paths.go @@ -1,4 +1,4 @@ -package jean +package ajean import ( "fmt" @@ -15,7 +15,7 @@ import ( // modèles et les fichiers que l'agent écrit. On centralise donc la réponse ici, // et on l'expose à la fois en ligne de commande (`jean where`) et dans l'UI. -type jeanPaths struct { +type ajeanPaths struct { Home string `json:"home"` // JEAN_HOME : racine des données Config string `json:"config"` // config.env Exe string `json:"exe"` // binaire en cours d'exécution @@ -31,18 +31,18 @@ type jeanPaths struct { // faute d'équivalent, on utilise JEAN_HOME\bin ajouté au PATH utilisateur. func installedExePath() string { if runtime.GOOS == "windows" { - return filepath.Join(JeanHome(), "bin", "jean.exe") + return filepath.Join(AjeanHome(), "bin", "jean.exe") } return "/usr/local/bin/jean" } -func currentPaths() jeanPaths { +func currentPaths() ajeanPaths { exe, _ := os.Executable() if resolved, err := filepath.EvalSymlinks(exe); err == nil { exe = resolved } - return jeanPaths{ - Home: JeanHome(), + return ajeanPaths{ + Home: AjeanHome(), Config: confPath(), Exe: exe, Installed: installedExePath(), diff --git a/internal/jean/sys_platform_unix.go b/internal/ajean/sys_platform_unix.go similarity index 92% rename from internal/jean/sys_platform_unix.go rename to internal/ajean/sys_platform_unix.go index 8a1aa99..b36f497 100644 --- a/internal/jean/sys_platform_unix.go +++ b/internal/ajean/sys_platform_unix.go @@ -1,6 +1,6 @@ //go:build unix -package jean +package ajean import ( "context" @@ -16,25 +16,30 @@ import ( "time" ) -// defaultJeanHome is the data root when neither $JEAN_HOME nor /etc/default/jean -// override it. +// defaultAjeanHome is the data root when neither $AJEAN_HOME/$JEAN_HOME nor +// /etc/default/* override it. legacyDefaultHome est son équivalent d'avant le +// renommage, migré au premier lancement (voir sys_migrate.go). // -// Linux : /etc/jean (machine de prod, service systemd lancé par root). -// macOS : /etc/jean n'est PAS écrivable par une app lancée depuis le Finder -// (« mkdir /etc/jean: permission denied »), et un Mac est une machine de bureau, +// Linux : /etc/ajean (machine de prod, service systemd lancé par root). +// macOS : /etc/ajean n'est PAS écrivable par une app lancée depuis le Finder +// (« mkdir /etc/ajean: permission denied »), et un Mac est une machine de bureau, // pas un serveur — on retombe donc sur le dossier utilisateur standard, sauf si -// un `sudo jean install` a déjà créé /etc/jean à notre nom. -func defaultJeanHome() string { +// un `sudo ajean install` a déjà créé /etc/ajean à notre nom. +func defaultAjeanHome() string { return unixHome("ajean") } + +func legacyDefaultHome() string { return unixHome("jean") } + +func unixHome(name string) string { if runtime.GOOS != "darwin" || os.Geteuid() == 0 { - return "/etc/jean" + return "/etc/" + name } - if isWritableDir("/etc/jean") { - return "/etc/jean" // installé par `sudo jean install` puis chown à l'utilisateur + if isWritableDir("/etc/" + name) { + return "/etc/" + name // installé par `sudo ajean install` puis chown à l'utilisateur } if home, err := os.UserHomeDir(); err == nil { - return filepath.Join(home, "Library", "Application Support", "jean") + return filepath.Join(home, "Library", "Application Support", name) } - return "/etc/jean" + return "/etc/" + name } // isWritableDir teste qu'un dossier existe ET qu'on peut y écrire (le seul test diff --git a/internal/jean/sys_platform_windows.go b/internal/ajean/sys_platform_windows.go similarity index 95% rename from internal/jean/sys_platform_windows.go rename to internal/ajean/sys_platform_windows.go index 2e6bbb3..c7d77ec 100644 --- a/internal/jean/sys_platform_windows.go +++ b/internal/ajean/sys_platform_windows.go @@ -1,6 +1,6 @@ //go:build windows -package jean +package ajean import ( "context" @@ -40,19 +40,26 @@ func init() { } } -// defaultJeanHome is the data root when $JEAN_HOME is unset. We use -// %ProgramData%\jean (machine-wide, the closest analogue to /etc/jean), falling -// back to %LOCALAPPDATA%\jean for unprivileged setups. -func defaultJeanHome() string { +// homeRoot is the parent directory of the data root: %ProgramData% (machine-wide, +// the closest analogue to /etc), falling back to %LOCALAPPDATA% for unprivileged +// setups. Ancien et nouveau dossier partagent ce parent, ce qui garantit que la +// migration jean → ajean est un rename intra-volume (voir sys_migrate.go). +func homeRoot() string { if pd := os.Getenv("ProgramData"); pd != "" { - return filepath.Join(pd, "jean") + return pd } if la := os.Getenv("LOCALAPPDATA"); la != "" { - return filepath.Join(la, "jean") + return la } - return filepath.Join(os.TempDir(), "jean") + return os.TempDir() } +// defaultAjeanHome is the data root when no AJEAN_HOME/JEAN_HOME is set. +func defaultAjeanHome() string { return filepath.Join(homeRoot(), "ajean") } + +// legacyDefaultHome is the pre-rename data root, migrated on first run. +func legacyDefaultHome() string { return filepath.Join(homeRoot(), "jean") } + // defaultEditor is used by `jean edit` when $EDITOR is unset. func defaultEditor() string { return "notepad" } diff --git a/internal/jean/sys_service.go b/internal/ajean/sys_service.go similarity index 99% rename from internal/jean/sys_service.go rename to internal/ajean/sys_service.go index a5b6fb3..fa3d46f 100644 --- a/internal/jean/sys_service.go +++ b/internal/ajean/sys_service.go @@ -1,4 +1,4 @@ -package jean +package ajean import ( "fmt" diff --git a/internal/jean/sys_service_darwin.go b/internal/ajean/sys_service_darwin.go similarity index 96% rename from internal/jean/sys_service_darwin.go rename to internal/ajean/sys_service_darwin.go index ca9e609..4a167f5 100644 --- a/internal/jean/sys_service_darwin.go +++ b/internal/ajean/sys_service_darwin.go @@ -1,6 +1,6 @@ //go:build darwin -package jean +package ajean import ( "fmt" @@ -29,7 +29,7 @@ func launchdPlistPath(svc string) string { // launchdLogPath : sortie standard/erreur du service, sous JEAN_HOME (accessible // en écriture par l'utilisateur du service après le chown de l'installation). -func launchdLogPath() string { return filepath.Join(JeanHome(), serviceName()+".log") } +func launchdLogPath() string { return filepath.Join(AjeanHome(), serviceName()+".log") } // serviceAction mappe start/stop/restart/enable/disable sur launchctl. `load -w` // (re)active le service ET le rend persistant au boot ; `unload -w` le désactive. @@ -133,8 +133,8 @@ func serviceIsActive() bool { // la fermeture de Jean.app, et permet de tuer tout le groupe (llama-server // enfant compris) d'un seul signal. -func pidFilePath() string { return filepath.Join(JeanHome(), serviceName()+".pid") } -func logFilePath() string { return filepath.Join(JeanHome(), serviceName()+".log") } +func pidFilePath() string { return filepath.Join(AjeanHome(), serviceName()+".pid") } +func logFilePath() string { return filepath.Join(AjeanHome(), serviceName()+".log") } func userSvcAction(action string) error { switch action { @@ -173,7 +173,7 @@ func userSvcStart() error { if err != nil { return err } - if err := os.MkdirAll(JeanHome(), 0o755); err != nil { + if err := os.MkdirAll(AjeanHome(), 0o755); err != nil { return err } logf, err := os.OpenFile(logFilePath(), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644) @@ -187,7 +187,7 @@ func userSvcStart() error { // Même répertoire de travail que sous systemd/launchd : les chemins relatifs // de config.env (MODEL=…gguf) se résolvent depuis JEAN_HOME, pas depuis « / » // que nous hérite le Finder. - cmd.Dir = JeanHome() + cmd.Dir = AjeanHome() cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} if err := cmd.Start(); err != nil { return fmt.Errorf("démarrage de 'jean serve': %w", err) diff --git a/internal/jean/sys_service_linux.go b/internal/ajean/sys_service_linux.go similarity index 99% rename from internal/jean/sys_service_linux.go rename to internal/ajean/sys_service_linux.go index ede3a77..0558e22 100644 --- a/internal/jean/sys_service_linux.go +++ b/internal/ajean/sys_service_linux.go @@ -1,6 +1,6 @@ //go:build linux -package jean +package ajean import ( "fmt" diff --git a/internal/jean/sys_service_windows.go b/internal/ajean/sys_service_windows.go similarity index 95% rename from internal/jean/sys_service_windows.go rename to internal/ajean/sys_service_windows.go index ccf50a2..da9f807 100644 --- a/internal/jean/sys_service_windows.go +++ b/internal/ajean/sys_service_windows.go @@ -1,6 +1,6 @@ //go:build windows -package jean +package ajean import ( "fmt" @@ -26,8 +26,8 @@ const ( createNoWindow = 0x08000000 // CREATE_NO_WINDOW (aucune console pour l'enfant) ) -func pidFilePath() string { return filepath.Join(JeanHome(), serviceName()+".pid") } -func logFilePath() string { return filepath.Join(JeanHome(), serviceName()+".log") } +func pidFilePath() string { return filepath.Join(AjeanHome(), serviceName()+".pid") } +func logFilePath() string { return filepath.Join(AjeanHome(), serviceName()+".log") } func serviceAction(action string) error { switch action { @@ -59,7 +59,7 @@ func svcStart() error { if err != nil { return err } - if err := os.MkdirAll(JeanHome(), 0o755); err != nil { + if err := os.MkdirAll(AjeanHome(), 0o755); err != nil { return err } logf, err := os.OpenFile(logFilePath(), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644) @@ -71,7 +71,7 @@ func svcStart() error { cmd := exec.Command(self, "serve") cmd.Stdout = logf cmd.Stderr = logf - cmd.Dir = JeanHome() // les chemins relatifs de config.env se résolvent depuis JEAN_HOME + cmd.Dir = AjeanHome() // les chemins relatifs de config.env se résolvent depuis JEAN_HOME // createNoWindow + HideWindow : le service enfant (`jean serve`) ne doit JAMAIS // faire clignoter de console noire quand Jean est lancé en mode app (double-clic). cmd.SysProcAttr = &syscall.SysProcAttr{ diff --git a/internal/jean/sys_splash_other.go b/internal/ajean/sys_splash_other.go similarity index 94% rename from internal/jean/sys_splash_other.go rename to internal/ajean/sys_splash_other.go index 8dc5b26..f91f5bc 100644 --- a/internal/jean/sys_splash_other.go +++ b/internal/ajean/sys_splash_other.go @@ -1,6 +1,6 @@ //go:build !windows -package jean +package ajean // sys_splash_other.go — pas de fenêtre de démarrage hors Windows (no-op). diff --git a/internal/jean/sys_splash_windows.go b/internal/ajean/sys_splash_windows.go similarity index 99% rename from internal/jean/sys_splash_windows.go rename to internal/ajean/sys_splash_windows.go index d1ff561..4a96703 100644 --- a/internal/jean/sys_splash_windows.go +++ b/internal/ajean/sys_splash_windows.go @@ -1,6 +1,6 @@ //go:build windows -package jean +package ajean // sys_splash_windows.go — écran de démarrage « Lancement de Jean… ». // diff --git a/internal/jean/sys_tray_darwin.go b/internal/ajean/sys_tray_darwin.go similarity index 99% rename from internal/jean/sys_tray_darwin.go rename to internal/ajean/sys_tray_darwin.go index 038788f..0a2e56b 100644 --- a/internal/jean/sys_tray_darwin.go +++ b/internal/ajean/sys_tray_darwin.go @@ -1,6 +1,6 @@ //go:build darwin -package jean +package ajean // sys_tray_darwin.go — icône de Jean dans la barre de menus macOS (en haut à // droite, à côté de l'horloge), pendant exact du system tray Windows : quand on diff --git a/internal/jean/sys_tray_other.go b/internal/ajean/sys_tray_other.go similarity index 97% rename from internal/jean/sys_tray_other.go rename to internal/ajean/sys_tray_other.go index 9824225..d1992c8 100644 --- a/internal/jean/sys_tray_other.go +++ b/internal/ajean/sys_tray_other.go @@ -1,6 +1,6 @@ //go:build !windows && !darwin -package jean +package ajean // sys_tray_other.go — pas d'icône de zone de notification sous Linux (serveur // sans session graphique la plupart du temps). On se contente d'ouvrir l'UI dans diff --git a/internal/jean/sys_tray_windows.go b/internal/ajean/sys_tray_windows.go similarity index 99% rename from internal/jean/sys_tray_windows.go rename to internal/ajean/sys_tray_windows.go index 655e4e3..6fb8ed2 100644 --- a/internal/jean/sys_tray_windows.go +++ b/internal/ajean/sys_tray_windows.go @@ -1,6 +1,6 @@ //go:build windows -package jean +package ajean // sys_tray_windows.go — icône de Jean dans la zone de notification Windows (system // tray). Quand on lance l'app (double-clic ou `jean app`), l'UI web s'ouvre dans diff --git a/internal/jean/sys_tty.go b/internal/ajean/sys_tty.go similarity index 94% rename from internal/jean/sys_tty.go rename to internal/ajean/sys_tty.go index b5b24d7..d3d6329 100644 --- a/internal/jean/sys_tty.go +++ b/internal/ajean/sys_tty.go @@ -1,4 +1,4 @@ -package jean +package ajean import "os" diff --git a/internal/jean/sys_update.go b/internal/ajean/sys_update.go similarity index 84% rename from internal/jean/sys_update.go rename to internal/ajean/sys_update.go index e6abadc..f91e5c0 100644 --- a/internal/jean/sys_update.go +++ b/internal/ajean/sys_update.go @@ -1,4 +1,4 @@ -package jean +package ajean import ( "crypto/sha256" @@ -22,7 +22,10 @@ import ( // 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" +// Le dépôt a été renommé jean → ajean ; GitHub redirige les anciennes URL et +// l'API REST, donc les binaires déjà installés qui pointent encore sur +// « nathaninline/jean » continuent de trouver les releases. +const updateRepo = "nathaninline/ajean" type ghRelease struct { TagName string `json:"tag_name"` @@ -34,14 +37,39 @@ type ghRelease struct { } `json:"assets"` } -// updateAssetName reconstruit le nom d'asset attendu pour la plateforme courante, -// suivant la convention des releases : jean--[.exe]. -func updateAssetName() string { - n := "jean-" + runtime.GOOS + "-" + runtime.GOARCH +// updateAssetNames reconstruit les noms d'asset acceptables pour la plateforme +// courante, par ordre de préférence : ajean--[.exe] puis, en repli, +// l'ancien nom jean--[.exe]. +// +// Le repli n'est pas de la coquetterie : pendant la transition, les releases +// publient les DEUX noms (mêmes binaires) pour que le parc déjà installé — qui +// ne connaît que « jean-* » — continue de se mettre à jour. Ce binaire-ci sait +// lire les deux, il fonctionne donc aussi bien avec une release de transition +// qu'avec une release future qui n'aura plus que « ajean-* ». +func updateAssetNames() []string { + suffix := runtime.GOOS + "-" + runtime.GOARCH if runtime.GOOS == "windows" { - n += ".exe" + suffix += ".exe" } - return n + return []string{"ajean-" + suffix, "jean-" + suffix} +} + +// updateAssetName renvoie le nom d'asset préféré (usage : messages à l'écran). +func updateAssetName() string { return updateAssetNames()[0] } + +// pickAsset choisit dans la release le premier asset correspondant à la +// plateforme, en respectant l'ordre de préférence. Renvoie son nom, son URL et +// sa taille — le nom retourné est celui qui servira à vérifier le SHA-256, qui +// doit donc être celui réellement téléchargé, pas celui qu'on préférait. +func pickAsset(rel *ghRelease) (name, url string, size int64) { + for _, want := range updateAssetNames() { + for _, a := range rel.Assets { + if a.Name == want { + return a.Name, a.BrowserDownloadURL, a.Size + } + } + } + return "", "", 0 } // ensureV préfixe "v" si absent, pour comparer via golang.org/x/mod/semver. @@ -56,7 +84,7 @@ func ensureV(s string) string { 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") + req.Header.Set("User-Agent", "ajean-update") resp, err := (&http.Client{Timeout: 30 * time.Second}).Do(req) if err != nil { return nil, err @@ -114,17 +142,10 @@ func applyUpdate() (string, error) { return Version, fmt.Errorf("déjà à jour (%s)", Version) } - want := updateAssetName() - var url string - var size int64 - for _, a := range rel.Assets { - if a.Name == want { - url, size = a.BrowserDownloadURL, a.Size - break - } - } + want, url, size := pickAsset(rel) 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 %s dans la release %s (os/arch %s/%s)", + strings.Join(updateAssetNames(), " ni "), latest, runtime.GOOS, runtime.GOARCH) } exe, err := os.Executable() @@ -136,12 +157,12 @@ func applyUpdate() (string, error) { } // Contrôle des droits AVANT de télécharger : sans ça, l'échec survient sur le // os.Create du fichier temporaire et l'utilisateur reçoit un « open - // /usr/local/bin/.jean-update.tmp: permission denied » incompréhensible, + // /usr/local/bin/.ajean-update.tmp: permission denied » incompréhensible, // notamment depuis le bouton de l'UI web (jean web lancé sans sudo). if err := checkUpdateWritable(exe); err != nil { return "", err } - tmp := filepath.Join(filepath.Dir(exe), ".jean-update.tmp") + tmp := filepath.Join(filepath.Dir(exe), ".ajean-update.tmp") if err := downloadTo(url, tmp); err != nil { os.Remove(tmp) @@ -190,7 +211,7 @@ func updatePermissionError(exe string) error { // pas : root, groupes, ACL, montage en lecture seule…). func checkUpdateWritable(exe string) error { dir := filepath.Dir(exe) - probe, err := os.CreateTemp(dir, ".jean-update-probe-*") + probe, err := os.CreateTemp(dir, ".ajean-update-probe-*") if err != nil { if os.IsPermission(err) { return updatePermissionError(exe) @@ -237,7 +258,7 @@ func cmdUpdate(args []string) error { func downloadTo(url, dst string) error { req, _ := http.NewRequest("GET", url, nil) - req.Header.Set("User-Agent", "jean-update") + req.Header.Set("User-Agent", "ajean-update") req.Header.Set("Accept", "application/octet-stream") resp, err := (&http.Client{Timeout: 5 * time.Minute}).Do(req) if err != nil { @@ -274,7 +295,7 @@ func verifyChecksum(rel *ghRelease, assetName, path string) error { return nil } req, _ := http.NewRequest("GET", sumsURL, nil) - req.Header.Set("User-Agent", "jean-update") + req.Header.Set("User-Agent", "ajean-update") resp, err := (&http.Client{Timeout: 30 * time.Second}).Do(req) if err != nil { return fmt.Errorf("téléchargement des sommes de contrôle : %w", err) diff --git a/internal/ajean/sys_update_asset_test.go b/internal/ajean/sys_update_asset_test.go new file mode 100644 index 0000000..db8eb02 --- /dev/null +++ b/internal/ajean/sys_update_asset_test.go @@ -0,0 +1,63 @@ +package ajean + +import "testing" + +// relWith fabrique une release ne contenant que les assets nommés. +func relWith(names ...string) *ghRelease { + rel := &ghRelease{TagName: "v9.9.9"} + for _, n := range names { + rel.Assets = append(rel.Assets, struct { + Name string `json:"name"` + BrowserDownloadURL string `json:"browser_download_url"` + Size int64 `json:"size"` + }{Name: n, BrowserDownloadURL: "https://example.invalid/" + n, Size: 42}) + } + return rel +} + +// Une release de transition publie les deux noms : on doit préférer ajean-*. +func TestPickAssetPrefersAjean(t *testing.T) { + names := updateAssetNames() + got, url, _ := pickAsset(relWith(names[1], names[0])) + if got != names[0] { + t.Fatalf("got %q, attendu la préférence %q", got, names[0]) + } + if url == "" { + t.Fatal("URL vide") + } +} + +// Release future, purgée des anciens noms : ajean-* seul suffit. +func TestPickAssetAjeanOnly(t *testing.T) { + names := updateAssetNames() + if got, _, _ := pickAsset(relWith(names[0])); got != names[0] { + t.Fatalf("got %q, attendu %q", got, names[0]) + } +} + +// Ancienne release, antérieure au renommage : le repli sur jean-* doit marcher, +// sinon un client neuf ne pourrait pas s'installer depuis une release existante. +func TestPickAssetFallsBackToLegacy(t *testing.T) { + names := updateAssetNames() + got, _, _ := pickAsset(relWith(names[1])) + if got != names[1] { + t.Fatalf("got %q, attendu le repli %q", got, names[1]) + } +} + +// Le nom renvoyé sert à vérifier le SHA-256 : il doit être celui réellement +// choisi, pas le nom préféré, sinon la vérification échoue sur un repli. +func TestPickAssetReturnsCheckedName(t *testing.T) { + names := updateAssetNames() + got, url, size := pickAsset(relWith(names[1])) + if got != names[1] || url != "https://example.invalid/"+names[1] || size != 42 { + t.Fatalf("incohérence nom/URL/taille: %q %q %d", got, url, size) + } +} + +// Aucun asset pour cette plateforme → pas d'URL, l'appelant doit pouvoir le voir. +func TestPickAssetNoMatch(t *testing.T) { + if _, url, _ := pickAsset(relWith("ajean-plan9-386", "SHA256SUMS")); url != "" { + t.Fatalf("URL inattendue: %q", url) + } +} diff --git a/internal/jean/ui/index.html b/internal/ajean/ui/index.html similarity index 100% rename from internal/jean/ui/index.html rename to internal/ajean/ui/index.html diff --git a/internal/jean/ui/marked.min.js b/internal/ajean/ui/marked.min.js similarity index 100% rename from internal/jean/ui/marked.min.js rename to internal/ajean/ui/marked.min.js diff --git a/internal/jean/ui/src/index.tmpl.html b/internal/ajean/ui/src/index.tmpl.html similarity index 100% rename from internal/jean/ui/src/index.tmpl.html rename to internal/ajean/ui/src/index.tmpl.html diff --git a/internal/jean/ui/src/js/00-md.js b/internal/ajean/ui/src/js/00-md.js similarity index 100% rename from internal/jean/ui/src/js/00-md.js rename to internal/ajean/ui/src/js/00-md.js diff --git a/internal/jean/ui/src/js/01-theme.js b/internal/ajean/ui/src/js/01-theme.js similarity index 100% rename from internal/jean/ui/src/js/01-theme.js rename to internal/ajean/ui/src/js/01-theme.js diff --git a/internal/jean/ui/src/js/02-prefs.js b/internal/ajean/ui/src/js/02-prefs.js similarity index 100% rename from internal/jean/ui/src/js/02-prefs.js rename to internal/ajean/ui/src/js/02-prefs.js diff --git a/internal/jean/ui/src/js/03-ui-kit.js b/internal/ajean/ui/src/js/03-ui-kit.js similarity index 100% rename from internal/jean/ui/src/js/03-ui-kit.js rename to internal/ajean/ui/src/js/03-ui-kit.js diff --git a/internal/jean/ui/src/js/04-api.js b/internal/ajean/ui/src/js/04-api.js similarity index 100% rename from internal/jean/ui/src/js/04-api.js rename to internal/ajean/ui/src/js/04-api.js diff --git a/internal/jean/ui/src/js/05-status.js b/internal/ajean/ui/src/js/05-status.js similarity index 100% rename from internal/jean/ui/src/js/05-status.js rename to internal/ajean/ui/src/js/05-status.js diff --git a/internal/jean/ui/src/js/06-settings.js b/internal/ajean/ui/src/js/06-settings.js similarity index 100% rename from internal/jean/ui/src/js/06-settings.js rename to internal/ajean/ui/src/js/06-settings.js diff --git a/internal/jean/ui/src/js/07-models.js b/internal/ajean/ui/src/js/07-models.js similarity index 100% rename from internal/jean/ui/src/js/07-models.js rename to internal/ajean/ui/src/js/07-models.js diff --git a/internal/jean/ui/src/js/08-chat-render.js b/internal/ajean/ui/src/js/08-chat-render.js similarity index 100% rename from internal/jean/ui/src/js/08-chat-render.js rename to internal/ajean/ui/src/js/08-chat-render.js diff --git a/internal/jean/ui/src/js/09-stream.js b/internal/ajean/ui/src/js/09-stream.js similarity index 100% rename from internal/jean/ui/src/js/09-stream.js rename to internal/ajean/ui/src/js/09-stream.js diff --git a/internal/jean/ui/src/js/10-llamacpp.js b/internal/ajean/ui/src/js/10-llamacpp.js similarity index 100% rename from internal/jean/ui/src/js/10-llamacpp.js rename to internal/ajean/ui/src/js/10-llamacpp.js diff --git a/internal/jean/ui/src/js/12-mcp.js b/internal/ajean/ui/src/js/12-mcp.js similarity index 100% rename from internal/jean/ui/src/js/12-mcp.js rename to internal/ajean/ui/src/js/12-mcp.js diff --git a/internal/jean/ui/src/js/13-remote.js b/internal/ajean/ui/src/js/13-remote.js similarity index 100% rename from internal/jean/ui/src/js/13-remote.js rename to internal/ajean/ui/src/js/13-remote.js diff --git a/internal/jean/ui/src/styles.css b/internal/ajean/ui/src/styles.css similarity index 100% rename from internal/jean/ui/src/styles.css rename to internal/ajean/ui/src/styles.css diff --git a/internal/jean/web_api.go b/internal/ajean/web_api.go similarity index 99% rename from internal/jean/web_api.go rename to internal/ajean/web_api.go index 70d35f8..fd0b98c 100644 --- a/internal/jean/web_api.go +++ b/internal/ajean/web_api.go @@ -1,6 +1,6 @@ // web_api.go — handlers REST /api/* (statut, config, presets, modèles, // mémoire, agent, clés, bench…) du serveur web local. -package jean +package ajean import ( "encoding/json" @@ -169,7 +169,7 @@ func handleBackends(w http.ResponseWriter, r *http.Request) { // (essaie les sous-chemins des différents générateurs CMake). Renvoie // [{name, path}] — un par dossier de backend contenant un binaire. func listBackendBins() []map[string]any { - root := JeanHome() + "/backends" + root := AjeanHome() + "/backends" entries, err := os.ReadDir(root) if err != nil { return []map[string]any{} @@ -269,7 +269,7 @@ func handleLlamacppUninstallCustom(w http.ResponseWriter, r *http.Request) { func handleModels(w http.ResponseWriter, r *http.Request) { out := []map[string]any{} seen := map[string]bool{} - home := JeanHome() + home := AjeanHome() for _, dir := range modelDirs() { entries, err := os.ReadDir(dir) if err != nil { diff --git a/internal/jean/web_auth.go b/internal/ajean/web_auth.go similarity index 96% rename from internal/jean/web_auth.go rename to internal/ajean/web_auth.go index 66013e8..ae0e7bd 100644 --- a/internal/jean/web_auth.go +++ b/internal/ajean/web_auth.go @@ -1,4 +1,4 @@ -package jean +package ajean import ( "crypto/rand" @@ -22,7 +22,7 @@ import ( // requête (pas de cache) pour qu'un changement de clé prenne effet sans // redémarrer le serveur web. -func webKeyPath() string { return filepath.Join(JeanHome(), ".web_key") } +func webKeyPath() string { return filepath.Join(AjeanHome(), ".web_key") } // readWebKey returns the trimmed contents of $JEAN_HOME/.web_key, or "" if the // file is absent/empty. @@ -98,7 +98,7 @@ func cmdSetWebKey(args []string) error { fmt.Printf("%s clé de pilotage supprimée — l'API web n'est plus protégée\n", yellow("[info]")) return nil } - if err := os.MkdirAll(JeanHome(), 0o755); err != nil { + if err := os.MkdirAll(AjeanHome(), 0o755); err != nil { return err } if err := os.WriteFile(webKeyPath(), []byte(key+"\n"), 0o600); err != nil { diff --git a/internal/jean/web_chat.go b/internal/ajean/web_chat.go similarity index 99% rename from internal/jean/web_chat.go rename to internal/ajean/web_chat.go index ec7bb87..aa211c0 100644 --- a/internal/jean/web_chat.go +++ b/internal/ajean/web_chat.go @@ -1,6 +1,6 @@ // web_chat.go — endpoints de chat du serveur web local : envoi/stop/reset, // flux SSE d'abonnement à la conversation serveur (voir chat_conversation.go). -package jean +package ajean import ( "context" diff --git a/internal/jean/web_link_api.go b/internal/ajean/web_link_api.go similarity index 99% rename from internal/jean/web_link_api.go rename to internal/ajean/web_link_api.go index 9b137ea..ef71886 100644 --- a/internal/jean/web_link_api.go +++ b/internal/ajean/web_link_api.go @@ -1,4 +1,4 @@ -package jean +package ajean // web_link_api.go — API locale pilotant l'accès distant (ajean.link) depuis l'UI // web de Jean, pour éviter le terminal. Le panneau « Accès distant » ouvre une diff --git a/internal/jean/web_llamacpp.go b/internal/ajean/web_llamacpp.go similarity index 99% rename from internal/jean/web_llamacpp.go rename to internal/ajean/web_llamacpp.go index 0232ac1..c983a6b 100644 --- a/internal/jean/web_llamacpp.go +++ b/internal/ajean/web_llamacpp.go @@ -8,7 +8,7 @@ // POST /api/llamacpp/install → job d'installation {force?} // POST /api/llamacpp/update → job de mise à jour {clean?} // GET /api/llamacpp/job?from= → progression du job (phase, lignes, erreur) -package jean +package ajean import ( "encoding/json" @@ -166,7 +166,7 @@ func handleLlamacpp(w http.ResponseWriter, r *http.Request) { "bin": pbBin, "in_use": pbBin != "" && samePath(pbBin, cfgBin), } - out["backends_dir"] = filepath.Join(JeanHome(), "backends") + out["backends_dir"] = filepath.Join(AjeanHome(), "backends") out["job"] = lcJobSnapshot(0, false) sendJSON(w, 200, out) } diff --git a/internal/jean/web_prefs.go b/internal/ajean/web_prefs.go similarity index 96% rename from internal/jean/web_prefs.go rename to internal/ajean/web_prefs.go index 424e0be..9c10988 100644 --- a/internal/jean/web_prefs.go +++ b/internal/ajean/web_prefs.go @@ -1,4 +1,4 @@ -package jean +package ajean import ( "encoding/json" @@ -15,7 +15,7 @@ import ( // Avant ça, elles ne vivaient qu'en localStorage, donc par navigateur/appareil. // webPrefsPath est le fichier JSON des préférences d'apparence. -func webPrefsPath() string { return filepath.Join(JeanHome(), "webprefs.json") } +func webPrefsPath() string { return filepath.Join(AjeanHome(), "webprefs.json") } var webPrefsMu sync.Mutex diff --git a/internal/jean/web_server.go b/internal/ajean/web_server.go similarity index 99% rename from internal/jean/web_server.go rename to internal/ajean/web_server.go index b13939a..0b8d514 100644 --- a/internal/jean/web_server.go +++ b/internal/ajean/web_server.go @@ -1,4 +1,4 @@ -package jean +package ajean import ( "bufio" diff --git a/internal/jean/web_sysprompt.go b/internal/ajean/web_sysprompt.go similarity index 95% rename from internal/jean/web_sysprompt.go rename to internal/ajean/web_sysprompt.go index 1902821..5722740 100644 --- a/internal/jean/web_sysprompt.go +++ b/internal/ajean/web_sysprompt.go @@ -5,7 +5,7 @@ // /api/chat/send ne porte que le message → le champ de l'UI n'avait plus aucun // effet. Il est maintenant lu ici par la génération (chat_conversation.go), et // InjectSkills (llm_client.go) le fusionne avec le préambule agent. -package jean +package ajean import ( "encoding/json" @@ -15,7 +15,7 @@ import ( "strings" ) -func sysPromptPath() string { return filepath.Join(JeanHome(), "sysprompt.txt") } +func sysPromptPath() string { return filepath.Join(AjeanHome(), "sysprompt.txt") } // readSysPrompt renvoie le prompt système personnalisé ("" si absent). func readSysPrompt() string { diff --git a/tools/gen-icon/main.go b/tools/gen-icon/main.go index d90b24f..469a2c3 100644 --- a/tools/gen-icon/main.go +++ b/tools/gen-icon/main.go @@ -11,7 +11,7 @@ import ( "os" "strings" - "github.com/nathaninline/jean/internal/jean" + "github.com/nathaninline/ajean/internal/ajean" ) func main() { @@ -21,13 +21,13 @@ func main() { } // Les tailles attendues par l'explorateur Windows, de la barre des tâches à // l'aperçu en grandes icônes. - write(out, jean.BrandICO(16, 32, 48, 64, 128, 256)) + write(out, ajean.BrandICO(16, 32, 48, 64, 128, 256)) // PNG 1024 pour le bundle macOS : la CI en tire le .icns via sips/iconutil. // Elle partait auparavant du .ico, ce qui suppose que sips sache lire un ICO — // et l'échec y est masqué par « || true », donc une app Mac sans icône passait // inaperçue. Un PNG ne laisse aucune place au doute. - write(strings.TrimSuffix(out, ".ico")+".png", jean.BrandIconPNG(1024)) + write(strings.TrimSuffix(out, ".ico")+".png", ajean.BrandIconPNG(1024)) } func write(path string, data []byte) {