npm 製 CLI を Google ドライブで限定配布する — 自己完結 zip をクラウドストレージで共有するレシピ
独自開発した CLI を、ごく限られた相手にだけ渡したい。公開 npm に出すのは論外だし、AWS CodeArtifact のようなプライベートレジストリを立てて認証を回すのも、相手が数人なら大げさすぎる。かといって、リポジトリをそのまま渡して「各自ビルドして」と言うのも不親切です。
こういう「レジストリを使わない、帯域外(out-of-band)の配布」が必要になる場面は実務で意外と多くあります。社外の協力者に検証版を渡す、特定の顧客に PoC を届ける、エアギャップ環境へ手で持ち込む、といったケースです。本記事では、その最小構成として、npm 製 CLI を 1 つの自己完結 zip に固めて Google ドライブの共有リンクで限定配布する方法を、実際に動くレシピとして組み立てます。題材は、配布チャネルごとのレシピを集めた公開リポジトリ cli-distribution-recipes の Google Drive レシピです。
- Issue: Add recipes/google-drive: distribute the CLI as a self-contained zip via Google Drive
- Pull request: Add Google Drive zip distribution recipe
なお本記事は、署名・公証やサプライチェーン堅牢化まで含めたエンタープライズ向けの本格的な配布パイプラインを扱う記事ではありません。そちらに関心があれば、別記事「npm 製 CLI ツールを特定顧客にセキュアに配る」を参照してください。ここで扱うのは、その対極にある、もっとも軽量で手数の少ない配布パターンです。
このパターンが向く場面と向かない場面
最初に適用範囲をはっきりさせておきます。クラウドストレージ経由の zip 配布が噛み合うのは、次のような条件のときです。
- 受け手が数人から数十人で、レジストリ運用の手間に見合わない
- 受け手の環境に Node.js と npm があり、
npm install -gでローカルの tarball を入れられる - 自動更新やバージョン探索は不要で、「このファイルを入れて」と都度伝えられる
- 配布物の完全性・真正性を、まだ署名で保証する段階にはない(あるいは別途チェックサムで足りる)
逆に、不特定多数へ広く配る、頻繁にバージョンを更新して自動アップデートさせたい、改ざん検知を必須要件にする、といった要求があるなら、このパターンは早晩破綻します。その場合はプライベートレジストリや署名付き配布へ移行すべきで、本記事はあくまでその手前の、軽量な選択肢として読んでください。
全体像 — 自己完結 zip をクラウドストレージで配る
配布の流れは単純です。ビルド側で 1 つの zip を作り、クラウドストレージにアップロードして共有設定をし、受け手がダウンロードして同梱の手順どおりにインストールする。これだけです。
flowchart TD
A[packages/hello-cli] --> B[npm pack]
B --> C[codenote-net-hello-cli-0.1.0.tgz]
D[INSTALL.md] --> E[自己完結 zip]
C --> E
E --> F[Google ドライブへアップロード]
F --> G1[リンク共有: デモ用]
F --> G2[メール制限共有: 本番用]
G1 --> H[ダウンロード: ブラウザで人が取得]
G2 --> H
H --> I[unzip]
I --> J[npm install -g ./*.tgz]
J --> K[codenote-hello を実行して検証]
ここでの設計上の肝は、配布の単位を「リポジトリ」でも「tarball 単体」でもなく、インストール手順まで同梱した自己完結 zip にする点です。受け手はリポジトリを読まなくても、zip を展開して中の INSTALL.md に従うだけでインストールを完了できます。配布チャネル(クラウドストレージ)と、中身(インストール可能な成果物)が完全に分離されているので、同じ zip を別のストレージで配っても手順は変わりません。
配布物の設計 — 自己完結 zip
配布する zip の中身は、たった 2 ファイルです。
codenote-hello-0.1.0.zip
├── codenote-net-hello-cli-0.1.0.tgz # npm pack の出力
└── INSTALL.md # 同梱のインストール手順tarball(.tgz)は npm pack が生成する、npm publish するのと同じ形式のアーカイブです。package.json の files フィールドや .npmignore で公開対象を制御できるため、内部用のスクリプトや設定ファイルを混入させずに、配るべきファイルだけを固められます。受け手は npm install -g ./codenote-net-hello-cli-0.1.0.tgz のように、このローカルの tarball を直接グローバルインストールできます。レジストリを一切経由しないのがポイントです。
もう一方の INSTALL.md は、リポジトリの文脈を知らない受け手でも、これだけ読めば作業が完結するように書きます。要件、インストールコマンド、検証用の期待出力、アンインストール手順を、コピペできる形で並べます。
# Install codenote-hello
## Requirements
- Node.js 22 or newer
- npm
## Install
npm install -g ./*.tgz
## Verify
codenote-hello
# => Ohayou gozaimasu, Konnichiwa, Konbanwa!
## Uninstall
npm uninstall -g @codenote-net/hello-cli期待出力(ここでは Ohayou gozaimasu, Konnichiwa, Konbanwa!)まで書いておくのが地味に重要です。受け手は実行結果と照合するだけで、インストールが正しく成功したかを自分で判断できます。人間が読んでもエージェントが読んでも、同じ手順をたどれば同じ結果に至る、という再現性を担保するための一行です。
この「INSTALL.md だけで作業が完結する」という性質には、もう一つ実利があります。受け手は人間とは限らないからです。コピペできるコマンドと検証用の期待出力が揃っていれば、zip を Claude Code のような AI コーディングエージェントに渡し、「この INSTALL.md に従ってインストールして」と指示するだけで、エージェントが展開・インストール・実行結果の照合まで自律的にこなせます。自然言語の散文ではなく、実行可能なコマンドと合否を判定できる期待出力で手順を書いておくことが、人間とエージェントのどちらにも渡せる配布物にするための条件です。
ビルド — npm pack して zip に固める
zip を手作業で作ると、tarball のバージョン番号を取り違えたり、INSTALL.md を入れ忘れたりします。package.json からバージョンを導出して機械的に組み立てるスクリプトにしておくのが確実です。レシピでは次のような POSIX sh スクリプトを使っています。
#!/usr/bin/env sh
set -eu
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
REPO_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/../.." && pwd)
PACKAGE_DIR="$REPO_ROOT/packages/hello-cli"
DIST_DIR="$SCRIPT_DIR/dist"
PACKAGE_VERSION=$(node -p "require(process.argv[1]).version" "$PACKAGE_DIR/package.json")
ZIP_NAME="codenote-hello-$PACKAGE_VERSION.zip"
rm -rf "$DIST_DIR"
mkdir -p "$DIST_DIR"
PACK_OUTPUT=$(npm pack "$PACKAGE_DIR" --pack-destination "$DIST_DIR" --json)
PACKAGE_TARBALL=$(printf '%s' "$PACK_OUTPUT" | node -e 'let input = ""; process.stdin.on("data", chunk => input += chunk); process.stdin.on("end", () => console.log(JSON.parse(input)[0].filename));')
test -f "$DIST_DIR/$PACKAGE_TARBALL"
cp "$SCRIPT_DIR/INSTALL.md" "$DIST_DIR/INSTALL.md"
(
cd "$DIST_DIR"
zip -q "$ZIP_NAME" "$PACKAGE_TARBALL" INSTALL.md
)
printf '%s\n' "$DIST_DIR/$ZIP_NAME"やっていることは 3 つだけです。npm pack --json で tarball を生成しつつファイル名を機械可読に取得し、INSTALL.md を同じ dist/ にコピーし、両者を zip にまとめる。バージョンは package.json から node -p で読むので、リリースのたびに人間が番号を書き換える必要はありません。スクリプトは最後に生成した zip の絶対パスを標準出力に返すので、呼び出し側でそのまま変数に受けられます。
ZIP_PATH=$(recipes/google-drive/build-distribution-zip.sh)
unzip -l "$ZIP_PATH"unzip -l で中身を確認し、.tgz と INSTALL.md の 2 ファイルだけが入っていることを検証しておきます。なお生成物の dist/ は .gitignore に入れ、ビルド成果物をリポジトリにコミットしないようにしておきます。
アップロードと共有設定 — リンク共有とメール制限共有
zip ができたら Google ドライブにアップロードし、共有設定を決めます。ここが「限定公開」の肝で、用途によって 2 つのモードを使い分けます。
第一に、リンクを知っている全員(Anyone with the link)。これは誰でも再現できるデモ用の設定です。本記事のように公開リポジトリのレシピを読者が手元で試す場合は、この設定にして共有リンクを配ります。リンクはおおむね次の形式になります。
https://drive.google.com/file/d/FILE_ID/view?usp=sharingただし「リンクを知っている全員」は、URL を入手した者には事実上の公開と同義です。リンクが転送・漏洩すれば、誰でも匿名で取得できてしまいます。
第二に、特定の Google アカウントにメールで限定する(メール制限共有)。これが実運用で推奨される設定です。受け手のメールアドレスを指定してアクセス権を付与し、そのリンクを当人にだけ送ります。後述のとおり、この場合の取得には認証済みアカウントが必須になり、匿名ダウンロードはできません。契約終了や離任のときにアクセス権を個別に剥奪できるのも、この方式の利点です。
本番配布で「リンクを知っている全員」を選ぶのは、それが意図したアクセスモデルである場合に限るべきです。迷ったらメール制限共有を既定にしてください。
ダウンロード — ブラウザで人が取得する
受け手側の取得は、ブラウザで人が手作業で行うのが、この配布パターンには最も素直で安全です。共有リンクを開き、ファイルを確認したうえでダウンロードボタンを押すだけです。
ダウンロードを CLI で自動化する手段(サードパーティのダウンローダなど)もありますが、本記事では推奨しません。理由は二つあります。第一に、署名もチェックサムもない zip を取得する以上、どのファイルを受け取るかは人が目で確認したほうが安全だからです。配布対象が少数で更新も稀、というこのパターンの前提では、自動化の利得よりも、人が確認して取る安全側の運用のほうが釣り合います。第二に、メール制限共有との相性です。アクセス権を特定アカウントに絞ったファイルは、その認証済みアカウントの文脈でしか取得できません。ブラウザで当該 Google アカウントにログインしていれば、制限共有のファイルもそのまま確認してダウンロードできます。
逆に言えば、「制限共有にして秘匿しつつ、匿名で自動ダウンロードしたい」という要求は原理的に両立しません。秘匿性を取るならアクセス制御が前提になり、その取得は認証済みの人(またはアカウント)が担う、と整理しておくと混乱しません。
インストールと検証
ダウンロードした zip を展開し、同梱の INSTALL.md に従ってインストールします。
unzip codenote-hello.zip -d codenote-hello
cd codenote-hello
cat INSTALL.md
npm install -g ./*.tgz
codenote-hello最後に期待出力と照合します。
Ohayou gozaimasu, Konnichiwa, Konbanwa!この一行が出れば、レジストリを一切経由せずに、クラウドストレージの共有リンクだけで CLI のインストールが完了したことになります。不要になったら npm uninstall -g @codenote-net/hello-cli で削除します。
限界と注意点
このパターンは手軽さと引き換えに、いくつかの保証を持ちません。配る前に、限界を明示的に受け手へ伝えておくべきです。
- 完全性・真正性の保証がない: zip 自体には署名もチェックサムも付かないため、帯域外で改ざんされても検知できません。最低限の対策として、配布元で
sha256sum codenote-hello-0.1.0.zipのハッシュを別経路(メール本文など)で伝え、受け手に照合してもらう運用を足せます。本格的に真正性を担保するなら、署名する別レシピへ進みます。 - バージョン探索・自動更新がない: 受け手は「どのファイルのどのバージョンを取るか」を都度教えてもらう必要があります。新版を出したら全員に再配布の連絡をする、という手作業が前提です。
- リンク共有は事実上の公開: 「リンクを知っている全員」は、URL が漏れた時点でアクセス制御を失います。秘匿性が要るならメール制限共有にしてください。
- メール制限共有は匿名取得不可: 前述のとおり、制限共有のファイルは認証済みアカウントでしか落とせません。自動化と秘匿性はトレードオフです。
- 手動アップロードはスケールしない: アップロード自体が手作業なので、配布対象が増えると破綻します。これはあくまで配布パターンの最小実装であって、本番の配布パイプラインではありません。
これらは欠陥というより、設計上のトレードオフです。受け手が少数で、更新が稀で、完全性をまだ署名で守る段階にない、という前提が崩れたら、別のチャネルへ移行する合図だと捉えてください。
他のクラウドストレージへの応用
自己完結 zip を作るところまでは、配布先のストレージに依存しません。したがって同じ成果物を、Google ドライブ以外のクラウドストレージにそのまま載せ替えられます。
- Dropbox / OneDrive: 共有リンクの発行と、特定アカウントへの制限共有という考え方は Google ドライブと同じです。受け手はブラウザで開いてダウンロードする、という手順も変わりません。
- Amazon S3 / Cloudflare R2: 有効期限や送信元 IP を絞れる署名付き URL を発行でき、「リンクを知っている全員」よりも細かくアクセスを制御できます。秘匿性を重視する配布で候補になります。
- 共有ファイルサーバ / エアギャップ: ネットワークが隔離された環境では、zip を物理メディアや内部共有で持ち込み、同梱の
INSTALL.mdどおりにオフラインインストールする、という使い方もそのまま通用します。
どのストレージを選んでも、配布の単位が自己完結 zip である限り、受け手の作業手順(展開して tarball を入れる)は変わりません。ストレージはあくまで「どう届けるか」の選択であって、「何を届けるか」とは独立している、というのがこのパターンの設計上の利点です。
まとめ
レジストリを立てるほどではないが、ソースをそのまま渡すのも避けたい。その隙間を埋めるのが、npm 製 CLI を自己完結 zip に固めてクラウドストレージの共有リンクで限定配布する方法でした。要点を整理します。
- 配布の単位を、
npm packの tarball とINSTALL.mdを含む自己完結 zip にする。受け手はリポジトリを読まずにインストールできる。 - ビルドは
package.jsonからバージョンを導出するスクリプトに任せ、手作業のミスを排除する。 - 共有設定は、再現性重視のデモなら「リンクを知っている全員」、実運用ならメール制限共有を既定にする。
- 取得はブラウザで人が手作業で行うのが、この軽量パターンには最も素直で安全。署名のない zip を確認しながら取れ、メール制限共有とも素直に噛み合う。
- このパターンは完全性・真正性の保証、バージョン探索、自動更新を持たず、手動アップロードはスケールしない。前提が崩れたら署名付き配布やレジストリへ移行する。
- 自己完結 zip はストレージ非依存なので、Dropbox・OneDrive・S3 署名付き URL・エアギャップへもそのまま応用できる。
軽量な配布が必要になったとき、まずこの最小構成から始め、要件が育ったら署名やレジストリへ段階的に引き上げる。配布の引き出しを 1 つ増やしておくと、相手と状況に応じて過不足なく届けられるようになります。
以上、npm 製 CLI を Google ドライブなどのクラウドストレージ経由で限定配布する最小レシピをまとめた、現場からお送りしました。