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.

Mes critères pour un poste de dev
- Rien en root. Pas de
sudopour installer ou mettre à jour un outil de dev. Tout vit dans mon dossier utilisateur. - Un seul outil pour les SDK et les outils. Un seul endroit, une seule commande, un seul fichier à lire.
- La bonne version au
cd. J’entre dans un projet, j’ai sa version de Java et de Maven, sans y penser. - L’isolation. Chaque projet a ses versions. Un environnement particulier (par exemple un émulateur AWS) reste dans un onglet.
- 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 servelance MkDocs sans rien installer en global, etuv syncgè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éthode | Ouverture du shell |
|---|---|
| Aucune complétion | 93 ms |
Fichiers dans le fpath | 92 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,
awsne 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.

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 avecwhich -a awsqu’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ère | Ma réponse |
|---|---|
| Rien en root | mise et tous les outils dans ~/.local, la police dans ~/Library/Fonts |
| Un seul outil | mise use -g pour les SDK et les outils ; un fichier, ~/.config/mise/config.toml |
La bonne version au cd | mise.local.toml ou mise.toml par projet, et les .nvmrc existants |
| L’isolation | un fichier par projet ; un profil AWS nommé, sans profil par défaut ; un onglet Floci |
| Un terminal rapide | les 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.