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

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 を置く。

手順

  1. takt を node@25 の npm global に install
cd your/local/path/to/takt
npm run build
npm install -g .   # node@25 側に配布
  1. ~/.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
  1. 動作確認
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