Un développeur Java travaille souvent sur plusieurs stacks à la fois : un projet legacy en Java 8, un service en 17, un nouveau projet en 25. Maven ici, Gradle là. Node pour une doc, Python pour MkDocs. Et autour, des outils : aws, gh, kubectl, helm.

Chaque SDK et chaque outil arrive avec son installeur. Peu à peu, le poste se remplit : des versions globales qui se mélangent, des sudo, un .zshrc rempli par les installeurs, une variable AWS restée en global. Ma question : comment avoir la bonne version de chaque SDK et de chaque outil dans chaque projet, sans rien installer en root, avec un seul outil, et un terminal qui reste rapide ?

Voici mon poste de dev sur macOS, construit sur un OS propre que je veux garder propre.

Dans le dossier floci, java -version donne Java 25 ; après cd ../quickfixj, il donne Java 8. La commande mise ls montre que Java et Maven viennent du fichier mise.local.toml du projet, et Node de la configuration du poste.

Mes critères pour un poste de dev

  1. Rien en root. Pas de sudo pour installer ou mettre à jour un outil de dev. Tout vit dans mon dossier utilisateur.
  2. Un seul outil pour les SDK et les outils. Un seul endroit, une seule commande, un seul fichier à lire.
  3. La bonne version au cd. J’entre dans un projet, j’ai sa version de Java et de Maven, sans y penser.
  4. L’isolation. Chaque projet a ses versions. Un environnement particulier (par exemple un émulateur AWS) reste dans un onglet.
  5. Un terminal qui reste rapide, même avec des complétions pour tous les outils.

La suite répond à ces critères, dans l’ordre.

mise, en deux mots

mise se présente comme « dev tools, env vars, and tasks in one CLI » : il installe les outils d’un projet, règle ses variables d’environnement et lance ses tâches. Ici, je m’en sers surtout pour les outils. C’est un seul binaire, sous licence MIT, qui marche aussi sous Linux et Windows.

L’installation tient en une commande, sans sudo :

curl -fsSL https://mise.run | sh

Le script vérifie la somme SHA-256 du binaire et le place dans ~/.local/bin/mise. Il ne modifie pas ~/.zshrc : il affiche la ligne à ajouter. Je l’ajoute moi-même :

eval "$("$HOME/.local/bin/mise" activate zsh)"

Dans un nouvel onglet, mise doctor confirme :

activated: yes
...
No problems found

mise range tout dans mon dossier : les outils dans ~/.local/share/mise/installs/<outil>/<version>, la configuration dans ~/.config/mise/config.toml.

Un détail pour IntelliJ : une application lancée depuis le Dock lit l’environnement du shell de connexion, pas celui de .zshrc. La doc de mise conseille d’ajouter les « shims » dans ~/.zprofile, pour que ces applications trouvent aussi les outils :

eval "$("$HOME/.local/bin/mise" activate zsh --shims)"

Tous les SDK et outils en deux commandes

D’abord les langages, avec leur version LTS quand elle existe :

mise use -g java@temurin-25 node@lts maven@3.10 gradle@9 python@3.15

Puis les outils en ligne de commande :

mise use -g uv starship gh aws-cli helm github:floci-io/floci-cli

-g veut dire « global » : ce sont les versions par défaut du poste. La plupart viennent du registre de mise (plus de 1 000 outils). Un outil absent du registre s’installe depuis ses versions GitHub, comme ici github:floci-io/floci-cli.

Le résultat est un fichier lisible, qui décrit le poste :

# ~/.config/mise/config.toml
[tools]
java = "temurin-25"
node = "lts"
maven = "3.10"
gradle = "9"
python = "3.15"
uv = "latest"
starship = "latest"
gh = "latest"
aws-cli = { version = "latest", symlink_bins = "true" }
helm = "latest"
"github:floci-io/floci-cli" = "latest"

Une version se lit simplement : temurin-25 prend le dernier Temurin 25, lts la dernière LTS de Node, 3.10 le dernier correctif de Maven 3.10. Sur un nouveau poste, il suffit de copier ce fichier, puis de lancer mise install.

uv, en plus de Python ? mise installe Python lui-même. uv sert à ce qu’on fait avec Python : uvx mkdocs serve lance MkDocs sans rien installer en global, et uv sync gère les dépendances d’un projet.

Une version par projet

Chaque projet peut avoir son propre fichier. Pour quickfixj, un projet encore en Java 8 :

# ~/devs/GitHub/quickfixj/mise.local.toml
[tools]
java = "corretto-8"
maven = "3.10"

mise.local.toml est un fichier personnel : la doc de mise le décrit comme « personal overrides, not committed ». Je l’ajoute à .git/info/exclude du projet, pour que Git ne le voie pas. Pour une version partagée avec l’équipe, on met plutôt un mise.toml dans le dépôt. Le fichier le plus proche du dossier courant gagne, et ce qu’il ne précise pas vient de la configuration globale.

C’est ce que montre la capture du début. Dans floci, java -version donne Java 25 ; dans quickfixj, Java 8. Et mise ls --current dit d’où vient chaque version : Java et Maven du mise.local.toml du projet, Node de ~/.config/mise/config.toml. mise met aussi à jour JAVA_HOME.

Un projet a déjà un fichier de version, comme un .nvmrc ? mise sait le lire, une fois l’option activée :

mise settings add idiomatic_version_file_enable_tools node

Dans un dossier dont le .nvmrc contient 20, j’ai Node 20 ; ailleurs, Node 24 (la LTS). Rien à changer dans le projet.

Un terminal confortable

Le prompt : Starship

Starship affiche ce qui compte dans le dossier courant : la branche Git, la version du projet, Java, Maven, le profil AWS. Il est déjà installé par mise ; il suffit de l’activer à la fin de ~/.zshrc :

eval "$(starship init zsh)"

Sa doc demande une Nerd Font, une police avec des icônes. J’ai pris JetBrains Mono Nerd Font, depuis les versions officielles de Nerd Fonts (la somme SHA-256 de chaque archive y est publiée). Pour rester sans root, la police va dans ~/Library/Fonts : elle n’est installée que pour mon utilisateur. Puis je la choisis dans les réglages du terminal.

Les complétions, sans ralentir le shell

La touche Tab complète les commandes de docker, kubectl, gh… Beaucoup de guides proposent de charger chaque complétion au démarrage :

source <(kubectl completion zsh)

Le programme tourne alors à chaque nouvel onglet. Docker et kubectl documentent une autre méthode : écrire la complétion une fois dans un fichier, dans un dossier que zsh connaît (le fpath). zsh ne la charge qu’au premier Tab.

mkdir -p ~/.zsh/completions
kubectl completion zsh > ~/.zsh/completions/_kubectl
docker completion zsh > ~/.zsh/completions/_docker
gh completion -s zsh > ~/.zsh/completions/_gh

Et dans ~/.zshrc, avant compinit :

fpath=("$HOME/.zsh/completions" $fpath)
autoload -Uz compinit
compinit

J’ai mesuré l’ouverture d’un shell avec les mêmes 10 complétions (docker, kubectl, mise, gh, glab, helm, uv, uvx, Starship, hvm), 10 ouvertures pour chaque cas :

MéthodeOuverture du shell
Aucune complétion93 ms
Fichiers dans le fpath92 ms
source <(… completion zsh)348 ms

Les fichiers ne coûtent rien à l’ouverture. Leur seule limite : ils décrivent l’outil au moment où on les a créés. Après une mise à jour (mise upgrade), je les recrée avec un petit script, ~/.zsh/update-completions.zsh, qui reprend une ligne par outil.

Deux cas à part. La complétion d’aws n’est pas un fichier mais un programme ; la doc AWS donne ces deux lignes pour zsh :

autoload -Uz bashcompinit && bashcompinit
complete -C aws_completer aws

Et un alias garde la complétion de sa commande : alias k=kubectl, puis compdef k=kubectl.

L’historique et quelques alias

macOS règle l’historique à 1 000 lignes (SAVEHIST dans /etc/zshrc). J’en garde plus, sans doublons, partagé entre les onglets :

HISTSIZE=50000
SAVEHIST=50000
setopt HIST_IGNORE_ALL_DUPS SHARE_HISTORY HIST_IGNORE_SPACE

Avec HIST_IGNORE_SPACE, une commande qui commence par une espace n’est pas enregistrée : pratique pour une commande qui contient un secret. Et quelques alias :

alias ls='ls -G'                              # couleurs
alias ll='ls -lrth'
alias lla='ls -lrtha'
alias reload='exec zsh'                       # recharger le shell
alias ports='lsof -nP -iTCP -sTCP:LISTEN'     # qui écoute sur quel port

Isoler un environnement : l’exemple de Floci

Floci est un émulateur AWS local (je le présente dans AWS en local avec Floci). Pour l’utiliser, le client AWS doit viser http://localhost:4566. La solution facile est une variable globale : export AWS_ENDPOINT_URL=http://localhost:4566 dans .zshrc. Mais alors, toutes les commandes AWS du poste partent vers l’émulateur, et on finit par l’oublier.

La doc AWS permet de mettre l’adresse dans un profil nommé de ~/.aws/config :

[profile floci]
region = us-east-1
endpoint_url = http://localhost:4566

Les identifiants de test vont dans ~/.aws/credentials, sous [floci]. Je n’ai pas de profil par défaut. Résultat :

  • dans un onglet normal, aws ne trouve aucun identifiant (« Unable to locate credentials ») : la commande ne part ni vers Floci, ni vers un vrai compte AWS ;
  • avec AWS_PROFILE=floci (ou --profile floci), elle part vers Floci.

Pour ne pas taper AWS_PROFILE à chaque fois, un fichier ~/.zsh/profiles/floci.zsh le règle, et charge la complétion de floci :

export AWS_PROFILE=floci
if command -v floci >/dev/null; then
  source <(floci completion zsh)
fi

La complétion de floci est un script qu’on ne peut pas mettre dans le fpath. Elle reste donc dans ce profil, et ne coûte rien aux autres onglets.

Dans iTerm2, un « profil dynamique » crée un profil « Floci » à partir d’un fichier JSON, déposé dans ~/Library/Application Support/iTerm2/DynamicProfiles/. Il hérite du profil par défaut et affiche un badge :

{
  "Profiles": [
    {
      "Name": "Floci",
      "Guid": "647AE153-1576-4C43-A514-1581774C216B",
      "Dynamic Profile Parent Name": "Default",
      "Badge Text": "Floci"
    }
  ]
}

La doc d’iTerm2 demande un Guid unique : je l’ai créé avec uuidgen. iTerm2 met le nom du profil dans la variable $ITERM_PROFILE (je l’ai constaté, mais sa doc n’en parle pas). ~/.zshrc charge alors le profil Floci dans ces onglets-là. Dans un autre terminal, comme Ghostty ou celui d’IntelliJ, une fonction profile floci fait la même chose.

Un onglet iTerm2 avec le profil Floci : le badge Floci en haut à droite, le prompt Starship affiche floci (us-east-1), aws s3 ls liste les buckets de Floci, et la complétion de la commande floci est active.

Si vous avez déjà des outils installés en root

Déjà un outil installé en root ? Prenons aws, souvent installé avec le paquet officiel dans /usr/local. Installez d’abord la version mise (mise use -g aws-cli), vérifiez avec which -a aws qu’elle passe devant, puis désinstallez l’ancienne en suivant la page Uninstall the AWS CLI d’AWS. Même idée pour les autres outils : la version mise d’abord, l’ancienne ensuite.

En résumé

CritèreMa réponse
Rien en rootmise et tous les outils dans ~/.local, la police dans ~/Library/Fonts
Un seul outilmise use -g pour les SDK et les outils ; un fichier, ~/.config/mise/config.toml
La bonne version au cdmise.local.toml ou mise.toml par projet, et les .nvmrc existants
L’isolationun fichier par projet ; un profil AWS nommé, sans profil par défaut ; un onglet Floci
Un terminal rapideles complétions dans des fichiers : 92 ms au lieu de 348 ms

Le ~/.zshrc complet :

# PATH : pas de doublons, les outils utilisateur d'abord
typeset -U path PATH
path=("$HOME/.local/bin" $path)

# Historique
HISTSIZE=50000
SAVEHIST=50000
setopt HIST_IGNORE_ALL_DUPS SHARE_HISTORY HIST_IGNORE_SPACE

# Complétions : les fichiers de ~/.zsh/completions
fpath=("$HOME/.zsh/completions" $fpath)
autoload -Uz compinit
compinit
autoload -Uz bashcompinit && bashcompinit
complete -C aws_completer aws

# Alias
alias ls='ls -G'
alias ll='ls -lrth'
alias lla='ls -lrtha'
alias reload='exec zsh'
alias ports='lsof -nP -iTCP -sTCP:LISTEN'
alias k=kubectl
alias update-completions='~/.zsh/update-completions.zsh'
compdef k=kubectl

# mise : les versions du dossier courant
eval "$("$HOME/.local/bin/mise" activate zsh)"

# Profils de terminal (~/.zsh/profiles), par exemple : profile floci
profile() {
  local target="$HOME/.zsh/profiles/${1:l}.zsh"
  if [[ -f "$target" ]]; then
    source "$target"
  else
    print -u2 "Unknown profile: $1 (see the files in ~/.zsh/profiles)"
    return 1
  fi
}
case "$ITERM_PROFILE" in
  Floci) profile floci ;;
esac

# Prompt (à garder à la fin)
eval "$(starship init zsh)"

Testé sur macOS 27 (Apple Silicon) avec zsh 5.9, mise 2026.10, Starship 1.27, iTerm2 et Ghostty, Floci 2.3.

Sources : la documentation de mise (« Getting started », « Configuration », « Editors and IDEs », Java, Node), le manuel de zsh, les pages de complétion de Docker, de kubectl et de l’AWS CLI, le guide AWS sur les endpoints, le guide de Starship et la doc des profils dynamiques d’iTerm2.

Pour aller plus loin