mizulba
npm/CLI パッケージの配布設計と公開の落とし穴
npm CLIが自分のpackage.jsonルートを実行時に特定するには name フィールド一致まで上方向探索する
11日前
公開npmパッケージのCLIが、同梱した非バンドルアセット(テンプレート・スキーマ・別ツールへパスで渡す未コンパイルのソースファイルなど)を実行時に見つける必要がある場合、import.meta.url(CommonJSなら__dirname)のディレクトリから親方向へpackage.jsonを探索し、そのnameフィールドが自パッケージ名と一致するディレクトリを「自分のパッケージルート」として確定する、という汎用パターンが使える。
判断基準
- この方法は、ソースから直接実行する場合(開発時のトランスパイラ実行等)と、npmインストール後にビルド済みファイルから実行される場合(node_modules配下にネストされる場合も含む)の両方で、同一ロジックのまま正しく解決できる。固定の相対パス段数(例:「常に3階層上」)に頼る実装は、実行元がソースかビルド成果物かで階層が変わるため壊れやすい。
- package.jsonが見つかってもnameが自パッケージと一致しない場合は探索を継続する。ネストされたnode_modules内で別パッケージのpackage.jsonに先に当たる可能性があるため、nameの一致確認は必須。
- ルートまで遡っても見つからない場合はエラーとして扱う(無限ループやundefined参照を防ぐ)。
関連する設計判断: アセットをビルド成果物へコピーするか、ソースのまま参照するか
同梱アセットをビルド時にビルド成果物側へコピーして参照する方法(成果物を自己完結させる)に対し、ビルドせずソースディレクトリに置いたまま実行時にパッケージルートからの相対パスで参照する方法もある。後者が有利なのは、そのアセットが別ツール(バンドラー等)へ「ファイルパスとして渡すだけで、自分ではimportしない」入力である場合——コピーを経由すると差分管理の手間が増えるだけで、実行時にパッケージルートさえ分かれば十分な場面では、探索ロジックの方が単純になる。
検証
ビルドしたエントリーファイルをリポジトリ外の空ディレクトリへ実際にインストールし、そこから直接実行してアセット参照が解決できるかを確認する(tarball経由のインストールが最も実運用に近い)。
プログラマティックなバンドラー/コンパイラAPIを使う公開パッケージは、その言語ツールチェーン自体もdependenciesに含める
11日前
一部のバンドラー・レンダラーライブラリは、内部で別言語のコンパイラのJS API(プログラム的なコンパイラホスト機能)を実行時に呼び出す設計になっていることがある。このライブラリ自身のpackage.jsonは、そのコンパイラを直接の依存として宣言していないことがある(利用側の環境に既にあるものを使う前提の設計)。
誤解しやすい条件
- 「そのツールチェーンは型チェック・ビルド時にしか要らない」という直感から、自作の公開npmパッケージでこの種のライブラリを使う際にツールチェーンをdevDependenciesに置いてしまうと、devDependenciesはパッケージ利用者のインストール時には入らないため、エンドユーザー環境で実行時にバンドラーがそのツールチェーンを解決できずエラーで壊れる。
- ローカル開発中はリポジトリ自身のdevDependenciesとしてツールチェーンがインストールされているため症状が出ず、パッケージ化(tarball生成)した上で別プロジェクトへインストールして初めて再現する。ローカルのテスト実行が通ることは、この種の依存漏れの検出にならない。
- 同様に、公開パッケージがプラグイン・シナリオ等の動的ロードに特定のトランスパイラのランタイムAPIを使う設計の場合、そのトランスパイラ自体もdependenciesに含める必要がある。
判断基準
- 依存先ライブラリが「実行時に別の言語ツールチェーンをプログラム的に呼び出す」設計かどうかを、そのライブラリのドキュメント・ソースで確認する(対象ツールチェーンパッケージの動的解決をしていないか)。該当する場合、そのツールチェーンを自パッケージのdependenciesへ昇格する。
- 対象ツールチェーンのメジャーバージョン互換性にも注意する。ネイティブ実装への移行など破壊的変更があるツールでは、意図しないメジャーバージョンが解決されるとJS API自体が失われ壊れることがあるため、バージョン範囲を固定気味にする。
検証
パッケージ化した成果物をリポジトリ外の新規ディレクトリへインストールし、そこから実際にCLI/ライブラリを実行して当該機能(バンドル・レンダリング等)が動くことを確認する。devDependenciesは新規インストールに含まれないため、これが依存漏れを検出する最も確実な方法になる。
AI エージェントに使わせる CLI は認識経路を多層で同梱する — install-skill 自己設置・JSON Schema 同梱・語彙付き --help
11日前
新しい CLI ツールを npm 等で配布しても、AI エージェント(Claude Code / Codex など)は学習データにないツールの存在と使い方を知らない。「エージェントに指示すれば使ってくれる」状態にするには、認識経路をツール自身に多層で同梱する。確実な順に:
多層の設計(実装パターン)
- スキルの自己設置コマンド(最強): CLI に install-skill 等のサブコマンドを持たせ、同梱した SKILL.md(使い方・入力ファイルの書き方の薄いガイド。スクリプト非同梱)をエージェントのスキル検出ディレクトリ(.claude/skills/ と .agents/skills/ の両方、--user でユーザーレベル)へ書き込む。@playwright/cli の install-skill が先行例。スキルの版が CLI の版と常に一致し、プラグイン等の別配布経路を管理しなくて済む。
- 入力ファイルの JSON Schema 同梱: 宣言的な設定/シナリオを受け取るツールならスキーマをパッケージに同梱し、scaffold(init)が生成するファイルに $schema を書き込む。AI はスキーマを読めば語彙を正確に書け、人間のエディタ補完・検証にも効く。スキーマとランタイムバリデーションの乖離は空通しになるので、同じ入力集合に対する accept/reject の同値性をテストで固定する(ajv 等でスキーマを実行して照合)。
- 語彙付き --help: AI は未知の CLI でまず help を叩くので、サブコマンド一覧だけでなく入力ファイルの語彙一覧と最小例まで help に載せ、help 自体を仕様書にする。
判断基準
- スキルを別のプラグイン・マーケットプレイスで配る案は、ツール本体との版不一致と二重の配布管理を生む。ツールが自分で設置する方が自己完結。
- 重い依存(ブラウザ・動画ライブラリ等で GB 級)を持つツールは、エージェントのプラグイン/スキルにコード同梱しない(プラグインキャッシュはバージョン別ディレクトリで再インストールが走る)。コードは npm 等のパッケージマネージャに任せ、スキルは「使い方の薄い層」に限定する。
- TS 等のコードエスケープハッチを提供する場合、例示が自パッケージの import を要求しないか確認する。npx 実行のみの環境では利用側 node_modules にパッケージが存在しないため、型補助は任意層(依存に持つ場合の identity ヘルパー)に分離する。
検証
スキル未設置の素のエージェントに「このツールで○○して」と指示し、help と $schema だけから正しい入力を一発で書けるかを見る。書けなければ help の語彙説明かスキーマの制約記述が不足している。
npm パッケージ名の空き確認は完全一致 404 だけでは不十分 — 区切り文字を除いた類似名ルールで publish 時に 403 になる
11日前
npm に新規パッケージを公開する前の名前選定で、レジストリへの問い合わせ(該当名の URL が 404)だけを空き確認にすると、publish 時に初めて 403 で拒否されることがある。npm はタイポスクワッティング防止のため、区切り文字(ハイフン・ピリオド・アンダースコア)を除いて同名になる既存パッケージがあると新規公開を拒否する(例: demo-reel が既存だと demoreel は公開不可。逆方向も同様)。エラーは「Package name too similar to existing package」とスコープ付き名への変更提案を含む E403。
判断基準・手順
- 空き確認では候補名とその区切り文字変種(ハイフンあり/なしの両方)をレジストリに問い合わせる。両方が 404 の候補だけを採用候補に残す。
- このルールは防御にも使える: どちらかの変種を取得すれば、もう一方の変種を第三者が取得することもできなくなる。
- 衝突した場合の選択肢は (1) スコープ付き(@user/name。bin 名は短名のままにできるが npx 初回コマンドが長い)、(2) 別名への改名。公開前なら改名の方が将来の互換負債がない。公開前の最終タイミングで、利用側契約(設定ディレクトリ名・スキル名等、パッケージ名由来のもの)ごと見直す。
- 名前は現在の機能でなく拡張予定の外延で選ぶ。出力形態が増える予定(例: 動画→スクショ・検証)があるなら、特定出力に寄った名より入力(シナリオ等)軸の汎用名の方が崩れにくい。
検証
公開前に npm publish --dry-run ではこのルールは検出されない(サーバー側判定のため)。確実には候補名と全区切り変種のレジストリ URL を事前に確認するしかなく、最終確認は実際の publish になる。
npx はキャッシュ済みの旧バージョンを実行し続ける — 「機能がない」と結論する前に実行中の実バージョンを確認する
10日前
npx で引数なしにパッケージを実行すると(npx pkg 形式)、初回に解決したバージョンがキャッシュディレクトリ(npm の _npx 配下)に保存され、以後はレジストリへ最新版を確認せずキャッシュ済みバイナリを使い続けることがある。パッケージが新機能をリリースしていても手元の npx 実行には反映されず、「この設定キーは存在しない」「このオプションは未実装」に見える。
誤解しやすい条件
- lockfile 管理下の依存と違い、npx の一時実行はプロジェクトの package.json に現れないため、バージョン固定の存在自体に気づきにくい。
- ドキュメントやスキーマは最新版を示すので、「ドキュメントが先行している」「機能が未リリース」と誤読しやすい。実際は実行側が古い。
- ツールの作者自身でも起きる(リリース済み機能を忘れて回避策を実装してしまう型)。
確認手順
- npm view でレジストリの latest バージョンを確認する。
- 実行されている実バージョンを確認する(そのツールの version 表示、またはキャッシュディレクトリ配下の package.json を直接見る)。
- 両者がずれていればキャッシュ起因と確定できる。
回避
- バージョンを明示して実行する(npx -y でパッケージ名に @latest や具体バージョンを付ける)。挙動が決定的になるので、スクリプトや手順書に書く npx コマンドは常にバージョン明示が安全。
- もしくは npx キャッシュを削除して再解決させる。
一般化
「機能が存在しない」と結論する前に、参照しているドキュメントのバージョンと実行中の実バージョンの一致を先に確認する。lockfile・キャッシュ・グローバルインストールなど、実行系がバージョンを固定する層はツールごとに違う場所にある。