diff --git a/install_unix.go b/install_unix.go index 2e0cd85..e4a8119 100644 --- a/install_unix.go +++ b/install_unix.go @@ -108,15 +108,31 @@ func cmdInstall(args []string) error { if err != nil { return err } + if rp, e := filepath.EvalSymlinks(self); e == nil { + self = rp + } target := "/usr/local/bin/jean" - _ = os.Remove(target) - if err := os.Symlink(self, target); err != nil { - // fall back to copy if symlink fails (e.g. cross-fs) - if data, err := os.ReadFile(self); err == nil { - _ = os.WriteFile(target, data, 0o755) + // 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 + // et créerait /usr/local/bin/jean -> /usr/local/bin/jean (« Too many levels + // of symbolic links »). On compare les chemins réels (résolus). + alreadyInPlace := false + if tp, e := filepath.EvalSymlinks(target); e == nil && tp == self { + alreadyInPlace = true + } + if alreadyInPlace { + fmt.Printf(" %s %s est déjà le binaire installé (aucun lien à créer)\n", green("✓"), target) + } else { + _ = os.Remove(target) + if err := os.Symlink(self, target); err != nil { + // fall back to copy if symlink fails (e.g. cross-fs) + if data, err := os.ReadFile(self); err == nil { + _ = os.WriteFile(target, data, 0o755) + } } + fmt.Printf(" %s %s -> %s\n", green("✓"), target, self) } - 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)