mise 環境で npm install -g した CLI が別 repo から見えない問題と shim による解決
mise 環境で npm install -g した CLI が別 repo から見えない問題と shim による解決
TL;DR
mise(asdf 系)で Node を管理している環境では、
npm install -g . した CLI は install 時の Node version に閉じる。
別 version の repo からは見えない。
~/.local/bin に shim を置くことで、どの repo からでも使えるようにした。
はじめに
Node 製 CLI(ここでは takt)を少し改造しながら、別リポジトリで使いたい。
npm install -g . や npm link ではうまくいかなかったので、対策を備忘録として残す。
この記事は mise や asdf 系ツールで Node を管理しつつ、
ローカル開発中の CLI を複数 repo で使いたい人向け。
問題の構造
mise は .tool-versions に基づいて、ディレクトリごとに使う Node を切り替える。
切り替えの仕組みは PATH の差し替えである。
# RepoA(node@22)に cd した瞬間の PATH
~/.local/share/mise/installs/node/22.x.x/bin ← 先頭に来る
...
npm install -g . や npm link で CLI を入れると、その CLI は 実行時の Node version 配下 に配置される。
~/.local/share/mise/installs/node/25.2.1/lib/node_modules/takt
~/.local/share/mise/installs/node/25.2.1/bin/takt
つまり、node@25 で install した CLI は node@22 の repo からは見えない。
.tool-versions がある repo に入った瞬間、その Node が PATH の先頭に来るからである。
前提構成
- Node 管理: mise
- RepoA:
.tool-versionsで node@22 - CLI 開発 repo(takt): node@25
- シェル: fish
失敗事例:npm link / npm install -g .
takt の開発ディレクトリで以下を実行する:
which node
# ~/.local/share/mise/installs/node/25.2.1/bin/node
npm run build
npm link
npm link は node@25.2.1 の global に takt の symlink を貼る。
よって node@22 の RepoA からはそもそも見えない。
偶然 RepoA が node@25.2.1 だった場合にのみ使える、という脆い構成になっている。
npm install -g . でも同じ?
RepoA から takt が見えない点は同じである。
ただし npm link(symlink)と違い、npm install -g . はビルド成果物を配置するため、CLI としての安定性・再現性が高い。
よって以後は npm link ではなく npm install -g . を使用する。
mise tool に落とし込めばいいのでは?
- mise の npm tool は registry 前提
-
file:/ ローカルパスは非対応
解決策: ~/.local/bin に shim を置く
問題の本質は、npm install -g . した Node version と、
実行時の repo の Node version が異なると takt の名前解決が失敗する点にある。
この差を吸収するために、~/.local/bin に shim を置く。
手順
- takt を node@25 の npm global に install
cd your/local/path/to/takt
npm run build
npm install -g . # node@25 側に配布
-
~/.local/bin/taktを 1 回だけ作る
cat << 'EOF' > ~/.local/bin/takt
#!/usr/bin/env bash
exec mise x node@25 -- takt "$@"
EOF
chmod +x ~/.local/bin/takt
- 動作確認
which takt
# ~/.local/bin/takt
takt --version
mise x node@25 -- により、shim は常に node@25 のコンテキストで takt を実行する。
repo 側の .tool-versions に関係なく動く。
どの takt が実行されるか
挙動は PATH 順で決まる。
~/.local/bin/takt より前に同名の takt が PATH 上にあれば、そちらが優先される。
前段に同名コマンドがなければ、この shim が使われる。
- repo の runtime(
.tool-versions等)は変更不要 - 改造 →
npm install -g .→ 即反映 - mise tool と同じ使用感
- repo に artifact が混入することはない
abbr / alias ではダメ?
fish で以下を定義すれば、見た目上は同じ挙動になる。
abbr -a takt "mise x node@25 -- takt"
- abbr / alias でも動く
- ただし shell 限定
- editor / script /
whichでは認識されない
CLI として成立させるなら、PATH 上に実体が存在する shim が安全。
まとめ
- npm global の CLI は Node version ごとに分離される
- mise は cd 時に PATH を差し替えるため、別 version の global CLI は見えなくなる
-
~/.local/binに shim を置き、mise x node@25 -- takt "$@"で実行時の Node を固定する - どの
taktが実行されるかは PATH 順で決まる
Appendix: markdownlint(mise tool)が動く理由
npm:markdownlint-cli は mise の tool(shim 管理)である。
- PATH には常に
~/.local/share/mise/shims - shim が適切な Node を選んで実行する
Node runtime をまたいで使える。
管理レイヤがそもそも違う。
| CLI | 管理方式 | Node version 依存 |
|---|---|---|
| takt | npm global | あり(install 時の version に閉じる) |
| markdownlint | mise tool(shim) | なし(shim が解決) |
Discussion