一覧に戻る

ブログ

30 秒で接続するまで: `npx tacticremote` 配布パスの作り方

`npx tacticremote` の裏側にあるエンジニアリングの話。Bun でビルドしたバイナリ、薄い npm ランチャー、QR の自動保存、そしてより速い初回導線までをまとめています。

公開日 タグ: engineering / cli / distribution / onboarding
ブログ

このカテゴリのプロダクト/開発アップデートを確認できます。

カテゴリを見る

私たちの目標はシンプルでした。新しいユーザーが 1 つのコマンドを実行し、QR Code を読み取り、30 秒以内にスマホでライブのターミナルを見られるようにすることです。

この投稿では、その実現方法を説明します。

何が問題だったのか

npx tacticremote が登場する前、サーバーの起動はプラットフォームごとに異なる複数ステップの作業でした。

  • macOS: DMG をダウンロードし、マウントし、App を Applications に移動し、起動し、Gatekeeper とやり取りする。
  • Linux / CI: リポジトリを clone して npm install を実行し、その環境でどうやってサーバーを起動し、動かし続けるかを自分で考える。

リモートの Linux VM や Docker コンテナで作業している開発者にとって、これはかなりつらい流れでした。リポジトリを clone するには git、Node.js、そして npm registry にアクセスできるネットワークが必要です。誰かにそのまま渡せる「これを実行して」で済む 1 行コマンドはありませんでした。

私たちが欲しかったのは、npx tacticremote → QR 読み取り → 完了、という流れです。

なぜそのまま npm install しないのか

サーバーの runtime 依存は多くありませんが、それでも npm install には摩擦があります。現在の runtime package には ws、bonjour-service、qrcode-terminal が含まれています。

npm の依存解決にはそれだけで時間がかかり、その前にサーバーは何も始まりません。企業ネットワークやコンテナ化された CI のような制約環境では、proxy 設定、registry mirror、権限の問題で npm install が失敗することもあります。

さらに重要なのは、CLI パス自体が軽くあるべきだということです。もし npm に公開するパッケージが runtime tree をそのまま抱えていたら、初回体験は依然として「JavaScript を大量にダウンロードしてからサーバーを起動する」になってしまいます。私たちが目指した導線にはまだ届きません。

だからこそ、npm に載せる層は極力薄く保つ必要がありました。

Bun によるビルド

私たちは Bun を使ってサーバーを単一の自己完結型バイナリにコンパイルしています。結果として得られるのは、およそ 20 MB の実行ファイルです。gzip 前の状態でもそのまま実行でき、次のものを内包しています。

  • Bun の実行環境
  • すべての runtime 依存
  • サーバーの完全なソースコード

node_modules はありません。別途 Bun を入れる必要もありません。起動時の依存解決もありません。バイナリを受け取って、そのまま実行するだけです。

GitHub Actions(.github/workflows/build-cli.yml)は、リリースごとに次の 4 ターゲットをビルドしています。

  • bun-darwin-arm64 - Apple Silicon Mac
  • bun-darwin-x64 - Intel Mac
  • bun-linux-x64 - x86_64 Linux
  • bun-linux-arm64 - ARM Linux(Raspberry Pi、AWS Graviton など)

各バイナリは gzip 圧縮されたアセットとして GitHub Release にアップロードされます。

薄いラッパー

npmjs.com に公開されるパッケージはサーバー本体ではありません。bin/tacticremote.js という 124 行のランチャースクリプトです。実際にやっていることは次のとおりです。

  1. process.platform と process.arch を使って、現在の OS と CPU アーキテクチャを判定する
  2. この環境に対応するバイナリ名を解決する
  3. ~/.tacticremote/bin にある現在バージョンのキャッシュ済みバイナリを確認する
  4. キャッシュがなければ、GitHub Releases から gzip 圧縮されたバイナリをダウンロードし、展開してディスクに書き込み、実行権限を付与する
  5. そのバイナリを起動し、stdio を継承しつつ SIGINT や SIGTERM などのシグナルを転送する
const binaryName = `tacticremote-${platform}-${arch}`
const url = `https://github.com/TacticSpaceTech/TacticRemote/releases/download/cli-v${version}/${binaryName}.gz`
const cacheDir = path.join(os.homedir(), '.tacticremote', 'bin')
// download, gunzip, cache locally, chmod +x, spawn

初回実行では、その release アセットをダウンロードしてローカルにキャッシュします。2 回目以降は、現在の npm パッケージ version に対応するローカルバイナリを再利用します。

このパターンは新しいものではありません。esbuild が先駆けで、turbo も使っており、prisma にも似た仕組みがあります。要するに、npm は配布チャネルであって、コンパイル環境ではないということです。依存ツリーではなく、ダウンローダーを届けるために使うのです。

package.json の分離

私たちは 2 つの package manifest を並べて管理しています。

  • package.json - 開発用設定。Jest、ESLint、Prettier などの devDependencies、テストスクリプト、lint スクリプト、本地開発で使う完全な依存を含みます。
  • package.npm.json - 省いた公開用 manifest。bin を tacticremote.js に向け、runtime 依存ツリーを外しています。

この分離により、開発用と公開用の責務は明確になります。ただし当然ながら、両方の metadata を一致させておくという維持コストはあります。

現時点では、ここに対する公開時の検証はまだ十分ではありません。この切り替えが常に正しいことを保証する仕組みが repo にないので、CI の自動化が次に必要な改善です。

30 秒の導線

配布の問題を解決したあと、接続フローの各ステップを見直し、摩擦をひとつずつ潰しました。

0-5 秒。 ユーザーが端末で npx tacticremote を実行します。初回実行では、ランチャーがキャッシュ済みバイナリがないことを検知してダウンロードします。2 回目以降は、キャッシュ確認は数ミリ秒で終わります。

5-10 秒。 サーバーバイナリが起動します。ローカル IP を検出し、ランダムな API key を生成し、Unicode ブロック文字で QR Code を端末に直接表示します。ブラウザも、Web UI も不要です。

10-15 秒。 ユーザーがスマホで Tactic Remote を開きます。ホーム画面には「コンピュータを接続」カードが表示され、npx tacticremote コマンドと「QR をスキャン」ボタンがあります。ボタンを押すとカメラが開きます。

15-25 秒。 端末に表示された QR Code をスキャンします。QR にはサーバーの IP、ポート、API key を含む JSON payload が入っています。アプリはそれを解釈し、サーバー設定を自動保存します。Add Server 画面も、手動 IP 入力も、API key のコピペもありません。

25-30 秒。 WebSocket handshake が完了し、ターミナル出力がスマホへ流れ始めます。接続インジケータが緑になります。

QR の自動保存が、いちばん大きな摩擦を消しました。以前の導線では、Add Server を押して、IP を入れて、ポートを入れて、API key を入れて、保存して、接続する必要がありました。5 回のタップと 3 回の手入力です。QR スキャンは、それを 1 つのジェスチャーに圧縮します。

やり直せるならどうするか

いくつか、まだ直せていない既知の課題があります。

署名検証がまだない。 ランチャーは HTTPS でバイナリを取得しますが、公開された署名や checksum で整合性を検証してはいません。これは現行方式で最も重要な残りの完全性ギャップです。

manifest の切り替えはまだ自動化が必要。 開発用 manifest と公開用 manifest を分けるのは正しい判断ですが、両者がずれないことを保証する公開時のガードレールがまだ足りません。

バックアップミラーがない。 GitHub Releases が一時的に使えない場合、ランチャーには別の取得先がありません。二次ミラーがあれば、配布パスの信頼性はさらに上がります。

全体像

この配布パスは 1 つの原則に基づいています。ユーザーにインフラのことを考えさせないことです。Bun が何かを知る必要も、Linux マシンの CPU アーキテクチャを知る必要も、API key を手で写す必要もありません。コマンドが入口を作り、ランチャーがバイナリを取得し、QR Code が接続情報を運びます。

サーバー本体のアーキテクチャ、単一依存設計、セキュリティモデルについては、姉妹記事 「依存パッケージ1つ:ws パッケージだけで本番サーバーを構築する」 で詳しく説明しています。この記事は、そのサーバーをいかに素早くユーザーの手元に届けるかについてです。

まだ 30 秒フローを試していないなら、/download から試してみてください。


サーバーのアーキテクチャについては「依存パッケージ1つ:ws パッケージだけで本番サーバーを構築する」を参照してください。セットアップ手順はGetting Started ガイドを参照してください。

Tactic Remote を試す

スマートフォンからコーディングエージェントを操作

Mac、Windows、Linux 上の Claude Code や Codex などに接続。iPhone や iPad で進捗を確認し、次の指示を送れます。