diff --git a/internal/ajean/backend_llamacpp.go b/internal/ajean/backend_llamacpp.go index c84f664..1eb70fd 100644 --- a/internal/ajean/backend_llamacpp.go +++ b/internal/ajean/backend_llamacpp.go @@ -82,7 +82,7 @@ func llamacppRepoDir() string { } func defaultRepoDir() string { - return filepath.Join(AjeanHome(), "backends", "llama.cpp") + return filepath.Join(backendsDir(), "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(AjeanHome(), "backends")) + root, err := filepath.Abs(backendsDir()) if err != nil { return "", err } diff --git a/internal/ajean/backend_prebuilt.go b/internal/ajean/backend_prebuilt.go index 8330b8f..3a4e415 100644 --- a/internal/ajean/backend_prebuilt.go +++ b/internal/ajean/backend_prebuilt.go @@ -38,7 +38,7 @@ type ghAsset struct { } func prebuiltDir() string { - return filepath.Join(AjeanHome(), "backends", "llama.cpp-prebuilt") + return filepath.Join(backendsDir(), "llama.cpp-prebuilt") } // prebuiltVersion lit le marqueur VERSION du dossier prebuilt : "tag cudaVer" diff --git a/internal/ajean/chat_workspace.go b/internal/ajean/chat_workspace.go index 687b8fb..20016fb 100644 --- a/internal/ajean/chat_workspace.go +++ b/internal/ajean/chat_workspace.go @@ -60,11 +60,8 @@ func workspaceCandidates() []string { if v := strings.TrimSpace(os.Getenv(workspaceEnv)); v != "" { c = append(c, v) } - c = append(c, filepath.Join(AjeanHome(), "workspace")) + c = append(c, workspaceDir()) if home, err := os.UserHomeDir(); err == nil && home != "" { - // Le nouveau nom d'abord, l'ancien ensuite : un dossier ~/ajean/workspace - // déjà rempli par l'agent reste utilisable au lieu de repartir de zéro. - c = append(c, filepath.Join(home, "ajean", "workspace")) c = append(c, filepath.Join(home, "ajean", "workspace")) } c = append(c, filepath.Join(os.TempDir(), "ajean-workspace")) diff --git a/internal/ajean/sys_datadir.go b/internal/ajean/sys_datadir.go new file mode 100644 index 0000000..0d3f0ff --- /dev/null +++ b/internal/ajean/sys_datadir.go @@ -0,0 +1,51 @@ +package ajean + +import ( + "os" + "runtime" +) + +// sys_datadir.go — création du dossier de données, identique sur les trois +// plateformes. Avant, chaque installateur (Linux, macOS, Windows, plus le +// premier lancement de l'app) créait ses dossiers et écrivait son propre +// modèle de config.env : quatre copies qui divergeaient à la première +// modification. Il n'y en a plus qu'une. + +// dataDirs est l'arborescence complète de $AJEAN_HOME. Rien d'autre n'y est +// créé : tout le reste vit dans ajean.db. +func dataDirs() []string { + return []string{AjeanHome(), backendsDir(), binDir(), presetsDir(), memoryDir(), modelsDir(), workspaceDir()} +} + +// defaultConfig est la configuration de départ d'une installation neuve. Les +// valeurs sont volontairement incomplètes (BIN et MODEL sont vides) : c'est +// l'écran d'accueil, ou « ajean llamacpp install », qui les renseigne. +func defaultConfig() map[string]string { + host := "0.0.0.0" + if runtime.GOOS == "windows" { + host = "127.0.0.1" + } + return map[string]string{ + "PORT": "8080", + "HOST": host, + "CTX": "32768", + "BATCH": "2048", + "UBATCH": "512", + "NGL": "999", + } +} + +// provisionDataDir crée l'arborescence et, sur une installation neuve, pose la +// configuration de départ. Idempotente : une configuration existante n'est +// jamais écrasée. +func provisionDataDir() error { + for _, d := range dataDirs() { + if err := os.MkdirAll(d, 0o755); err != nil { + return err + } + } + if len(ReadConfig()) > 0 { + return nil + } + return WriteConfig(defaultConfig()) +} diff --git a/internal/ajean/sys_firstrun_other.go b/internal/ajean/sys_firstrun_other.go index b99bf1a..51df785 100644 --- a/internal/ajean/sys_firstrun_other.go +++ b/internal/ajean/sys_firstrun_other.go @@ -2,14 +2,12 @@ package ajean -import "os" - // appFirstRun : hors Windows, `ajean app` est lancé depuis un terminal par // quelqu'un qui a déjà suivi l'installation — pas de double-clic à désambiguïser, // donc pas de boîte de dialogue. On se contente de provisionner les données au -// premier lancement, comme avant. Ne relance jamais (renvoie toujours false). +// premier lancement. Ne relance jamais (renvoie toujours false). func appFirstRun() bool { - if _, err := os.Stat(confPath()); err != nil { + if len(ReadConfig()) == 0 { _ = cmdInstall(nil) } return false diff --git a/internal/ajean/sys_firstrun_windows.go b/internal/ajean/sys_firstrun_windows.go index fe54e78..d38e353 100644 --- a/internal/ajean/sys_firstrun_windows.go +++ b/internal/ajean/sys_firstrun_windows.go @@ -372,26 +372,6 @@ type vsFixedFileInfo struct { FileDateLS uint32 } -// provisionDataDir crée le dossier de données et un config.env de départ. -// Extrait de cmdInstall pour être réutilisable au premier lancement, sans -// embarquer l'installation du binaire ni les messages de console. -func provisionDataDir() error { - ajeanHome := AjeanHome() - // Plus de dossier SKILLS : les skills ont ete fondus dans la memoire. - // On ne le CREE plus, mais on continue de le LIRE une fois au demarrage - // pour reprendre ceux d'une ancienne installation (migrateSkillsToMemory). - for _, d := range []string{ajeanHome, filepath.Join(ajeanHome, "configs")} { - if err := os.MkdirAll(d, 0o755); err != nil { - return err - } - } - conf := filepath.Join(ajeanHome, "config.env") - if _, err := os.Stat(conf); os.IsNotExist(err) { - return os.WriteFile(conf, []byte(configTemplate), 0o644) - } - return nil -} - // ensureShortcuts garantit qu'un raccourci « AJEAN » existe dans le menu // Démarrer et sur le Bureau, et qu'il pointe sur le bon exécutable. Appelée à // CHAQUE lancement du fichier téléchargé, pas seulement à l'installation : diff --git a/internal/ajean/sys_install_darwin.go b/internal/ajean/sys_install_darwin.go index 3931718..96953a0 100644 --- a/internal/ajean/sys_install_darwin.go +++ b/internal/ajean/sys_install_darwin.go @@ -17,36 +17,11 @@ import ( // sur « systemctl: command not found » / « /etc/default/ajean », cf. issue #4). // Implémentation prudente ; à valider sur une machine Apple. -const configTemplate = `# Configuration AJEAN — édite-moi puis: ajean restart -# Le service launchd lit ce fichier et lance ton binaire llama.cpp. - -# Chemins -BIN="/usr/local/bin/llama-server" -MODEL="/Users/USER/models/your-model.gguf" - -# Serveur -PORT="8080" -HOST="0.0.0.0" - -# Inference -CTX="32768" -BATCH="2048" -UBATCH="512" -NGL="999" - -# Args supplémentaires passés à llama-server -EXTRA_ARGS="" -` // launchdPlistTemplate — champs formatés : Label, chemin du binaire, UserName, -// WorkingDirectory, AJEAN_HOME, AJEAN_HOME, StandardOutPath, StandardErrorPath. +// WorkingDirectory, AJEAN_HOME, StandardOutPath, StandardErrorPath. // KeepAlive/SuccessfulExit=false ≈ Restart=on-failure ; RunAtLoad relance au // boot une fois chargé avec `-w`. -// -// Le chemin du binaire est injecté et non codé en dur : il a changé avec le -// renommage, et un plist figé sur l'ancien chemin donnerait un daemon qui refuse -// de démarrer. Les deux variables d'environnement sont posées car le code lit -// encore AJEAN_HOME en héritage. const launchdPlistTemplate = ` @@ -60,7 +35,7 @@ const launchdPlistTemplate = ` UserName%s WorkingDirectory%s EnvironmentVariables - AJEAN_HOME%sAJEAN_HOME%s + AJEAN_HOME%s RunAtLoad KeepAlive SuccessfulExit @@ -87,14 +62,6 @@ func cmdInstall(args []string) error { if err != nil { return fmt.Errorf("utilisateur '%s' introuvable: %w", targetUser, err) } - // Migration de l'agencement ajean -> ajean AVANT de résoudre les chemins : - // `install` est root et délibéré, c'est le bon moment. Ne fait rien si la - // machine est déjà en ajean, ou si l'utilisateur a imposé un chemin. - if err := migrateLayout(defaultLayoutPlan()); err != nil { - return err - } - resetResolvedHome() // le dossier a pu changer à l'instant - ajeanHome := defaultAjeanHome() if v := os.Getenv("AJEAN_HOME"); v != "" { ajeanHome = v @@ -105,27 +72,12 @@ func cmdInstall(args []string) error { fmt.Printf(" AJEAN_HOME = %s\n", ajeanHome) fmt.Printf(" service = %s (launchd)\n", svc) - // 1. Répertoires - // Plus de dossier SKILLS : les skills ont ete fondus dans la memoire. - // On ne le CREE plus, mais on continue de le LIRE une fois au demarrage - // pour reprendre ceux d'une ancienne installation (migrateSkillsToMemory). - for _, d := range []string{ajeanHome, filepath.Join(ajeanHome, "configs")} { - if err := os.MkdirAll(d, 0o755); err != nil { - return err - } + // 1. Arborescence + configuration de départ + if err := provisionDataDir(); err != nil { + return err } - // 2. config.env si absent - 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 { - return err - } - fmt.Printf(" %s écrit %s\n", green("✓"), conf) - } - - // 3. Symlink idempotent vers /usr/local/bin/ajean — même garde-fou que Linux + // 2. Symlink idempotent vers /usr/local/bin/ajean — même garde-fou que Linux // (issue #5 : ne pas se lier sur soi-même si on tourne déjà depuis la cible). self, err := os.Executable() if err != nil { @@ -154,38 +106,24 @@ func cmdInstall(args []string) error { fmt.Printf(" %s %s -> %s\n", green("✓"), target, self) } - // 3 bis. Alias hérité : `ajean` doit rester tapable (voir sys_paths.go). - 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. /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 $AJEAN_HOME. + // 3. /etc/default/ajean pour que les invocations CLI résolvent AJEAN_HOME. _ = os.MkdirAll("/etc/default", 0o755) - defaults := fmt.Sprintf("# Generated by ajean install — racine des configs/memoire/presets\nAJEAN_HOME=%s\nJEAN_HOME=%s\n", ajeanHome, ajeanHome) + defaults := fmt.Sprintf("# Généré par ajean install — racine des données\nAJEAN_HOME=%s\n", ajeanHome) if err := os.WriteFile("/etc/default/ajean", []byte(defaults), 0o644); err != nil { return err } fmt.Printf(" %s /etc/default/ajean\n", green("✓")) - // 5. LaunchDaemon plist (le log va sous AJEAN_HOME, accessible au user cible) + // 4. LaunchDaemon plist (le log va sous AJEAN_HOME, accessible au user cible) logPath := filepath.Join(ajeanHome, svc+".log") - plist := fmt.Sprintf(launchdPlistTemplate, launchdLabel(svc), target, targetUser, ajeanHome, ajeanHome, ajeanHome, logPath, logPath) + plist := fmt.Sprintf(launchdPlistTemplate, launchdLabel(svc), target, targetUser, ajeanHome, ajeanHome, logPath, logPath) plistPath := launchdPlistPath(svc) if err := os.WriteFile(plistPath, []byte(plist), 0o644); err != nil { return err } fmt.Printf(" %s %s\n", green("✓"), plistPath) - // 6. chown AJEAN_HOME au user cible + // 5. chown AJEAN_HOME au user cible chown(ajeanHome, u) fmt.Println() @@ -208,13 +146,12 @@ func cmdUninstall(args []string) error { launchdPlistPath(svc), "/etc/default/ajean", installedExePath(), - legacyExePath(), } { if err := os.Remove(p); err == nil { fmt.Printf(" %s %s\n", green("✓"), p) } } - // On ne supprime jamais AJEAN_HOME automatiquement (modèles, configs). + // On ne supprime jamais AJEAN_HOME automatiquement (modèles, presets). fmt.Println(dim("(données utilisateur conservées — supprime $AJEAN_HOME manuellement si tu veux purger)")) fmt.Println(green("[ok]") + " désinstallé") return nil diff --git a/internal/ajean/sys_install_linux.go b/internal/ajean/sys_install_linux.go index b782e5a..022f2f5 100644 --- a/internal/ajean/sys_install_linux.go +++ b/internal/ajean/sys_install_linux.go @@ -11,26 +11,6 @@ import ( "strings" ) -const configTemplate = `# Configuration AJEAN — édite-moi puis: ajean restart -# Le service systemd lit ce fichier et lance ton binaire llama.cpp. - -# Chemins -BIN="/usr/local/bin/llama-server" -MODEL="/home/USER/models/your-model.gguf" - -# Serveur -PORT="8080" -HOST="0.0.0.0" - -# Inference -CTX="32768" -BATCH="2048" -UBATCH="512" -NGL="999" - -# Args supplémentaires passés à llama-server -EXTRA_ARGS="" -` const sudoersTemplate = `# Allow %s to manage the %s systemd unit without a password (installed by ajean). %s ALL=(root) NOPASSWD: /bin/systemctl start %s, /bin/systemctl stop %s, /bin/systemctl restart %s, /bin/systemctl enable %s, /bin/systemctl disable %s @@ -76,14 +56,6 @@ func cmdInstall(args []string) error { if err != nil { return fmt.Errorf("utilisateur '%s' introuvable: %w", targetUser, err) } - // Migration de l'agencement ajean -> ajean AVANT de résoudre les chemins : - // `install` est root et délibéré, c'est le bon moment. Ne fait rien si la - // machine est déjà en ajean, ou si l'utilisateur a imposé un chemin. - if err := migrateLayout(defaultLayoutPlan()); err != nil { - return err - } - resetResolvedHome() // le dossier a pu changer à l'instant - ajeanHome := defaultAjeanHome() if v := os.Getenv("AJEAN_HOME"); v != "" { ajeanHome = v @@ -94,27 +66,13 @@ func cmdInstall(args []string) error { fmt.Printf(" AJEAN_HOME = %s\n", ajeanHome) fmt.Printf(" service = %s\n", svc) - // 1. Create directories - // Plus de dossier SKILLS : les skills ont ete fondus dans la memoire. - // On ne le CREE plus, mais on continue de le LIRE une fois au demarrage - // pour reprendre ceux d'une ancienne installation (migrateSkillsToMemory). - for _, d := range []string{ajeanHome, filepath.Join(ajeanHome, "configs")} { - if err := os.MkdirAll(d, 0o755); err != nil { - return err - } + // 1. Arborescence + configuration de départ + if err := provisionDataDir(); err != nil { + return err } + fmt.Printf(" %s arborescence prête\n", green("✓")) - // 2. Drop a config.env if none exists - 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 { - return err - } - fmt.Printf(" %s écrit %s\n", green("✓"), conf) - } - - // 3. Symlink current binary to /usr/local/bin/ajean + // 2. Symlink current binary to /usr/local/bin/ajean self, err := os.Executable() if err != nil { return err @@ -145,27 +103,8 @@ func cmdInstall(args []string) error { fmt.Printf(" %s %s -> %s\n", green("✓"), target, self) } - // 3 bis. Alias hérité : `ajean` doit rester tapable. Des utilisateurs ont des - // alias shell, des cron, des scripts de déploiement et des tutos qui appellent - // « ajean » — 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 $AJEAN_HOME. L'ancien /etc/default/ajean 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) + // 3. /etc/default/ajean, pour que les invocations root résolvent AJEAN_HOME. + defaults := fmt.Sprintf("# Généré par ajean install — racine des données\nAJEAN_HOME=%s\n", ajeanHome) if err := os.WriteFile("/etc/default/ajean", []byte(defaults), 0o644); err != nil { return err } diff --git a/internal/ajean/sys_install_windows.go b/internal/ajean/sys_install_windows.go index 93a186a..a216c0a 100644 --- a/internal/ajean/sys_install_windows.go +++ b/internal/ajean/sys_install_windows.go @@ -16,26 +16,6 @@ import ( // service itself is managed by the PID-file supervisor in sys_service_windows.go // (ajean start / stop / status), which needs no admin rights. -const configTemplate = `# Configuration AJEAN — édite-moi puis: ajean restart -# ajean serve lit ce fichier et lance ton binaire llama.cpp. - -# Chemins (utilise des chemins Windows ; les antislashs ou slashs marchent) -BIN="C:/llama.cpp/build/bin/Release/llama-server.exe" -MODEL="C:/models/your-model.gguf" - -# Serveur -PORT="8080" -HOST="127.0.0.1" - -# Inference -CTX="32768" -BATCH="2048" -UBATCH="512" -NGL="999" - -# Args supplémentaires passés à llama-server -EXTRA_ARGS="" -` func cmdInstall(args []string) error { ajeanHome := AjeanHome() @@ -44,41 +24,31 @@ func cmdInstall(args []string) error { fmt.Printf(" AJEAN_HOME = %s\n", ajeanHome) fmt.Printf(" service = %s\n", serviceName()) - // 1-2. Dossiers + config.env de départ (partagé avec le premier lancement de - // l'app, voir provisionDataDir dans sys_firstrun_windows.go). - conf := filepath.Join(ajeanHome, "config.env") - _, statErr := os.Stat(conf) - alreadyThere := statErr == nil + // 1. Arborescence + configuration de départ (partagé avec le premier + // lancement de l'app, voir sys_datadir.go). if err := provisionDataDir(); err != nil { return err } - if alreadyThere { - fmt.Printf(" %s config.env déjà présent — inchangé\n", dim("•")) - } else { - fmt.Printf(" %s écrit %s\n", green("✓"), conf) - } + fmt.Printf(" %s arborescence prête\n", green("✓")) - // 3. Install the binary into AJEAN_HOME\bin and put that dir on the user PATH, - // so `ajean` is callable from any shell (this is the Windows analogue of the - // /usr/local/bin symlink the Unix installer creates). - binDir := filepath.Join(ajeanHome, "bin") - if err := os.MkdirAll(binDir, 0o755); err != nil { - return err - } + // 2. Le binaire va dans AJEAN_HOME\bin, ajouté au PATH utilisateur, pour que + // `ajean` soit appelable depuis n'importe quel shell (pendant Windows du + // lien /usr/local/bin créé par l'installateur Unix). + dir := binDir() onPath := false - if dst, err := installSelf(binDir); err != nil { + if dst, err := installSelf(dir); err != nil { fmt.Printf(" %s copie du binaire impossible (%v) — ajoute-le au PATH à la main\n", dim("•"), err) } else { fmt.Printf(" %s binaire installé %s\n", green("✓"), dst) - added, err := addToUserPath(binDir) + added, err := addToUserPath(dir) switch { case err != nil: fmt.Printf(" %s mise à jour du PATH impossible (%v)\n", dim("•"), err) case added: - fmt.Printf(" %s %s ajouté au PATH utilisateur\n", green("✓"), binDir) + fmt.Printf(" %s %s ajouté au PATH utilisateur\n", green("✓"), dir) onPath = true default: - fmt.Printf(" %s %s déjà dans le PATH\n", dim("•"), binDir) + fmt.Printf(" %s %s déjà dans le PATH\n", dim("•"), dir) onPath = true } } @@ -97,10 +67,9 @@ func cmdInstall(args []string) error { return nil } -// 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. Une copie « ajean.exe » est posée à côté pour que -// l'ancienne commande reste tapable (voir installLegacyAlias). +// installSelf copie l'exécutable en cours dans binDir sous le nom ajean.exe et +// renvoie le chemin de destination. Sans effet si l'exe tourne déjà depuis là +// (ré-installation). // // 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 @@ -115,7 +84,6 @@ func installSelf(binDir string) (string, error) { src, _ = filepath.EvalSymlinks(src) 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 { @@ -126,26 +94,9 @@ func installSelf(binDir string) (string, error) { if err := replaceExe(src, dst); err != nil { return "", err } - installLegacyAlias(binDir, dst) return dst, nil } -// installLegacyAlias pose une copie « ajean.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, "ajean.exe") - if strings.EqualFold(src, alias) { - return - } - _ = replaceExe(src, alias) -} - // replaceExe copie src vers dst, y compris quand dst est un exécutable EN COURS. // // Windows refuse d'écraser un .exe en cours d'exécution, mais accepte de le @@ -253,9 +204,9 @@ func cmdUninstall(args []string) error { _ = svcStop(false) // Pull AJEAN_HOME\bin off the user PATH (best-effort). - binDir := filepath.Join(AjeanHome(), "bin") - if removed, err := removeFromUserPath(binDir); err == nil && removed { - fmt.Printf(" %s %s retiré du PATH utilisateur\n", green("✓"), binDir) + dir := binDir() + if removed, err := removeFromUserPath(dir); err == nil && removed { + fmt.Printf(" %s %s retiré du PATH utilisateur\n", green("✓"), dir) } // Raccourcis posés à l'installation (menu Démarrer + Bureau).