diff --git a/.github/workflows/mac-build.yml b/.github/workflows/mac-build.yml
index caee71d..52fb76a 100644
--- a/.github/workflows/mac-build.yml
+++ b/.github/workflows/mac-build.yml
@@ -1,4 +1,4 @@
-# Build macOS à la demande, SANS rien publier : produit Jean.app en artifact
+# Build macOS à la demande, SANS rien publier : produit AJEAN.app en artifact
# téléchargeable depuis l'onglet Actions (visible seulement par le dépôt).
# Sert à tester le bundle avant de décider d'une vraie release (release.yml).
name: mac-build
@@ -13,38 +13,38 @@ jobs:
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- - name: build + bundle Jean.app (arm64)
+ - name: build + bundle AJEAN.app (arm64)
run: |
mkdir -p dist
CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 \
- go build -trimpath -ldflags="-s -w" -o dist/jean-darwin-arm64 ./cmd/jean
+ go build -trimpath -ldflags="-s -w" -o dist/ajean-darwin-arm64 ./cmd/ajean
mkdir -p /tmp/jean.iconset
sips -s format png 'cmd/jean/icon.ico' --out /tmp/icon.png >/dev/null 2>&1 || true
for s in 16 32 128 256 512; do
sips -z $s $s /tmp/icon.png --out "/tmp/jean.iconset/icon_${s}x${s}.png" >/dev/null 2>&1 || true
done
iconutil -c icns /tmp/jean.iconset -o /tmp/jean.icns >/dev/null 2>&1 || true
- app="dist/Jean.app"
+ app="dist/AJEAN.app"
mkdir -p "$app/Contents/MacOS" "$app/Contents/Resources"
- cp dist/jean-darwin-arm64 "$app/Contents/MacOS/jean"
- chmod +x "$app/Contents/MacOS/jean"
+ cp dist/ajean-darwin-arm64 "$app/Contents/MacOS/ajean"
+ chmod +x "$app/Contents/MacOS/ajean"
cp /tmp/jean.icns "$app/Contents/Resources/jean.icns" 2>/dev/null || true
printf '%s\n' \
'' \
'' \
'' \
- 'CFBundleNameJean' \
- 'CFBundleDisplayNameJean' \
- 'CFBundleExecutablejean' \
- 'CFBundleIdentifierlink.ajean.jean' \
- 'CFBundleIconFilejean' \
+ 'CFBundleNameAJEAN' \
+ 'CFBundleDisplayNameAJEAN' \
+ 'CFBundleExecutableajean' \
+ 'CFBundleIdentifierlink.ajean.app' \
+ 'CFBundleIconFileajean' \
'CFBundlePackageTypeAPPL' \
'LSMinimumSystemVersion11.0' \
'LSUIElement' \
'NSHighResolutionCapable' \
'' > "$app/Contents/Info.plist"
codesign --force --deep -s - "$app" || true
- (cd dist && zip -qry Jean-macos-arm64.zip Jean.app && rm -rf Jean.app)
+ (cd dist && zip -qry AJEAN-macos-arm64.zip AJEAN.app && rm -rf AJEAN.app)
ls -l dist
- uses: actions/upload-artifact@v4
with:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 0b09f1f..c98b884 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -29,11 +29,11 @@ jobs:
mkdir -p dist
# arm64 = natif ; amd64 = cross avec le SDK universel (-arch x86_64).
CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 \
- go build -trimpath -ldflags="-s -w" -o dist/jean-darwin-arm64 ./cmd/jean
+ go build -trimpath -ldflags="-s -w" -o dist/ajean-darwin-arm64 ./cmd/ajean
CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 \
CGO_CFLAGS="-arch x86_64" CGO_LDFLAGS="-arch x86_64" \
- go build -trimpath -ldflags="-s -w" -o dist/jean-darwin-amd64 ./cmd/jean
- file dist/jean-darwin-*
+ go build -trimpath -ldflags="-s -w" -o dist/ajean-darwin-amd64 ./cmd/ajean
+ file dist/ajean-darwin-*
- name: bundles Jean.app
run: |
# Un binaire nu double-cliqué ouvrirait le Terminal. On publie donc
@@ -43,29 +43,29 @@ jobs:
# la barre de menus), comme le double-clic sur jean.exe sous Windows.
# LSUIElement : app de barre de menus, aucune icône dans le Dock.
# L'icône part du PNG 1024 généré avec le reste de la marque
- # (cmd/jean/icon.png, voir tools/gen-icon) : plus de conversion depuis
+ # (cmd/ajean/icon.png, voir tools/gen-icon) : plus de conversion depuis
# le .ico, dont l'échec silencieux donnait une app sans icône.
mkdir -p /tmp/jean.iconset
for s in 16 32 128 256 512; do
- sips -z $s $s 'cmd/jean/icon.png' --out "/tmp/jean.iconset/icon_${s}x${s}.png" >/dev/null
+ sips -z $s $s 'cmd/ajean/icon.png' --out "/tmp/jean.iconset/icon_${s}x${s}.png" >/dev/null
done
iconutil -c icns /tmp/jean.iconset -o /tmp/jean.icns
v="${GITHUB_REF_NAME#v}"
for arch in amd64 arm64; do
- app="dist/mac-$arch/Jean.app"
+ app="dist/mac-$arch/AJEAN.app"
mkdir -p "$app/Contents/MacOS" "$app/Contents/Resources"
- cp "dist/jean-darwin-$arch" "$app/Contents/MacOS/jean"
- chmod +x "$app/Contents/MacOS/jean"
- cp /tmp/jean.icns "$app/Contents/Resources/jean.icns" 2>/dev/null || true
+ cp "dist/ajean-darwin-$arch" "$app/Contents/MacOS/ajean"
+ chmod +x "$app/Contents/MacOS/ajean"
+ cp /tmp/jean.icns "$app/Contents/Resources/ajean.icns" 2>/dev/null || true
printf '%s\n' \
'' \
'' \
'' \
- 'CFBundleNameJean' \
- 'CFBundleDisplayNameJean' \
- 'CFBundleExecutablejean' \
- 'CFBundleIdentifierlink.ajean.jean' \
- 'CFBundleIconFilejean' \
+ 'CFBundleNameAJEAN' \
+ 'CFBundleDisplayNameAJEAN' \
+ 'CFBundleExecutableajean' \
+ 'CFBundleIdentifierlink.ajean.app' \
+ 'CFBundleIconFileajean' \
'CFBundlePackageTypeAPPL' \
"CFBundleShortVersionString$v" \
"CFBundleVersion$v" \
@@ -76,7 +76,11 @@ jobs:
# Signature ad-hoc : sans elle, macOS refuse purement et simplement
# de lancer un binaire arm64 non signé (pas juste un avertissement).
codesign --force --deep -s - "$app" || true
- (cd "dist/mac-$arch" && zip -qry "../Jean-macos-$arch.zip" Jean.app)
+ (cd "dist/mac-$arch" && zip -qry "../AJEAN-macos-$arch.zip" AJEAN.app)
+ # Nom hérité conservé : les liens de téléchargement déjà publiés
+ # (site, mails, messages) pointent dessus et doivent continuer de
+ # fonctionner. Même contenu, deux noms.
+ cp "dist/AJEAN-macos-$arch.zip" "dist/Jean-macos-$arch.zip"
rm -rf "dist/mac-$arch"
done
ls -l dist
@@ -102,16 +106,28 @@ jobs:
# (l'app se rattache à la console du terminal parent si lancée en CLI).
[ "$os" = windows ] && { ext=".exe"; ldflags="$ldflags -H=windowsgui"; }
GOOS=$os GOARCH=$arch CGO_ENABLED=0 \
- go build -trimpath -ldflags="$ldflags" -o "dist/jean-$os-$arch$ext" ./cmd/jean
+ go build -trimpath -ldflags="$ldflags" -o "dist/ajean-$os-$arch$ext" ./cmd/ajean
done
- uses: actions/download-artifact@v4
with:
name: macos
path: dist
+ - name: assets hérités jean-* (transition)
+ run: |
+ # Le parc déjà installé ne sait chercher QUE « jean-- » : sans
+ # ces copies, aucun des utilisateurs actuels ne pourrait se mettre à
+ # jour — pas même vers la version qui apprend à lire « ajean-* ».
+ # Après cette étape SEULEMENT, donc une fois les binaires macOS
+ # récupérés, les deux jeux de noms sont complets.
+ # À supprimer quand le parc aura basculé (quelques versions).
+ for f in dist/ajean-*; do
+ cp "$f" "dist/jean-${f#dist/ajean-}"
+ done
+ ls -l dist
- name: SHA256SUMS
run: |
- chmod +x dist/jean-darwin-*
- (cd dist && sha256sum jean-* Jean-macos-*.zip > SHA256SUMS)
+ chmod +x dist/ajean-darwin-* dist/jean-darwin-*
+ (cd dist && sha256sum ajean-* jean-* *.zip > SHA256SUMS)
ls -l dist && cat dist/SHA256SUMS
- name: publier la release
uses: softprops/action-gh-release@v2
diff --git a/internal/ajean/relay_link.go b/internal/ajean/relay_link.go
index 2b02f0e..1b2580b 100644
--- a/internal/ajean/relay_link.go
+++ b/internal/ajean/relay_link.go
@@ -93,8 +93,16 @@ func relayURL() string {
return defaultRelayURL
}
-// linkServiceName est l'unité systemd qui exécute le worker « jean link --foreground ».
-const linkServiceName = "jean-link"
+// linkUnitName / legacyLinkUnitName : l'unité systemd qui exécute le worker
+// « ajean link --foreground », et son nom d'avant le renommage.
+const (
+ linkUnitName = "ajean-link"
+ legacyLinkUnitName = "jean-link"
+)
+
+// linkServiceName renvoie l'unité de lien réellement installée sur la machine
+// (voir sys_unitname.go : un serveur mis à jour tourne encore sous jean-link).
+func linkServiceName() string { return resolveUnitName(linkUnitName, legacyLinkUnitName) }
func cmdLink(args []string) error {
sub := ""
@@ -121,9 +129,9 @@ func cmdLink(args []string) error {
}
fmt.Printf("%s token enregistré (%s…), relais: %s\n", green("[ok]"), tok[:min(8, len(tok))], relayURL())
if linkServiceActive() {
- fmt.Printf("%s service %s: actif\n", green("[ok]"), linkServiceName)
+ fmt.Printf("%s service %s: actif\n", green("[ok]"), linkServiceName())
} else {
- fmt.Printf("%s service %s: arrêté (jean link pour démarrer)\n", yellow("[info]"), linkServiceName)
+ fmt.Printf("%s service %s: arrêté (jean link pour démarrer)\n", yellow("[info]"), linkServiceName())
}
return nil
case "logout":
@@ -174,7 +182,7 @@ func startLink(force bool) error {
return err
}
case linkServiceActive():
- fmt.Printf("%s service %s déjà en cours — « jean link restart » pour le relancer\n", yellow("[info]"), linkServiceName)
+ fmt.Printf("%s service %s déjà en cours — « jean link restart » pour le relancer\n", yellow("[info]"), linkServiceName())
default:
if err := linkServiceCtl("start"); err != nil {
return err
@@ -375,8 +383,29 @@ 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(AjeanHome(), linkServiceName+".pid") }
-func linkLogPath() string { return filepath.Join(AjeanHome(), linkServiceName+".log") }
+func linkPIDPath() string { return adoptLegacyLinkFile(".pid") }
+func linkLogPath() string { return adoptLegacyLinkFile(".log") }
+
+// adoptLegacyLinkFile renvoie le chemin « ajean-link » en reprenant au
+// passage le « jean-link » laissé par une version antérieure.
+//
+// Reprendre le fichier PID n'est pas cosmétique : c'est LUI qui dit si un worker
+// tourne déjà. Sans cette reprise, la première version renommée ne verrait plus
+// le worker en cours, en démarrerait un second, et la machine se retrouverait
+// avec deux tunnels concurrents vers le relais. Idempotent, silencieux en cas
+// d'échec (on retombe simplement sur le nouveau nom).
+func adoptLegacyLinkFile(ext string) string {
+ newPath := filepath.Join(AjeanHome(), linkUnitName+ext)
+ if _, err := os.Stat(newPath); err == nil {
+ return newPath
+ }
+ legacy := filepath.Join(AjeanHome(), legacyLinkUnitName+ext)
+ if _, err := os.Stat(legacy); err != nil {
+ return newPath
+ }
+ _ = os.Rename(legacy, newPath)
+ return newPath
+}
// 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) ;
@@ -411,16 +440,16 @@ func linkServiceCtl(action string) error {
if os.Geteuid() != 0 {
bin, pre = "sudo", []string{"-n", "systemctl"}
}
- cmd := exec.Command(bin, append(pre, action, linkServiceName)...)
+ cmd := exec.Command(bin, append(pre, action, linkServiceName())...)
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
if err := cmd.Run(); err != nil {
- return fmt.Errorf("systemctl %s %s: %w", action, linkServiceName, err)
+ return fmt.Errorf("systemctl %s %s: %w", action, linkServiceName(), err)
}
switch action {
case "start", "restart":
- fmt.Printf("%s service %s %s\n", green("[ok]"), linkServiceName, action+"é")
+ fmt.Printf("%s service %s %s\n", green("[ok]"), linkServiceName(), action+"é")
case "stop":
- fmt.Printf("%s service %s arrêté\n", green("[ok]"), linkServiceName)
+ fmt.Printf("%s service %s arrêté\n", green("[ok]"), linkServiceName())
}
return nil
}
@@ -434,7 +463,7 @@ func linkServiceActive() bool {
}
return linkUserPID() > 0
}
- out, _ := exec.Command("systemctl", "is-active", linkServiceName).Output()
+ out, _ := exec.Command("systemctl", "is-active", linkServiceName()).Output()
return strings.TrimSpace(string(out)) == "active"
}
@@ -467,12 +496,12 @@ func linkUserSvcCtl(action string) error {
}
_ = os.Remove(linkPIDPath())
if action == "stop" {
- fmt.Printf("%s service %s arrêté\n", green("[ok]"), linkServiceName)
+ fmt.Printf("%s service %s arrêté\n", green("[ok]"), linkServiceName())
return nil
}
case "start":
if linkUserPID() > 0 {
- fmt.Printf("%s service %s déjà en cours\n", yellow("[info]"), linkServiceName)
+ fmt.Printf("%s service %s déjà en cours\n", yellow("[info]"), linkServiceName())
return nil
}
default:
@@ -516,7 +545,7 @@ func linkUserSvcCtl(action string) error {
_ = os.Remove(linkPIDPath())
return fmt.Errorf("le lien s'est arrêté aussitôt — voir %s", linkLogPath())
}
- fmt.Printf("%s service %s démarré (PID %d)\n", green("[ok]"), linkServiceName, pid)
+ fmt.Printf("%s service %s démarré (PID %d)\n", green("[ok]"), linkServiceName(), pid)
return nil
}
diff --git a/internal/ajean/run.go b/internal/ajean/run.go
index b526c94..cfc0f97 100644
--- a/internal/ajean/run.go
+++ b/internal/ajean/run.go
@@ -234,13 +234,25 @@ func legacySkillsFlag() string { return filepath.Join(skillsDir(), ".enabled") }
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") }
+// serviceName est le nom de l'unité qui exécute llama-server. AJEAN_SERVICE et
+// JEAN_SERVICE sont tous deux honorés : le second est posé à la main sur des
+// machines qu'on ne voit pas, le renommage ne doit pas les casser.
func serviceName() string {
+ if n := os.Getenv("AJEAN_SERVICE"); n != "" {
+ return n
+ }
if n := os.Getenv("JEAN_SERVICE"); n != "" {
return n
}
- return "jean"
+ // Pas de nom imposé : on prend l'unité réellement installée. Un serveur
+ // simplement mis à jour tourne encore sous « jean.service » — voir
+ // sys_unitname.go.
+ return resolveUnitName("ajean", legacyServiceName())
}
+// legacyServiceName est le nom d'avant le renommage.
+func legacyServiceName() string { return "jean" }
+
// Color helpers (ANSI). Disabled when stdout is not a TTY.
var colorOn = isTerminal()
diff --git a/internal/ajean/sys_install_darwin.go b/internal/ajean/sys_install_darwin.go
index 56f55d8..404e83d 100644
--- a/internal/ajean/sys_install_darwin.go
+++ b/internal/ajean/sys_install_darwin.go
@@ -81,25 +81,25 @@ func cmdInstall(args []string) error {
if err != nil {
return fmt.Errorf("utilisateur '%s' introuvable: %w", targetUser, err)
}
- jeanHome := defaultJeanHome()
+ ajeanHome := defaultAjeanHome()
if v := os.Getenv("JEAN_HOME"); v != "" {
- jeanHome = v
+ ajeanHome = v
}
svc := serviceName()
fmt.Printf("Installation pour utilisateur %s\n", cyan(targetUser))
- fmt.Printf(" JEAN_HOME = %s\n", jeanHome)
+ fmt.Printf(" JEAN_HOME = %s\n", ajeanHome)
fmt.Printf(" service = %s (launchd)\n", svc)
// 1. Répertoires
- for _, d := range []string{jeanHome, filepath.Join(jeanHome, "configs"), filepath.Join(jeanHome, "SKILLS")} {
+ for _, d := range []string{ajeanHome, filepath.Join(ajeanHome, "configs"), filepath.Join(ajeanHome, "SKILLS")} {
if err := os.MkdirAll(d, 0o755); err != nil {
return err
}
}
// 2. config.env si absent
- conf := filepath.Join(jeanHome, "config.env")
+ conf := filepath.Join(ajeanHome, "config.env")
if _, err := os.Stat(conf); os.IsNotExist(err) {
body := strings.ReplaceAll(configTemplate, "USER", targetUser)
if err := os.WriteFile(conf, []byte(body), 0o644); err != nil {
@@ -137,17 +137,19 @@ func cmdInstall(args []string) error {
fmt.Printf(" %s %s -> %s\n", green("✓"), target, self)
}
- // 4. /etc/default/jean pour que les invocations CLI résolvent JEAN_HOME.
+ // 4. /etc/default/ajean pour que les invocations CLI résolvent AJEAN_HOME.
+ // Les deux clés sont écrites : des scripts d'utilisateurs sourcent ce fichier
+ // et lisent encore $JEAN_HOME.
_ = os.MkdirAll("/etc/default", 0o755)
- defaults := fmt.Sprintf("# Generated by jean install — racine des configs/skills/SKILLS\nJEAN_HOME=%s\n", jeanHome)
- if err := os.WriteFile("/etc/default/jean", []byte(defaults), 0o644); err != nil {
+ defaults := fmt.Sprintf("# Generated by ajean install — racine des configs/memoire/presets\nAJEAN_HOME=%s\nJEAN_HOME=%s\n", ajeanHome, ajeanHome)
+ if err := os.WriteFile("/etc/default/ajean", []byte(defaults), 0o644); err != nil {
return err
}
- fmt.Printf(" %s /etc/default/jean\n", green("✓"))
+ fmt.Printf(" %s /etc/default/ajean\n", green("✓"))
// 5. LaunchDaemon plist (le log va sous JEAN_HOME, accessible au user cible)
- logPath := filepath.Join(jeanHome, svc+".log")
- plist := fmt.Sprintf(launchdPlistTemplate, launchdLabel(svc), targetUser, jeanHome, jeanHome, logPath, logPath)
+ logPath := filepath.Join(ajeanHome, svc+".log")
+ plist := fmt.Sprintf(launchdPlistTemplate, launchdLabel(svc), targetUser, ajeanHome, ajeanHome, logPath, logPath)
plistPath := launchdPlistPath(svc)
if err := os.WriteFile(plistPath, []byte(plist), 0o644); err != nil {
return err
@@ -155,7 +157,7 @@ func cmdInstall(args []string) error {
fmt.Printf(" %s %s\n", green("✓"), plistPath)
// 6. chown JEAN_HOME au user cible
- chown(jeanHome, u)
+ chown(ajeanHome, u)
fmt.Println()
fmt.Printf("%s installation terminée.\n", green("[ok]"))
@@ -175,8 +177,9 @@ func cmdUninstall(args []string) error {
_ = exec.Command("launchctl", "unload", "-w", launchdPlistPath(svc)).Run()
for _, p := range []string{
launchdPlistPath(svc),
- "/etc/default/jean",
- "/usr/local/bin/jean",
+ "/etc/default/ajean",
+ installedExePath(),
+ legacyExePath(),
} {
if err := os.Remove(p); err == nil {
fmt.Printf(" %s %s\n", green("✓"), p)
diff --git a/internal/ajean/sys_install_linux.go b/internal/ajean/sys_install_linux.go
index d53fedc..2d3f0b0 100644
--- a/internal/ajean/sys_install_linux.go
+++ b/internal/ajean/sys_install_linux.go
@@ -76,25 +76,25 @@ func cmdInstall(args []string) error {
if err != nil {
return fmt.Errorf("utilisateur '%s' introuvable: %w", targetUser, err)
}
- jeanHome := defaultJeanHome()
+ ajeanHome := defaultAjeanHome()
if v := os.Getenv("JEAN_HOME"); v != "" {
- jeanHome = v
+ ajeanHome = v
}
svc := serviceName()
fmt.Printf("Installation pour utilisateur %s\n", cyan(targetUser))
- fmt.Printf(" JEAN_HOME = %s\n", jeanHome)
+ fmt.Printf(" JEAN_HOME = %s\n", ajeanHome)
fmt.Printf(" service = %s\n", svc)
// 1. Create directories
- for _, d := range []string{jeanHome, filepath.Join(jeanHome, "configs"), filepath.Join(jeanHome, "SKILLS")} {
+ for _, d := range []string{ajeanHome, filepath.Join(ajeanHome, "configs"), filepath.Join(ajeanHome, "SKILLS")} {
if err := os.MkdirAll(d, 0o755); err != nil {
return err
}
}
// 2. Drop a config.env if none exists
- conf := filepath.Join(jeanHome, "config.env")
+ conf := filepath.Join(ajeanHome, "config.env")
if _, err := os.Stat(conf); os.IsNotExist(err) {
body := strings.ReplaceAll(configTemplate, "USER", targetUser)
if err := os.WriteFile(conf, []byte(body), 0o644); err != nil {
@@ -111,7 +111,7 @@ func cmdInstall(args []string) error {
if rp, e := filepath.EvalSymlinks(self); e == nil {
self = rp
}
- target := "/usr/local/bin/jean"
+ target := installedExePath()
// Garde-fou (issue #5) : si on tourne DÉJÀ depuis la cible (l'utilisateur a
// posé le binaire dans /usr/local/bin/jean puis lancé `sudo jean install`),
// ne surtout pas Remove+Symlink sur soi-même — ça effacerait le vrai binaire
@@ -134,15 +134,34 @@ func cmdInstall(args []string) error {
fmt.Printf(" %s %s -> %s\n", green("✓"), target, self)
}
- // 4. Drop /etc/default/jean so root invocations resolve JEAN_HOME correctly.
- defaults := fmt.Sprintf("# Generated by jean install — racine des configs/skills/SKILLS\nJEAN_HOME=%s\n", jeanHome)
- if err := os.WriteFile("/etc/default/jean", []byte(defaults), 0o644); err != nil {
+ // 3 bis. Alias hérité : `jean` doit rester tapable. Des utilisateurs ont des
+ // alias shell, des cron, des scripts de déploiement et des tutos qui appellent
+ // « jean » — on ne contrôle rien de tout ça, et un binaire renommé qui casse
+ // la commande d'hier est une régression pour eux. Le lien pointe sur le
+ // nouveau nom, donc il suit les mises à jour sans entretien.
+ if alias := legacyExePath(); alias != target {
+ if rp, e := filepath.EvalSymlinks(alias); e != nil || rp != self {
+ _ = os.Remove(alias)
+ if err := os.Symlink(target, alias); err != nil {
+ fmt.Printf(" %s alias %s non créé (%v) — « ajean » reste disponible\n", yellow("[info]"), alias, err)
+ } else {
+ fmt.Printf(" %s %s -> %s (alias hérité)\n", green("✓"), alias, target)
+ }
+ }
+ }
+
+ // 4. Drop /etc/default/ajean so root invocations resolve AJEAN_HOME correctly.
+ // On écrit les DEUX clés : des scripts d'utilisateurs sourcent ce fichier et
+ // lisent $JEAN_HOME. L'ancien /etc/default/jean est laissé en place — le
+ // supprimer casserait ces mêmes scripts, et AjeanHome() sait déjà le lire.
+ defaults := fmt.Sprintf("# Generated by ajean install — racine des configs/memoire/presets\nAJEAN_HOME=%s\nJEAN_HOME=%s\n", ajeanHome, ajeanHome)
+ if err := os.WriteFile("/etc/default/ajean", []byte(defaults), 0o644); err != nil {
return err
}
- fmt.Printf(" %s /etc/default/jean\n", green("✓"))
+ fmt.Printf(" %s /etc/default/ajean\n", green("✓"))
- // 5. Write the systemd unit (ExecStart = `jean serve`, no start.sh needed)
- unit := fmt.Sprintf(serviceUnitTemplate, targetUser, jeanHome, "/usr/local/bin/jean serve")
+ // 5. Write the systemd unit (ExecStart = `ajean serve`, no start.sh needed)
+ unit := fmt.Sprintf(serviceUnitTemplate, targetUser, ajeanHome, installedExePath()+" serve")
unitPath := "/etc/systemd/system/" + svc + ".service"
if err := os.WriteFile(unitPath, []byte(unit), 0o644); err != nil {
return err
@@ -151,14 +170,14 @@ func cmdInstall(args []string) error {
// 5. Sudoers drop-in
sudoers := fmt.Sprintf(sudoersTemplate, targetUser, svc, targetUser, svc, svc, svc, svc, svc)
- sudoersPath := "/etc/sudoers.d/jean-" + svc
+ sudoersPath := "/etc/sudoers.d/ajean-" + svc
if err := os.WriteFile(sudoersPath, []byte(sudoers), 0o440); err != nil {
return err
}
fmt.Printf(" %s %s\n", green("✓"), sudoersPath)
// 6. chown JEAN_HOME contents to target user
- chown(jeanHome, u)
+ chown(ajeanHome, u)
// 7. systemd reload
_ = exec.Command("systemctl", "daemon-reload").Run()
@@ -191,9 +210,9 @@ func cmdUninstall(args []string) error {
_ = exec.Command("systemctl", "disable", svc).Run()
for _, p := range []string{
"/etc/systemd/system/" + svc + ".service",
- "/etc/sudoers.d/jean-" + svc,
+ "/etc/sudoers.d/ajean-" + svc,
"/etc/default/jean",
- "/usr/local/bin/jean",
+ installedExePath(),
} {
if err := os.Remove(p); err == nil {
fmt.Printf(" %s %s\n", green("✓"), p)
diff --git a/internal/ajean/sys_install_windows.go b/internal/ajean/sys_install_windows.go
index 9aae2dc..4cfd47e 100644
--- a/internal/ajean/sys_install_windows.go
+++ b/internal/ajean/sys_install_windows.go
@@ -97,14 +97,15 @@ func cmdInstall(args []string) error {
return nil
}
-// installSelf copies the currently running executable into binDir as jean.exe
+// installSelf copies the currently running executable into binDir as ajean.exe
// and returns the destination path. If the running exe already lives there
-// (re-install), it's a no-op.
+// (re-install), it's a no-op. Une copie « jean.exe » est posée à côté pour que
+// l'ancienne commande reste tapable (voir installLegacyAlias).
//
// binDir est créé ici, et pas seulement par l'appelant : sur une machine vierge,
// le premier lancement passait par appFirstRun, qui ne créait que le dossier de
// données et pas son sous-dossier bin. L'utilisateur recevait alors « open
-// C:\ProgramData\jean\bin\jean.exe: The system cannot find the path specified »
+// C:\ProgramData\ajean\bin\ajean.exe: The system cannot find the path specified »
// et AJEAN démarrait depuis le fichier téléchargé, sans jamais s'installer.
func installSelf(binDir string) (string, error) {
src, err := os.Executable()
@@ -112,32 +113,54 @@ func installSelf(binDir string) (string, error) {
return "", err
}
src, _ = filepath.EvalSymlinks(src)
- dst := filepath.Join(binDir, "jean.exe")
+ dst := filepath.Join(binDir, "ajean.exe")
if strings.EqualFold(src, dst) {
+ installLegacyAlias(binDir, src)
return dst, nil
}
if err := os.MkdirAll(binDir, 0o755); err != nil {
return "", err
}
- in, err := os.Open(src)
- if err != nil {
+ if err := copyExe(src, dst); err != nil {
return "", err
}
+ installLegacyAlias(binDir, dst)
+ return dst, nil
+}
+
+// installLegacyAlias pose une copie « jean.exe » à côté de « ajean.exe ».
+//
+// Une copie et pas un lien : créer un lien symbolique sous Windows demande des
+// droits particuliers qu'on n'a pas toujours, et un binaire de quelques dizaines
+// de Mo dupliqué est un prix dérisoire pour ne casser ni les raccourcis du menu
+// Démarrer, ni les tâches planifiées, ni les scripts que les utilisateurs ont
+// écrits eux-mêmes. Best-effort : un échec ne compromet pas l'installation,
+// « ajean » reste disponible.
+func installLegacyAlias(binDir, src string) {
+ alias := filepath.Join(binDir, "jean.exe")
+ if strings.EqualFold(src, alias) {
+ return
+ }
+ // Échoue si un « jean.exe » est en cours d'exécution — sans gravité, il s'agit
+ // alors déjà d'une version d'AJEAN, et la copie repassera au prochain lancement.
+ _ = copyExe(src, alias)
+}
+
+func copyExe(src, dst string) error {
+ in, err := os.Open(src)
+ if err != nil {
+ return err
+ }
defer in.Close()
- // Can't overwrite a running exe, but jean.exe in binDir isn't the one we're
- // running (src != dst here), so a plain create is fine.
out, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o755)
if err != nil {
- return "", err
+ return err
}
if _, err := io.Copy(out, in); err != nil {
out.Close()
- return "", err
+ return err
}
- if err := out.Close(); err != nil {
- return "", err
- }
- return dst, nil
+ return out.Close()
}
// addToUserPath appends dir to the per-user PATH (HKCU\Environment) persistently,
diff --git a/internal/ajean/sys_paths.go b/internal/ajean/sys_paths.go
index a7ec1e8..7ef148d 100644
--- a/internal/ajean/sys_paths.go
+++ b/internal/ajean/sys_paths.go
@@ -26,14 +26,22 @@ type ajeanPaths struct {
}
// installedExePath est l'emplacement canonique du binaire après installation.
-// Il diffère par plateforme : sous Unix les installateurs posent /usr/local/bin/jean
+// Il diffère par plateforme : sous Unix les installateurs posent /usr/local/bin/ajean
// (c'est ce que référencent les unités systemd et le plist launchd) ; sous Windows,
-// faute d'équivalent, on utilise JEAN_HOME\bin ajouté au PATH utilisateur.
-func installedExePath() string {
+// faute d'équivalent, on utilise AJEAN_HOME\bin ajouté au PATH utilisateur.
+func installedExePath() string { return exePathNamed("ajean") }
+
+// legacyExePath est l'emplacement d'avant le renommage. L'installation continue
+// d'y poser un alias : `jean` reste tapable indéfiniment. Ça ne coûte qu'un lien
+// et ça préserve les habitudes, les scripts cron, les alias shell et les tutos
+// déjà écrits par les utilisateurs — dont on ne contrôle aucun.
+func legacyExePath() string { return exePathNamed("jean") }
+
+func exePathNamed(name string) string {
if runtime.GOOS == "windows" {
- return filepath.Join(AjeanHome(), "bin", "jean.exe")
+ return filepath.Join(AjeanHome(), "bin", name+".exe")
}
- return "/usr/local/bin/jean"
+ return "/usr/local/bin/" + name
}
func currentPaths() ajeanPaths {
diff --git a/internal/ajean/sys_service_darwin.go b/internal/ajean/sys_service_darwin.go
index 4a167f5..c277e46 100644
--- a/internal/ajean/sys_service_darwin.go
+++ b/internal/ajean/sys_service_darwin.go
@@ -18,8 +18,25 @@ import (
// le support macOS était absent (le code systemd/Linux était utilisé par erreur,
// cf. issue #4). Implémentation prudente basée sur launchctl load/unload/list.
-// launchdLabel dérive le label launchd du service (ex. "com.jean.jean").
-func launchdLabel(svc string) string { return "com.jean." + svc }
+// launchdLabel dérive le label launchd du service (ex. "com.ajean.ajean"), en
+// conservant le préfixe hérité « com.jean. » tant que c'est LUI qui est
+// réellement installé : le plist d'un daemon déjà chargé ne se renomme pas tout
+// seul, et viser le mauvais label reviendrait à ne plus voir le service.
+func launchdLabel(svc string) string {
+ modern := "com.ajean." + svc
+ if _, err := os.Stat("/Library/LaunchDaemons/" + modern + ".plist"); err == nil {
+ return modern
+ }
+ if legacy := "com.jean." + svc; fileExists("/Library/LaunchDaemons/" + legacy + ".plist") {
+ return legacy
+ }
+ return modern
+}
+
+func fileExists(p string) bool {
+ _, err := os.Stat(p)
+ return err == nil
+}
// launchdPlistPath : chemin du LaunchDaemon (domaine système, exécuté par root
// puis abaissé à l'utilisateur cible via la clé UserName du plist).
diff --git a/internal/ajean/sys_unitname.go b/internal/ajean/sys_unitname.go
new file mode 100644
index 0000000..711e81f
--- /dev/null
+++ b/internal/ajean/sys_unitname.go
@@ -0,0 +1,49 @@
+package ajean
+
+import "os"
+
+// Nom des unités système, pendant et après le renommage jean → ajean.
+//
+// Le problème : une mise à jour remplace le BINAIRE, pas les unités systemd.
+// Un serveur déjà installé continue donc de tourner sous « jean.service » et
+// « jean-link.service », qui pointent sur /usr/local/bin/jean. Si le binaire
+// renommé partait du principe que les unités s'appellent désormais « ajean* »,
+// le premier `systemctl restart` échouerait — et sur un serveur distant, ça veut
+// dire l'accès coupé, sans terminal pour réparer.
+//
+// La solution retenue n'est PAS de migrer les unités au démarrage (il faudrait
+// root, et un échec laisserait la machine sans service). C'est de résoudre le
+// nom à l'usage : on utilise l'unité qui existe réellement. Une machine ancienne
+// reste sur ses unités « jean* » et fonctionne ; une machine fraîchement
+// installée — ou réinstallée via `ajean install` — utilise les « ajean* ».
+// Aucune fenêtre pendant laquelle plus rien ne tourne.
+
+// unitExists dit si un service de ce nom est installé sur la machine : unité
+// systemd sous Linux, daemon launchd sous macOS (repéré par son plist, sous
+// l'un ou l'autre des deux préfixes de label). Toujours faux sous Windows, où
+// le « service » est un simple processus détaché suivi par un fichier PID.
+func unitExists(name string) bool {
+ paths := []string{
+ "/etc/systemd/system/" + name + ".service",
+ "/Library/LaunchDaemons/com.ajean." + name + ".plist",
+ "/Library/LaunchDaemons/com.jean." + name + ".plist",
+ }
+ for _, p := range paths {
+ if _, err := os.Stat(p); err == nil {
+ return true
+ }
+ }
+ return false
+}
+
+// resolveUnitName renvoie le nom d'unité à utiliser : le nouveau s'il est
+// installé, sinon l'ancien s'il l'est, sinon le nouveau (installation à venir).
+func resolveUnitName(modern, legacy string) string {
+ if unitExists(modern) {
+ return modern
+ }
+ if unitExists(legacy) {
+ return legacy
+ }
+ return modern
+}
diff --git a/internal/ajean/sys_update.go b/internal/ajean/sys_update.go
index f91e5c0..9717199 100644
--- a/internal/ajean/sys_update.go
+++ b/internal/ajean/sys_update.go
@@ -420,9 +420,9 @@ func restartAfterUpdate() (bool, string) {
time.Sleep(1500 * time.Millisecond) // laisser la réponse HTTP atteindre le client
// --no-block : on enregistre le job puis on rend la main ; systemd exécute le
// stop/start même si ce process (et le client systemctl) sont tués entre-temps.
- _ = exec.Command("systemctl", "--no-block", "restart", linkServiceName).Run()
+ _ = exec.Command("systemctl", "--no-block", "restart", linkServiceName()).Run()
}()
- return true, "Service " + linkServiceName + " redémarré automatiquement — la page va se reconnecter seule (le modèle n'est pas rechargé)."
+ return true, "Service " + linkServiceName() + " redémarré automatiquement — la page va se reconnecter seule (le modèle n'est pas rechargé)."
}
func restartHintText() string {