dev-server について
dev-server は、複数の Git worktree にまたがる Next.js 開発サーバーを一元管理するスキルです。デーモンがポートや環境変数、ログを管理し、起動確認と時間制限付きの診断プローブを提供します。
主な機能
- 01worktree 間のサーバー管理
- 02ポート割り当てと環境変数注入
- 03セッションログの集約
- 04起動確認とルートのプローブ
- 05遅延・失敗レスポンスの分類
- 06停止状態のセッション再起動
活用シーン
- 01複数の worktree で開発サーバーを並行稼働
- 02他のエージェントが開始したセッションのログ確認
- 03ビルドキャッシュ削除前の遅いルートの診断
- 04停止判定後のセッション再起動
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ civitai/ civitai --skill dev-serverインストール後、Claude Code で/dev-serverを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
.env.example.gitignorecli.mjsconsole.mjsenv-modes.exampleREADME.mdscripts/app-registry.selftest.mjsbranch-watch.selftest.mjscli-verbs.selftest.mjsdaemon-home.selftest.mjsdaemon-port.mjsdaemon.mjsdb-host.selftest.mjsdefender-exclusions.ps1env-chain.selftest.mjsenv-modes.mjspaths.mjsport-probe.mjsprobe.integration.selftest.mjsprobe.mjsprobe.selftest.mjstest-queue.mjsworktree-remove.integration.selftest.mjsworktree.mjsworktree.selftest.mjs詳細
開発サーバースキル
複数の Git ワークツリーにまたがる Next.js 開発サーバーを一元管理します。デーモンがポートの割り当て、環境変数の注入、ログの集約を行うため、どのエージェントでも、誰がサーバーを起動したかにかかわらず、開発サーバーのログにアクセスできます。
クイックスタート
# Check what's running
node .claude/skills/dev-server/cli.mjs status
# Start a dev server for current worktree
node .claude/skills/dev-server/cli.mjs start
# Start for a specific worktree
node .claude/skills/dev-server/cli.mjs start /path/to/worktree
# Start with a service on production (see Env modes)
node .claude/skills/dev-server/cli.mjs start /path/to/worktree --prod buzz
# View logs
node .claude/skills/dev-server/cli.mjs logs <session-id>
# Stop a session
node .claude/skills/dev-server/cli.mjs stop <session-id>
サーバーの準備完了を確認する: 起動後、セッションの状態をポーリングし、ready: true になっていることを確認します。デーモンは、設定されたヘルスチェックエンドポイント、またはログ内の「Ready」パターンの検出によって、セッションを準備完了と判定します。
デーモンが実行される Node と、そのバージョンが固定される理由
デーモンは process.execPath(cli.mjs と console.mjs の startDaemon)で起動されます。つまり、
最初にデーモンを起動した CLI コマンドを実行した Node が使われます。その後、デーモンは自身の環境を、管理対象のすべての next dev に引き継ぎます。そのため、上記のいずれかのコマンドを最初に入力したときに PATH 上にあった Node が、誰かがデーモンを終了するまで、プロセス全体で使われます。どのバージョンが使われたかは記録されません。
これは単なる理論上の話ではありません。開発用マシンで確認したところ、デーモンは環境に設定された Node 26.7.0 で実行されていた一方、.nvmrc では 24.19.0 が固定され、package.json では engines.node: ">=24.0.0 <25" が宣言され、本番環境は node:24.19.0-alpine3.24 上でビルドされていました。デーモンは開発シェルの外から起動されており、そのシェルには pnpm もありませんでした。そのため、ロックファイルやスキーマの変更を検出したときに pnpm install / pnpm run db:generate を実行するデーモン独自の自動インストール処理が、気づかないうちに無効になっていました。
環境にかかわらず、.nvmrc と同じ node --version を返し、PATH に pnpm が含まれるシェルから起動してください。 リポジトリのルートで nvm use を実行すれば、その両方が設定されます。
NixOS の場合(任意)、flake ラッパーがこれを自動で行います。.nvmrc で指定されたものと同じバージョンの Node を固定し、PATH に pnpm を追加して、NixOS で必要な Prisma エンジンのパスをエクスポートします。
nix run .#dev-server -- status
nix run .#dev-server -- start
nix run .#dev-server -- logs <session-id>
このドキュメント内のほかの箇所で実行する node .claude/skills/dev-server/cli.mjs … は、いずれも同じサブコマンドを受け付けます。ラッパーが決めるのは実行に使う Node だけなので、ここで説明する手順は Nix の有無に左右されません。
どちらの場合も、セッションを信頼する前に現在の設定を確認してください。
# the daemon's real interpreter, not the one you assume.
# the pid file belongs to the skill dir the daemon RUNS FROM, which is the primary checkout — a
# relative path here reads the wrong tree's file, or none, when you are standing in a worktree.
readlink -f /proc/$(cat <primary-checkout>/.claude/skills/dev-server/daemon.pid)/exe
Node を変更するには、デーモンの再起動が必要です。cli.mjs shutdown を実行し、正しいシェルから再び起動してください。実行中のデーモンは、新しい PATH を読み込みません。
2 つ目のデーモン:DEV_DAEMON_PORT
デーモンは 127.0.0.1:9444 で動作します。DEV_DAEMON_PORT を設定すると、もう 1 つデーモンを並行して起動できます。
DEV_DAEMON_PORT=9555 node .claude/skills/dev-server/cli.mjs status
CLI、コンソール、scripts/test-unit-run.mjs、デーモン自体は、すべて scripts/daemon-port.mjs を介してポートを解決します。また、クライアントが起動するデーモンは、そのクライアントの環境を引き継ぐため、これらがポートの場所について食い違うことはありません。2026-08-19 までは、デーモンはこの変数をまったく読み取らず、設定すると、クライアントは何も待ち受けていないポートに接続していました。
この変数はデーモンのデフォルト値にのみ適用されます。node scripts/daemon.mjs --port <port> でポートを明示した場合は、そちらが優先されます。各デーモンは共通の daemon.pid を使用するため、最後に起動したデーモンの情報がそのファイルに記録されます。
開発ポートには決して curl を使わず、代わりに probe を使ってください
node .claude/skills/dev-server/cli.mjs probe /home
正常に応答しなくなった開発サーバーは、接続を拒否しません。接続を受け付けて応答が遅いか、接続を受け付けても応答しません。そのため、制限時間のない curl はツールのタイムアウトである300秒まで待ち続け、対処に使える情報を何も返しません。シェル呼び出しでいくつか連続して実行すると、10分があっという間に過ぎてしまいます。このため、PreToolUse フックは開発ポートへの制限時間のないリクエストをブロックします。本当に curl を使いたい場合は、--max-time を指定してください。
probe は明確な時間制限を設けてルートに2回リクエストし、その2回のリクエストについてセッション自身のログを読み取り、対応する対処法とともに判定を返します。必ず終了します。
UPSTREAM-SLOW http://localhost:3000/home
UPSTREAM-SLOW — the framework is fine; time is spent in application code (database, tunnel, cache).
first : 200 in 8.10s [next.js 30ms | application-code 8.10s]
repeat: 200 in 8.10s [next.js 31ms | application-code 8.10s]
-> Not a cache problem — do NOT purge, it will not help. [...]
終了コードは、ok/cold の場合は0、それ以外の場合は1です。そのため、チェックで curl の代わりに使えます。
知っておくとよい判定が2つあります。stopped-answering は、最初のリクエストには応答したものの、2回目には応答しなかったことを意味します。プロセスは稼働しており、その内部で何かが停止しているため、別のセッションを開始するのは適切な対応ではありません。対処法にもその旨が示されます。また、probe は note: more than one request to this route in the window を追加することがあります。probe は、自身のリクエストに ?__probe= ノンスを付けて、自身のログ行を特定します。しかし、すでにクエリ文字列があるルートと、クエリを解析するハンドラーを持つすべての /api/trpc/* ルートでは、パスによる照合にフォールバックします。稼働中のセッションでは、他のトラフィックとこちらのリクエストを本当に区別できません。この注記は、表示された内容があなたのリクエストに関するものではない可能性を示します。これは、サーバーが自らの状況を説明しようとしないこととは異なります。
タイミングだけでは問題の原因を特定できない理由
異なる2つの障害が同じ形で現れます。ページの表示に、ウォーム時もコールド時も約8秒かかり、ステータスコードは200で、ログにもエラーらしきものはありません。どちらの障害でもヘルスチェックは通り続けます。/api/health はすでにコンパイル済みの安価な API ルートであり、どちらの障害もその動作には影響しないためです。
Next は、出力するすべてのリクエスト行で、すでにこの2つを区別しています。
GET /home 200 in 8.1s (next.js: 39ms, proxy.ts: 8ms, application-code: 8.1s)
| 読み取り結果 | 意味 | 対処 |
|---|---|---|
繰り返しアクセスで next.js が大半を占める | ビルドキャッシュが機能しておらず、フレームワークが毎回処理をやり直しています | unwedge — パージして再起動します |
application-code が大半を占める | フレームワークは正常で、時間がかかっているのは下流(データベース、トンネル、キャッシュ)です | 上流を修正します。パージには45秒かかり、何も変わりません |
| 繰り返しアクセスが速い | コールドコンパイルです。想定どおりの動作です | 対処は不要です |
すぐに終わる障害が2つあります。どちらもタイミングのルールを適用する前に確認します。 ページが壊れているのに判定が ok となるのは、判定がないよりも問題だからです。マージまたはチェックアウト後に node_modules が古いと、数ミリ秒で500エラーになります(STALE-DEPS — pnpm install で修正します。ビルドキャッシュをパージしても何もインストールされません)。また、以下の設定の自己フェッチが原因の場合も、劣化したページがすぐにレンダリングされます。
この切り分けでは判別できないケースが1つあり、probe はまずこれを確認します。 _app は SSR レンダリングのたびに /api/user/settings を自己フェッチし、APP_SETTINGS_FETCH_TIMEOUT_MS(デフォルトは8秒)で中断します。この中断は application-code として計上されるため、自己の API ルートに応答できないサーバーでも、この切り分けだけでは「データベースが遅い」と表示されます。検索可能なマーカー [_app] settings bootstrap fetch failed はタイミングより優先され、判定は SELF-FETCH-FAILING になります。続いて probe はそのエンドポイントに直接リクエストを送り、2つの原因のどちらなのかを伝えます。自己フェッチ先のポートが間違っている(NEXTAUTH_URL_INTERNAL とセッションのポートが異なる)か、エンドポイントが実際に応答していないかのどちらかです。
遅い依存関係と区別する手がかりは、ページの表示時間が設定されたタイムアウトと同じ一定値で、ウォーム時もコールド時も変わらないことです。遅い依存関係の所要時間は変動しますが、タイムアウトは変動しません。2026-08-16 の計測では、APP_SETTINGS_FETCH_TIMEOUT_MS を 8000 から 30000 に変更すると、/home の所要時間も連動して8.1秒から30.1秒に変わりました。どちらの設定値でも .next をパージしても効果はありませんでした。
この判定を読むときに知っておくとよい点があります。updateEnvUrlsForPort はポート3000の場合に早期リターンするため、プライマリセッションでは .env に記載されたとおりの NEXTAUTH_URL_INTERNAL が使われますが、セカンダリセッションではすべて、セッション自身のポートに書き換えられます。現在の値は正しいものです。これは2種類のセッションで動作が異なる理由であり、判定がセッションのポートとの比較を求める理由でもあります。
このため、「8秒で一定なら .next をパージする」というルールは誤りであり、習慣にすると実際に時間を無駄にします。2026-08-16 にこのリポジトリで計測したところ、一定して8.1秒かかった /home の内訳は、フレームワークが30ミリ秒、アプリケーションコードが8.1秒でした。パージしても数GBを削除するだけで、何も修正されなかったでしょう。合計時間ではなく、内訳を確認してください。
内訳を取得できない場合(ログバッファーがあふれた、またはセッションがない場合)、判定は slow-unclassified となり、推測せずにその旨を伝えます。
unwedge — WEDGED 判定の後にのみ実行
node .claude/skills/dev-server/cli.mjs unwedge <session-id>
セッションを停止し、そのビルドディレクトリを削除し、同じ env モードで再起動し、準備完了を待って再プローブし、 各タイミングを出力します。確実に約45秒の再ビルドを要するため、自動では実行されず、セッションを推測することも ありません。id を指定してください。指定しなかったセッションこそ、たいてい誰かが見ているセッションだからです。
セルフテスト。スタンドアロンのスクリプトで、vitest は使いません — 変更が触れるものを実行し、cli.mjs を編集した後は
必ず cli-verbs を実行してください。
node .claude/skills/dev-server/scripts/env-chain.selftest.mjs # .env layering, both directions
node .claude/skills/dev-server/scripts/app-registry.selftest.mjs # registry, ports, path identity, the lock
node .claude/skills/dev-server/scripts/db-host.selftest.mjs # the DB host line never emits a credential
node .claude/skills/dev-server/scripts/cli-verbs.selftest.mjs # every dispatch target in cli.mjs exists
node .claude/skills/dev-server/scripts/branch-watch.selftest.mjs # HEAD watching + the restart decision
node .claude/skills/dev-server/scripts/probe.selftest.mjs # the classifier, pure
node .claude/skills/dev-server/scripts/probe.integration.selftest.mjs # the real probe() end to end
node .claude/skills/dev-server/scripts/worktree.selftest.mjs # what `wt stale` / `wt rm` say about a PR, a prune, and the daemon's home
node .claude/skills/dev-server/scripts/worktree-remove.integration.selftest.mjs # `wt rm`'s daemon guard, against a throwaway repo
node .claude/skills/dev-server/scripts/daemon-home.selftest.mjs # the daemon runs from the primary, never the calling worktree
node .claude/hooks/check-writable.selftest.mjs # the hook, both directions
統合テストが存在するのは、単体テストでは最も重要なバグが見えないからです。かつて runSample が classify へ渡す途中で
missingModule を落としたため、stale-deps の判定全体がデッドコードになっていました — にもかかわらず、その単体テストケースは
すべてパスしていました。出荷コードが決して生成しなかったフィールドを、テストが手で組み立てていたからです。そのバグを今日
再現しても、単体テストは依然としてグリーンのまま、統合テストは失敗します。 新しいシグナルを追加するものは、単体ファイル
だけでなく統合ファイルに入れるべきです。
CLI コマンド
| コマンド | 説明 |
|---|---|
probe [route] | 制限付きリクエスト + 判定 (ok/cold/wedged/stale-deps/upstream-slow/proxy-slow/error-status/self-fetch-failing)。curl の代わりに使用 |
unwedge <session-id> | 停止、ビルドディレクトリの削除、再起動、待機、再プローブ (約45秒) |
status | デーモンのステータスを確認し、すべてのセッションを一覧表示 |
list | すべての開発セッションを一覧表示 |
start [worktree] [--app name] [--prod a,b] [--dev a,b] | 開発サーバーを起動 (デフォルト: メインアプリ、カレントディレクトリ) |
logs [session-id] [--app name] | セッション、またはこのワークツリー内のアプリのログを取得 |
tail [session-id] [--app name] | ログを継続的に表示 |
stop <session-id> | stop --app name | セッションまたはアプリを停止 |
restart <session-id> | restart --app name | セッションまたはアプリを再起動 |
rgb [subcmd] | RGB プロキシの操作 (status|start|stop|restart|logs) |
app | 実行中のアプリ、そのワークツリー、利用可能なものを一覧表示 |
app <name> [subcmd] [worktree] | アプリの操作 (status|start|stop|restart|logs) |
auth [subcmd] | 認証ハブの操作 (status|start|stop|restart|logs) |
test run [worktree] | 単体テストの実行をキューに追加。順位 + それを待つためのコマンドを返す |
test wait <run-id> | その実行が終了するまでブロック。実行の終了コードで終了 |
test list / test show <id> / test logs <id> | キューの状態、1回の実行、1回の実行の出力 |
test cancel <id> | キュー済みまたは実行中の実行をキャンセル |
test config [n] | 同時実行数の上限を表示または設定 (0 でキューを一時停止) |
shutdown | デーモンをシャットダウン |
Env モード — セッションがどのサービスと通信するか
セッションは .env ファイルを重ねます — プライマリチェックアウトのものをベースとして、ワークツリー自身のものをその上に —
さらにその上にサービスごとの オーバーレイ を適用します。オーバーレイは指定したサービスのキーだけを再定義するため、
チェーン内のそれ以外は変わりません。
env-modes.local がなければオーバーレイは一切適用されません — 「すべて dev で動く」わけではありません。このファイルは
gitignore されているため、チェックアウトに同梱されることはありません。存在するまでは、すべての起動はこの機能以前とまったく同じく
ベースの .env で動作し、サマリーには (no groups defined — no overlay applied) と表示されます。
# Every defined group on dev. This is what a bare start does.
node .claude/skills/dev-server/cli.mjs start
# Buzz on production, everything else still dev.
node .claude/skills/dev-server/cli.mjs start --prod buzz
# Several groups, the whole lot, and the whole lot with an exception.
node .claude/skills/dev-server/cli.mjs start --prod db,search
node .claude/skills/dev-server/cli.mjs start --prod all
node .claude/skills/dev-server/cli.mjs start --prod all --dev search
--prod/--dev が動かせるのは、あなた自身の env-modes.local が定義するグループだけであり、そのファイルは
gitignore されています — env-modes.example を新しくコピーしても何も定義されないため、記入するまで --prod all は
何も行いません。サービスの追加は、コードではなくそのファイルの編集です。
⚠️ env-modes.local は、.env が現在そうであるようにはフォールスルーしません。これは実行中のデーモン(env-modes.mjs)のスキルディレクトリから読み込まれ、そのファイルは gitignore されているため、プライマリチェックアウトにのみ存在します。resolveDaemonHome の変更を反映した CLI またはコンソールは、プライマリからデーモンを起動し、それが処理します。しかしスキルディレクトリはコミットされているため、各ワークツリーは独自のコピーを実行します。その変更より前に切られたツリーは依然としてデーモンを自身の中に起動し、node scripts/daemon.mjs で手動で起動されたデーモンも同様です。いずれの場合も、そのワークツリーのスキルディレクトリを読み込み、env-modes.local を見つけられず、オーバーレイをまったく適用しません。
いくつかのサービスには移行先となる dev の対応物がまったくありません。env-modes.mjs は PROD_ONLY_GROUPS に orchestrator、payments、s3、clickhouse、notifications、feeds、opensearch を、UNMOVABLE_GROUPS に auth-hub をリストしています(これは自身の .env を読む単一の共有プロセスであるため、そこでのオーバーレイはアプリを実行されていないハブに再ポイントしてしまいます)。したがって、mode: dev は決して「ここには本番のものが何もない」という意味にはなりません。
⚠️ アプリには env モードがありません。 --prod/--dev はメインアプリにのみ適用されます。アプリはオーバーレイなしで自身の .env チェーン上で動作します。start --app <name> --prod db は黙って無視されるのではなく拒否されます。
デフォルト。 すべてのグループは dev にデフォルト設定されます。dev サービスが不安定な場合は、スキルの自身の .env でデフォルトを移動してください。
DEVSERVER_PROD_GROUPS=buzz
その行を編集しても、すでに起動しているセッションは移動しません。セッションは作成時に使用したデフォルトを固定するため、ブランチの切り替えやクラッシュからの再起動によって密かに移動されることはありません。その代償として、それらのセッションが再起動されるまでは、そのうちの 1 つに対する素の start はモードの不一致として拒否されます。これは便利さよりも正確さを優先した結果です。素の start は今や別のものに解決されるからです。
--prod / --dev フラグはそれに優先し、そのフラグはその start にのみ適用されます。--prod start の後の素の start は dev に戻ります。これは死んだセッションを再利用する場合も含みます。そのワークツリーでセッションがすでに実行されている間に異なるモードを要求すると、実行中のもので黙って応答されるのではなく拒否されます。まずそれを停止してください。
⚠️ dev は安全の同義語ではありません。 orchestrator、payments、S3、ClickHouse、notifications DB、feeds プロキシ、OpenSearch にはdev の対応物がまったくありません。完全な dev モードのセッションでも、そのすべてに対して本番と通信します。Generate を押すと、モードが何と言おうと実際のジョブが送信され、実際の Buzz が消費されます。まさにこの理由から、そのリストはすべてのモードサマリーの後に出力されます。
auth ハブは db に従いません。 これはすべてのセッションで共有される単一のプロセスであり、自身の apps/auth/.env を読み込むため、--prod db セッションはそのファイルが指定するデータベースに対して認証し、結果のユーザー id を本番に対して解決します。dev と prod のユーザー id は無関係な行であるため、404 を予期してください。あるいはさらに悪いことに、別の実ユーザーとして行動してしまう可能性があります。ログインを伴う --prod db を使用する前に、apps/auth/.env を手動で同じデータベースに向けてください。
ウォームビルドディレクトリで search または signals モードを変更することは完全にはクリーンではありません。 これらのグループは NEXT_PUBLIC_* 値を設定し、Turbopack がそれをクライアントチャンクにインライン化します。またビルドディレクトリはモードではなくブランチでキー付けされます。ブラウザが重要な場合は、いずれかを変更するときに .next を削除してください。サーバー側のコードは新しい値を即座に読み込むため、これはクライアントが何に対してコンパイルされたかにのみ影響します。
実行中のセッションのモードを読む場所: status と list は envModes と envModeSummary を持ち、デーモンログは起動時に Env: の隣にそれらを出力します。
Env: C:\Dev\Repos\work\wt-thing\.env
Env modes: buzz=dev db=prod redis=dev search=dev signals=dev | always prod (no dev target): ...
キューに登録されたテスト実行
ユニットテストスイートはすべてのコアを使います。1 回の実行なら問題ありませんが、5 つのエージェントが同時にそれぞれ実行を開始すると、マシンに大きな負荷がかかります。デーモンが実行を順番に処理します。
# 1. Request a run. Returns immediately, whether it started or queued.
node .claude/skills/dev-server/cli.mjs test run
# Run t3f9a2 queued at position 2 of 3 (1/1 running).
# Wait for it in the background: node .claude/skills/dev-server/cli.mjs test wait t3f9a2
# 2. Wait for it — in the background, so you can work meanwhile.
node .claude/skills/dev-server/cli.mjs test wait t3f9a2
test wait は実行自体の終了コードで終了するため、確認に使っていた箇所では pnpm run test:unit:run の代わりになります。-- の後の追加引数は vitest に渡されるため、test run . -- path/to/one.test.ts のように実行対象を絞り込めます。
同時実行数のデフォルトは 1 で、スキルの .env 内の TEST_CONCURRENCY、または実行時の test config <n> で設定できます。0 も有効で、一時停止を意味します。値を上げるまで何も開始されません。一時停止中のキューの後ろに実行を登録した呼び出し元には、順番を伝えて待たせるのではなく、一時停止中であることが明示されます。
利用する前に知っておくべきこと:
- 実行を所有するのはあなたではなくデーモンです。 エージェントが待機中に終了しても、何も解放されません。エージェントは何も保持していないためです。子プロセスが終了するとスロットが空きます。
- 呼び出し元がポーリングを停止した、キュー内の実行は 10 分後に破棄されるため、停止したエージェントがキューを占有することはありません。
test waitは 2 秒ごとにポーリングするため、動作中の待機処理でこの事態は起こりません。 - 30 分を超えた実行は強制終了され、
timeoutとして報告されます。強制終了によって終了コードが返されない場合でも、スロットが永久に占有されないよう、猶予期間後に解放されます。 - デーモンを再起動するとキューは破棄されます。
test waitは不明な実行 ID を終了状態として扱い、再度リクエストするよう通知して、ゼロ以外のコードで終了します。デーモンが実行を忘れた後も、ポーリングを続けることはありません。 - 位置は推定ではなく正確な値です。 1 つの順序付きリスト内のインデックスを示します。
- ログウィンドウには直近 2000 行が保持され、切り詰められた場合はその旨が表示されます。 それを超える出力があると古い行が失われるため、遅れて実行した
test waitやtest logsの読み取り結果は一部だけになることがあります。どちらの待機処理も、失われた行数を示すWARNING: this log is INCOMPLETE …を出力します。また、実行ビューのlogsDroppedがゼロ以外の場合、表示内容を実行全体として引用しないでください。最初からログをストリーミングしている待機処理には影響しません。 - どちらの待機処理でも、終了コードは
exitCodeForのものです。test waitとpnpm run test:unit:runは同じルールを使うため、シグナルで強制終了された実行は、どちらでもシェルの 255 ではなく 1 を返します。
セッションオブジェクト
各セッションには、以下が含まれます。
{
"id": "a1b2c3d4",
"worktree": "/path/to/worktree",
"branch": "feature/my-feature",
"port": 3000,
"status": "running",
"ready": true,
"readyAt": "2024-01-15T10:30:02.000Z",
"startedAt": "2024-01-15T10:30:00.000Z",
"url": "http://localhost:3000",
"envModes": { "db": "dev", "buzz": "dev", "search": "dev", "signals": "dev", "redis": "dev" },
"envModeSummary": "buzz=dev db=dev ... | always prod (no dev target): orchestrator, payments, ..."
}
ステータス値:starting、running、stopped、crashed、error
ログエントリ
{
"index": 42,
"timestamp": "2024-01-15T10:30:05.123Z",
"level": "stdout",
"message": "Ready on http://localhost:3000"
}
ログレベル:stdout、stderr、error、warn、info
ダッシュボード TUI
ライブターミナルダッシュボードを起動するには、node .claude/skills/dev-server/console.mjs(または pnpm run dev:daemon)を実行します。
| キー | 操作 |
|---|---|
1 | フィルター:エラー(error + warn レベル) |
3 | フィルター:trpc |
4 | フィルター:api |
5 | フィルター:prisma |
6 | フィルター:stdout のみ |
7 | フィルター:stderr のみ |
8 | フィルター:info(デーモンメッセージ) |
/ または f | フリーテキスト検索(クエリを入力し、Enter で適用) |
a | すべてのログを表示(フィルターを解除) |
r | セッションを再起動 |
c | ログバッファをクリア |
x | セッションを停止して終了 |
R | RGB プロキシを切り替え(開始/停止) |
A | 認証ハブを切り替え(開始/停止) |
s または Tab | 次のセッションに切り替え(複数のセッションが実行中の場合のみ表示) |
q | ダッシュボードを終了(サーバーは実行を継続) |
K | デーモンを強制終了して終了 |
フィルターはオン/オフを切り替えられます。有効なフィルターはフッターバーで強調表示されます。検索では、一致するテキストが赤色で強調表示されます。
RGB プロキシ
デーモンでは、rgb-proxy リバースプロキシ(civitai-dev.{red,green,blue} をローカルの開発サーバーに接続)の管理を任意で有効にできます。
設定
.claude/skills/dev-server/.env を編集します。
RGB_PROXY_ENABLED=true # auto-start proxy when daemon boots
RGB_PROXY_PATH=../rgb-proxy # path relative to project root
また、メインの .env に NEXTAUTH_URL=https://civitai-dev.blue と SERVER_DOMAIN_* が設定されていること、hosts ファイルで 3 つのドメインが 127.0.0.1 に割り当てられていることも確認してください。初回の設定については .claude/skills/rgb-proxy/SKILL.md を参照してください。
操作
# Start / stop / restart / status / logs via CLI
node .claude/skills/dev-server/cli.mjs rgb start
node .claude/skills/dev-server/cli.mjs rgb status
# Or via pnpm scripts
pnpm dev:rgb # start proxy (daemon boots if not already running)
pnpm dev:rgb:stop
pnpm dev:rgb:status
ダッシュボード TUI で R を押すと、プロキシを切り替えられます。
管理者権限/sudo の要件
Redbird はポート 80 と 443 をバインドします。Windows では、昇格したターミナルからデーモンを起動する必要があります。macOS/Linux では、sudo を付けて起動してください。起動に失敗した場合、デーモンは /rgb のステータスと RGB プロキシログに lastError を表示します。
アプリ
moderator と creator-studio はメインアプリと同じ方法でデーモン経由で実行されます。現在いるワークツリーから起動し、ログは同じ場所に記録され、ポートは同じ割り当て機構によって確保されます。
node .claude/skills/dev-server/cli.mjs start --app moderator # from the worktree you are in
node .claude/skills/dev-server/cli.mjs app # what is running, and where
node .claude/skills/dev-server/cli.mjs app moderator logs
node .claude/skills/dev-server/cli.mjs app moderator restart
node .claude/skills/dev-server/cli.mjs app moderator stop
--app <name> は start、logs、tail、stop、restart で使えるため、ひとつの操作でライフサイクル全体を扱えます。
app <name> <subcmd> は同じ操作を別の書き方で表したものです。どちらも現在のディレクトリのワークツリーを既定で使用し、app <name> <subcmd> <worktree> を指定すると別のワークツリーを指定できます。実行中のアプリは、メインアプリのセッションと並んで status の apps にも表示されます。
| アプリ | 優先ポート |
|---|---|
| moderator | 5174 |
| creator-studio | 5175 |
固定ではなく優先ポートです。 アプリを起動する最初のワークツリーには、表の番号が割り当てられます。同じアプリを起動する 2 つ目のワークツリーには、EADDRINUSE で終了する代わりに、次の空きポートが割り当てられます。そのため、同じアプリの 2 つのブランチを並行して実行できます。ポートがずれても、それに合わせて何かを書き換える必要はありません。どちらのアプリの .env にも、専用のポートやベース URL は設定されていません。各アプリの優先ポートはポートのずれ先候補から除外されるため、moderator のポートがずれても creator-studio のポートが奪われることはありません。
🔴 起動したという事実だけでなく、path フィールドを確認してください。 アプリが誤ったチェックアウトを配信していても、200 を返し、完全に正常に見えます。それが違うと示す唯一のものが path です。
.env はプライマリチェックアウトから引き継がれます。 アプリは <primary>/apps/<name>/.env をベースとして読み込み、ワークツリーに <worktree>/apps/<name>/.env がある場合はその内容を上書きで適用します。これらのファイルは gitignore されているため、新しいワークツリーにはどちらもありません。ベースがあるからこそ、アプリを起動できます。
⚠️ そのベースが本番データベースを指している可能性があります。 そして、この引き継ぎにより、新しいワークツリーは誰も選んでいないのに本番データベースを引き継ぎます。アプリは起動時に DATABASE_URL のホスト(認証情報は表示しません)を出力し、app/status の出力では dbHost として表示します。書き込みを行う操作をする前に確認してください。
storage と notifications は対象外です。登録はされていましたが、起動することはありませんでした。vite.config.ts がなく(開発スクリプトは tsx watch src/server.ts)、.env もないため、毎回の試行が .env の事前チェックで auth-hub の問題を報告して終了していました。適切に登録するか、登録しないでください。
アプリを起動すると、auth hub が有効でまだ実行されていない場合は起動します。 auth hub がなければ、アプリにログインできないためです。メインアプリは起動しません。 アプリは必要に応じて REST 経由でメインアプリにアクセスします。また、Next.js の初回コンパイルは誰かに代わって実行するものではありません。開いているページで必要な場合は、自分で起動してください。
意図的に :: にバインドします
デーモンはすべての vite サイドカーを --host :: 付きで起動します。127.0.0.1 にバインドすると、IPv4 では応答するものの [::1] では何も応答しません。Windows は localhost を最初に ::1 に解決するため、ブラウザーは接続拒否になり、IPv4 にフォールバックする curl ではサーバーが完全に正常だと報告されていました。--host localhost も解決策ではありません。::1 だけに解決され、127.0.0.1 をハードコードしているすべての呼び出し元に障害が移るためです。:: は IPv4 マップアドレスを受け付けるため、localhost、127.0.0.1、[::1] のすべてに応答し、3000 番ポートですでに next dev がバインドしている方法とも一致します。トレードオフとして、:: は LAN インターフェイスでも待ち受けます。以前のバインドはループバックのみでした。
最初のリクエストは時間がかかります
Vite は開発時に、リクエストに応じてルートをコンパイルします。起動直後のアプリへの最初のアクセスには、25 秒以上かかることがあります(auth hub の /login が特に時間を要します)。2 回目以降のアクセスは数百ミリ秒です。起動直後にブラウザーが白いページのままでも、通常はハングではなくこの処理が原因です。
ダッシュボードに表示されるのはメインアプリのセッションだけです。--app で起動したアプリは表示されません。cli.mjs app または cli.mjs status を使ってください。
認証ハブ
認証は独立したログインハブ(ポート 5173 で動作する SvelteKit の apps/auth)に分離されました。メインアプリは検証専用のスポークとなり、ハブの JWKS を使ってハブの civ-token を検証します。メインアプリ自体では next-auth のサインインを行わなくなりました。そのため、開発環境で新たにログインするにはハブの起動が必要です。デーモンがサイドカーとしてハブを起動・管理します。また、AUTH_HUB_ENABLED が設定され、ハブがまだ起動していない場合は、アプリ(start --app <name>)の起動時にハブも起動します。
操作
node .claude/skills/dev-server/cli.mjs auth status # status + JWKS url + lastError
node .claude/skills/dev-server/cli.mjs auth start
node .claude/skills/dev-server/cli.mjs auth restart
node .claude/skills/dev-server/cli.mjs auth logs
ダッシュボード TUI で A を押すと切り替えられます。セッション行には AUTH: ready と表示されます。
設定
.claude/skills/dev-server/.env:
AUTH_HUB_ENABLED=true # auto-start the hub when the daemon boots
AUTH_HUB_PATH=apps/auth # relative to the PRIMARY checkout, not the tree the daemon runs from
AUTH_HUB_PORT=5173 # hub dev port (matches AUTH_JWT_ISSUER)
初回の環境変数設定
ハブは独自の apps/auth/.env を読み込みます(Vite が読み込みます。デーモンは環境変数を注入しません)。これはローカル開発用にすでに生成されており、メインアプリの開発用 DB/Redis/シークレット/プロバイダー認証情報を再利用し、新しい EC P-256(ES256)署名キーペアを使用します。重要なファイルは 2 つあります。
apps/auth/.env— ハブの署名キーペア(AUTH_JWT_PRIVATE_KEY/_PUBLIC_KEY、PKCS8/SPKI)、AUTH_JWT_ISSUER=http://localhost:5173、共有のNEXTAUTH_SECRETとAUTH_INTERNAL_TOKEN、DB/Redis、プロバイダー、メール。- ルートの
.env— スポーク側の設定:AUTH_JWT_ISSUERとAUTH_JWKS_URI(→localhost:5173)、および一致するAUTH_INTERNAL_TOKEN。
キーペアを再生成するには、次を実行します:openssl ecparam -genkey -name prime256v1 -noout -out sec1.pem && openssl pkcs8 -topk8 -nocrypt -in sec1.pem -out priv.pem && openssl ec -in sec1.pem -pubout -out pub.pem(jose の importPKCS8 で使う秘密鍵は PKCS8(BEGIN PRIVATE KEY)である必要があります。SEC1 ではありません)。
RGB プロキシモード
ハブは RGB プロキシ経由でも動作します。開発環境を https://civitai-dev.{red,green,blue}(localhost:3000 ではなく)で開く場合、これらはループバックではなく、登録可能なドメインがそれぞれ異なるため、信頼済みでなければハブはこれらのドメインからのファーストパーティログインを拒否します。ハブは apps/auth/.env の AUTH_DEV_TRUST_HOSTS を使ってこれらを信頼します(開発環境専用。カラー別ドメインに加えて civitai-dev.cyan がすでに設定されており、メインアプリの SERVER_DOMAIN_* と同じ設定です)。メインアプリは、どのカラーのドメインを開いているかにかかわらず、引き続き AUTH_JWT_ISSUER=http://localhost:5173 でハブを参照します。各カラーは、ファーストパーティ OAuth フローを通じて、それぞれのドメイン上で独自の civ-token を発行します。新しいカラーやエイリアスを追加する場合は、そのホストを AUTH_DEV_TRUST_HOSTS に追加し、ハブを再起動してください(cli.mjs auth restart)。本番環境では完全に無視されます。
ログイン
- メールのマジックリンクはそのまま動作します(
EMAIL_*は設定済み)。外部コンソールの変更は不要です。 - ソーシャルプロバイダー(GitHub/Google/Discord/Reddit)を使うには、プロバイダーのコンソールでハブのリダイレクト URI
http://localhost:5173/login/<provider>/callbackも許可する必要があります。追加されるまではredirect_uri_mismatchエラーで失敗します。 - ブラウザーに残っている古い
civitai-tokenCookie は、ハブなしでも(検証専用のデコードにより)引き続き使用できます。ハブの起動後、次のリクエストでciv-tokenにアップグレードされます。
ブランチのインプレース切り替え
git checkout <branch> を実行するだけです。あとはデーモンが処理するため、.next の rm -rf も手動での再インストールも不要です。
HEAD が移動すると、次のことが行われます。
- 開発サーバーは稼働を続けます。 停止されません。チェックアウト後もメモリ上のモジュールグラフが維持されるため、実際に変更された箇所だけが再コンパイルされます。
- デバウンス(
BRANCH_SWITCH_DEBOUNCE。デフォルトでは HEAD の変更が 3 秒間ないことを確認)により、リベースや短時間での連続した切り替えが落ち着くのを待ちます。 - 避けられない場合に限り再起動します。
pnpm-lock.yamlが変更された場合(インストール後に再起動)、またはprisma/schema.prismaが変更された場合(db:generate後に再起動)です。これらはnode_modulesと生成済みクライアントを入れ替えるため、実行中のプロセスには反映できません。それ以外の変更では再起動しません。 - 再度プリウォームし、次にクリックしたときではなく、デーモン上で再コンパイルが行われるようにします。
なぜ終了させないのか
当初の設計では、チェックアウトによって Turbopack の監視下にあるツリーが変更されるとサーバーが停止する、という仮説に基づき、サーバーを終了させていました。しかし、計測の結果、この仮説は誤りだと分かりました。終了させるほうが単純に悪い結果になります。同じマシンで同じルートを使い、2つのブランチを切り替えた場合の結果は次のとおりです。
/models | /images | |
|---|---|---|
| 終了後のコールドスタート | 42.6s | 3.1s |
| 起動したままでの初回切り替え | 23.0s | 0.3s |
| 起動したままでのその後の切り替え | 7.7-9.2s | 1.0-1.6s |
サーバーはすべての切り替え後も正常に動作していました。再ビルドを要求されなかったルートは、ずっとウォーム状態のままでした。
チェックアウトによってサーバーが停止してしまう場合は、KILL_ON_BRANCH_SWITCH=true を設定すると以前の動作に戻ります。
ブランチごとのビルドディレクトリ(デフォルトでは無効)
PER_BRANCH_DIST_DIR=true を設定すると、next.config.mjs の distDir: process.env.NEXT_DIST_DIR || '.next' により、各ブランチに専用の .next/branches/<slug> が割り当てられます。これはサーバーが実際に再起動するときにだけ意味があります。ブランチを切り替えてもプロセスが維持されるようになったため、現在は無効になっています。また、各ディレクトリは約 4 GB まで増加します。1つの .next を共有すれば、変更されていないモジュールをブランチ間でそのまま有効にできますが、ブランチごとのディレクトリではそれができません。
事前ウォームアップ
PREWARM_ROUTES は、サーバーの準備完了が報告された後、バックグラウンドでコンパイルされます。また、ブランチを切り替えるたびにもコンパイルされます。最初のルートは共有グラフ全体を読み込むため、コールドコンパイルに約 45s かかります。事前ウォームアップにより、その処理をバックグラウンドで実行できます。
デフォルトでは空で、スキルの .env は gitignore されているため、新しくチェックアウトした状態では事前ウォームアップは一切行われません。自分で PREWARM_ROUTES を設定してください(.env.example を参照)。起動時の事前ウォームアップには HEALTH_CHECK_URL の設定も必要です。ログのパターンだけで準備完了を検知した場合は、事前ウォームアップは開始されません。ブランチ切り替え後の事前ウォームアップは、どちらの場合も実行されます。
キャッシュがまったくないブランチでの計測結果は次のとおりです。
| デーモンによる事前ウォームアップ(バックグラウンド) | / 45.2s、/models 3.5s、/images 2.9s — t+62s で完了 |
| その後、これらのページを開く | 0.07-0.14s |
リストにないルート(/articles)を開く | 10.3s |
実際に最初に開くルートを追加してください。リストを長くしても増えるのはバックグラウンドでの処理だけですが、リスト内のルートは順番に処理されます。並列リクエストは同じコンパイラーを取り合うため、最初のルートの準備が遅くなります。無効にするには、空に設定してください。
一覧ページをウォームアップしても、その詳細ページはウォームアップされません。 /models と /models/[id]/[[...slug]] は別々のルートで、それぞれ個別にコンパイルされるため、/models がウォーム状態でも、モデルページを初めて開くと約 30s かかります。そのため、リストには特定のモデル ID を含めてください。ID は任意で、解決先のルートだけが重要です。リダイレクトも追従するため、/models/<id> でスラッグのルートに正しく到達します。
スキルの .env はセッションを開始するたびに再読み込みされます。セッションを維持したままのブランチ切り替えでは再起動されないため、PREWARM_ROUTES の変更が反映されるのは、ロックファイルまたはスキーマの変更によって再起動が発生する切り替え後か、明示的に restart を実行した後です。
キャッシュがなぜそんなに大きいのか
ページごとではありません。このリポジトリで空のストアから計測した結果です:
| ステップ | ストアサイズ | レスポンス |
|---|---|---|
起動 + /api/health | 1 MB | — |
/models (最初の実ページ) | 3995 MB | 49.9s |
/images | 2662 MB | 3.8s |
/articles | 2822 MB | 8.2s |
/bounties | 3762 MB | 28.3s |
最初にコンパイルされるルートが共有グラフ全体 — pages-router の _app とその推移的な世界 — を引き込み、それだけで約 4 GB を消費します。以降の各ルートは数百 MB しか追加せず、合計は増加するのではなく振動します: さらに多くのルートがコンパイルされている間に 3995 から 2662 MB へ減少しました。これはコンパクションが置き換えられたセグメントを回収したためです。ストアの LOG がそれを裏付けています: 最大 340 万キーの単一コミットがあり、その間ずっと MERGE/Compaction パスが実行されています。
つまり、このアプリのワーキングセットはおおよそブランチあたり 2.5〜4 GB であり、これは実際のデータであって、リークではありません。大きいのはグラフが大きいからです (340 万のキャッシュキー、node_modules 内の約 21 万ファイル)。ソースマップが原因ではありません — サンプリングされたセグメントの 0.3% にすぎません。
それはスペースに見合う働きをします: 再起動後の同じルートでコールド 45.8s 対 ウォーム 4.3s。turbopackFileSystemCacheForDev を無効にすると、10 倍遅い再起動と引き換えにディスクを得ることになり、余裕のあるマシンでは間違ったトレードオフです。
ディレクトリは起動ごとに両方の予算に対して LRU で削除されます: DIST_CACHE_KEEP の直近数 (デフォルト 4) と DIST_CACHE_MAX_GB の合計 (デフォルト 40) です。実際に効いているのはサイズ上限です — ブランチあたり約 4 GB のワーキングセットでは、数だけの上限だと 4 ブランチで約 16 GB に達してしまいます。アクティブなブランチは決して削除されません。それだけが予算を超過した場合、デーモンは代わりに警告をログに記録します。
設定項目は .claude/skills/dev-server/.env にあり、これは gitignore されています — すべてを文書化した .env.example をコピーしてください: BRANCH_WATCH_ENABLED、BRANCH_WATCH_INTERVAL、BRANCH_SWITCH_DEBOUNCE、KILL_ON_BRANCH_SWITCH、AUTO_INSTALL、PREWARM_ROUTES、PREWARM_TIMEOUT、PER_BRANCH_DIST_DIR、DIST_CACHE_KEEP、DIST_CACHE_MAX_GB。
ワークツリー
ワークツリーごとに 1 セッション、それぞれが独自のポート (3000、3001、…) を使用します。次のように開始します:
node .claude/skills/dev-server/cli.mjs start /path/to/worktree
すでに処理されていること:
-
ワークツリー自身の
.envはプライマリのものに重なります。 プライマリチェックアウトの.envがすべてのセッションのベースです;<worktree>/.env(または明示的な--env) がキーごとにそれを上書きします。したがって、ワークツリーのファイルは変更したいものだけを再記述すればよく、何も再記述しないファイルは停止ではなく no-op になります。チェーン全体はセッション開始時にEnv: <base> <- <overlay>としてログに記録され、セッションステータスのenvPathsとして返されます (envPathはチェーンの先頭のままです)。これ以前は、それらはマージされていませんでした — ワークツリーのファイルがプライマリを完全に置き換えていたため、2 キーの上書きファイルでは
DATABASE_URLもシークレットもない状態でサーバーが起動し、その原因となった編集とはまったく似つかない形で失敗していました。「プライマリチェックアウト」は、デーモンが起動された場所ではなく git から解決されます。 スキル ディレクトリはコミットされているため、すべてのワークツリーが独自の
daemon.mjsのコピーを持っています — あるワークツリーから起動されたデーモンは、そうでなければそのワークツリーをプライマリとみなし、持っていない.envにフォールバックしてしまいます。git rev-parse --git-common-dirはどのワークツリーの中からでも同じ答えを返します。wt stale/wt rmはプライマリをgit worktree listの最初のエントリから取得します。git はこれがメインワークツリーであることを保証しています; それ以前は、ワークツリー自身の CLI コピーを通してそれらを実行すると、すべてのチェックが反転し、実際のメインチェックアウトが削除可能として提示されていました。 デーモンのプロセスもそこから実行されます。スクリプト、PID ファイル、作業ディレクトリはすべてresolveDaemonHomeを通して解決されます。1 つのデーモンがすべてのツリーを担当し、ツリーの中で動作するデーモンは 稼働中ずっとそのディレクトリを開いたままにするため、最初に PR を完了した人がそのツリーに対してwt rmを実行すると、EBUSY で失敗します。wt rmとwt staleはデーモンにスクリプトのパスと作業ディレクトリを問い合わせ、どちらかが対象ツリー内にある場合は、無関係なシェルのせいにせずデーモンの名前を示します。wt staleはそのツリーを SAFE TO REMOVE の対象外にします。稼働中でも応答しないデーモン、つまりこの変更より前のもので、/に裸の PID を返すものは、稼働していないと確認できないため、すべてのツリーで両方のコマンドをブロックします。wt rm --forceはこの判定を上書きしますが、名前が特定されたホルダーは対象外です。稼働していないデーモンはディレクトリを開いていないため、何もブロックしません。これは適切な応答があるかどうかではなく、トランスポートによって判定されるため、/でエラーを返す稼働中のデーモンも、稼働中として扱われます。🔴 **これは、この変更を含む CLI にのみ適用されます。**スキルディレクトリはコミットされているため、各ワークツリーはそれぞれの
cli.mjsを実行します。変更を取り込む前に作られたツリーでは、そのツリーが最初にデーモンを起動する際、次にデーモンが終了したタイミングで再び自分自身を固定します。そのため、デーモンをプライマリに移すには、シャットダウンと起動スクリプトの更新の両方が必要です。遡及的には適用されず、すでに稼働中のデーモンは再起動するまで影響を受けません。 -
セカンダリポートでの認証。
NEXTAUTH_URL、NEXTAUTH_URL_INTERNAL、NEXT_PUBLIC_BASE_URLはhttp://localhost:<port>に書き換えられるため、3000 以外のセッションでもプライマリに戻されず、ログインできます。 -
セッションごとの独立したブランチ監視と事前ウォームアップ。
-
**ポート割り当てでは、デーモンが所有していないリスナーも検出します。**ポート選択処理はバインドを試みる前に、両方のループバックアドレスへの接続を試みます。そのため、デーモンが追跡できなくなったプロセス(
crashedとマークされたセッションや、ループバック上のその他のローカルサーバー)が使用しているポートは、割り当てずにスキップされます。明示的にポートを指定しても、回避策にはなりません。非ループバックアドレスのみにバインドされたリスナー(next dev -H <lan-ip>)は引き続き検出できません。Windows では、どの方法でも検出できません。 -
**ステータスの表示にかかわらず、セッションは停止されるまでポートを保持します。**ステータスは、デーモンから内部状態を確認できないプロセスについての報告にすぎません。稼働して配信中のセッションが
crashedと表示されたり、再起動中の kill と再バインドの間だけ停止していると表示されたりすることがあります。そのため、ポートの予約はステータスではなくセッションに紐づき、ポートを解放するのはセッションを削除するstop <session-id>です。 -
**すでにセッションがあるワークツリーで
startを実行すると、そのセッションを引き継ぎます。**新しいポートを割り当ててセッションを取り残すのではなく、セッションが使用しているポートで再起動します。既存のワークツリーでは、startとrestartは同じ動作になりました。誤ったポートを選んで失うことはありません。どちらのコマンドも、停止と起動の間にポートが実際に空いているかを確認し、別のプロセス(kill 後も残った自身の孤児プロセスや、別のローカルサーバー)がポートを使用している場合は、セッションに新しいポートを割り当てます。この確認は省略できません。
next devは使用中のポートで失敗せず、警告を出してひそかに別のポートに移動します。そのため、使用中のポートで起動したセッションは、自身が所有していないサーバーに対してurlを報告し、ヘルスチェックに成功する可能性があります。
新しいワークツリーでは、引き続き pnpm install(または node_modules のジャンクション)と git submodule update --init event-engine-common が必要です。
worktree 間でキャッシュを共有できません
直接テストしたところ、ウォーム状態の 12 GB のキャッシュをある worktree から別の worktree にコピーすると、Turbopack は起動時に停止します:
FATAL: An unexpected Turbopack error occurred.
Error [TurbopackInternalError]: failed to create junction point at ".next\dev\node_modules\..."
Caused by: removal of existing symbolic link or junction point failed: The directory is not empty. (os error 145)
独立した問題が 2 つあります:.next/dev/node_modules にはコピー後に維持されないジャンクションポイントが含まれており、キャッシュキーにはプロジェクトの絶対パスが埋め込まれています(1 つの 253 MB セグメント内に C:/Dev/Repos/work/model-share が約 3,000 箇所あります)。そのため、仮に起動できても、ほとんどのエントリーはキャッシュにヒットしません。
複数のセッションで 1 つの .next を共有するのは、さらに悪い結果になります。2 つの開発サーバーが同じ LSM ストアに書き込むと、破損します。
そのため、新しい worktree ではコールドビルドが 1 回発生します。対策は事前ウォームアップです。セッションを開始し、最初のクリックを待つのではなく、バックグラウンドでデーモンに処理させます。
RGB プロキシと複数セッション
rgb-proxy/index.mjs は 3 つすべての色と civitai-dev.cyan エイリアスを、ハードコードされた http://localhost:3000 に登録します。そのため、色のホスト名は常にポート 3000 を使用しているセッションに到達し、他のセッションには localhost:<port> でのみアクセスできます。別の worktree に気付かず接続してしまう URL を表示するのではなく、TUI がセッションの状態を示します。
Windows Defender
リアルタイム保護は、node_modules 内の約 210k 個のファイルすべてと、ビルドキャッシュへの書き込みのたびにスキャンします。管理者権限で起動した PowerShell から、次のコマンドを一度実行してください:
powershell -File .claude\skills\dev-server\scripts\defender-exclusions.ps1
セルフテスト
デーモンの複雑な不変条件は、プレーンな Node.js のセルフテストで固定されています。対象ファイルに変更を加えたら実行してください。数ミリ秒で完了し、デーモンは不要です:
node .claude/skills/dev-server/scripts/branch-watch.selftest.mjs
各ケースはこのリポジトリで測定された形状に基づいており、ミューテーションチェックも行われます。対象コードを元に戻すと、ハングせずに誤った数値が返ります。
メモ
- CLI コマンドを実行すると、デーモンが自動的に起動します
- セッションは明示的に停止するか、デーモンがシャットダウンするまで維持されます
- ログはメモリ内に保持されます(セッションあたり最大 2000 行)