node-inspect-debugger について
Node.js 開発者向けのデバッグスキルです。組み込みの V8 インスペクターやスクリプト可能な CDP クライアントを使い、ブレークポイント、スコープ確認、実行中プロセスへの接続、CPU・ヒープのプロファイリングを扱います。
主な機能
- 01組み込み node inspect CLI でデバッグ
- 02ブレークポイント設定とスコープ確認
- 03実行中の Node.js プロセスに接続
- 04CDP によるデバッグ自動化
- 05CPU プロファイルとヒープスナップショットの取得
活用シーン
- 01失敗した Node テストの中間状態を確認
- 02描画前の UI 状態を調査
- 03稼働中の開発サーバーをデバッグ
- 04CPU やヒープの診断情報を収集
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ nousresearch/ hermes-agent --skill node-inspect-debuggerインストール後、Claude Code で/node-inspect-debuggerを入力して読み込まれたか確認します。
詳細
Node.js Inspect Debugger
概要
console.log だけでは足りない場合は、ターミナルから Node 組み込みの V8 インスペクターをプログラムで操作できます。実際のブレークポイント、ステップイン/ステップオーバー/ステップアウト、コールスタックの追跡、ローカルスコープとクロージャースコープのダンプ、一時停止中のフレームでの任意の式の評価ができます。
2 つのツールから選べます。
node inspect— 組み込みで、インストール不要の CLI REPL です。手早く調べるのに最適です。ndb/ CDP viachrome-remote-interface— Node/Python からスクリプトで操作できます。多数のブレークポイントを自動化したい場合、実行をまたいで状態を収集したい場合、またはエージェントのループから非対話形式でデバッグしたい場合に最適です。
まずは node inspect を使ってください。 常に利用でき、REPL も高速です。
使用する場面
- Node のテストが失敗し、中間状態を確認したい場合
- ui-tui がクラッシュしたり、誤動作したりして、レンダリング前の React/Ink の状態を調べたい場合
- tui_gateway の子プロセス(
_SlashWorker、PTY ブリッジワーカー)が正常に動作しない場合 - パッチを当てずに
console.logからはアクセスできないクロージャー内の値を確認したい場合 - パフォーマンス調査:実行中のプロセスにアタッチして、CPU プロファイルまたはヒープスナップショットを取得したい場合
使用しない場面: 1 分以内に console.log で解決できること。ブレークポイントを使ったデバッグは手間がかかるため、十分な効果が見込める場合に使ってください。
クイックリファレンス:node inspect REPL
最初の行で一時停止して起動します。
node inspect path/to/script.js
# or with tsx
node --inspect-brk $(which tsx) path/to/script.ts
debug> プロンプトでは、次のコマンドを使えます。
| コマンド | 操作 |
|---|---|
c または cont | 続行 |
n または next | ステップオーバー |
s または step | ステップイン |
o または out | ステップアウト |
pause | 実行中のコードを一時停止 |
sb('file.js', 42) | file.js の 42 行目にブレークポイントを設定 |
sb(42) | 現在のファイルの 42 行目にブレークポイントを設定 |
sb('functionName') | 関数が呼び出されたときにブレーク |
cb('file.js', 42) | ブレークポイントを解除 |
breakpoints | すべてのブレークポイントを一覧表示 |
bt | バックトレース(コールスタック) |
list(5) | 現在位置の前後のソースコードを 5 行表示 |
watch('expr') | 一時停止のたびに expr を評価 |
watchers | 監視中の式を表示 |
repl | 現在のスコープで REPL に入る(Ctrl+C で REPL を終了) |
exec expr | 式を 1 回評価 |
restart | スクリプトを再起動 |
kill | スクリプトを終了 |
.exit | デバッガーを終了 |
repl サブモードでは: ローカル変数やクロージャー変数へのアクセスを含め、任意の JS 式を入力できます。Ctrl+C を押すと debug> に戻ります。
実行中のプロセスへのアタッチ
プロセスがすでに実行中の場合(長時間稼働する開発サーバーや TUI ゲートウェイなど):
# 1. Send SIGUSR1 to enable the inspector on an existing process
kill -SIGUSR1 <pid>
# Node prints: Debugger listening on ws://127.0.0.1:9229/<uuid>
# 2. Attach the debugger CLI
node inspect -p <pid>
# or by URL
node inspect ws://127.0.0.1:9229/<uuid>
インスペクターを使ってプロセスを最初から起動するには:
node --inspect script.js # listen on 127.0.0.1:9229, keep running
node --inspect-brk script.js # listen AND pause on first line
node --inspect=0.0.0.0:9230 script.js # custom host:port
tsx 経由の TypeScript の場合:
node --inspect-brk --import tsx script.ts
# or older tsx
node --inspect-brk -r tsx/cjs script.ts
プログラムによる CDP(ターミナルからのスクリプト操作)
自動化したい場合(多数のブレークポイントの設定、スコープ状態の取得、再現手順のスクリプト化など)は、chrome-remote-interface を使います。
npm i -g chrome-remote-interface # or project-local
# Start your target:
node --inspect-brk=9229 target.js &
ドライバースクリプト(~/.hermes/cache/scratch/cdp-debug.js として保存):
const CDP = require('chrome-remote-interface');
(async () => {
const client = await CDP({ port: 9229 });
const { Debugger, Runtime } = client;
Debugger.paused(async ({ callFrames, reason }) => {
const top = callFrames[0];
console.log(`PAUSED: ${reason} @ ${top.url}:${top.location.lineNumber + 1}`);
// Walk scopes for locals
for (const scope of top.scopeChain) {
if (scope.type === 'local' || scope.type === 'closure') {
const { result } = await Runtime.getProperties({
objectId: scope.object.objectId,
ownProperties: true,
});
for (const p of result) {
console.log(` ${scope.type}.${p.name} =`, p.value?.value ?? p.value?.description);
}
}
}
// Evaluate an expression in the paused frame
const { result } = await Debugger.evaluateOnCallFrame({
callFrameId: top.callFrameId,
expression: 'typeof state !== "undefined" ? JSON.stringify(state) : "n/a"',
});
console.log('state =', result.value ?? result.description);
await Debugger.resume();
});
await Runtime.enable();
await Debugger.enable();
// Set a breakpoint by URL regex + line
await Debugger.setBreakpointByUrl({
urlRegex: '.*app\\.tsx$',
lineNumber: 119, // 0-indexed
columnNumber: 0,
});
await Runtime.runIfWaitingForDebugger();
})();
実行します。
node ~/.hermes/cache/scratch/cdp-debug.js
Hermes 固有の注意:chrome-remote-interface は ui-tui/package.json に含まれていません。プロジェクトを汚したくない場合は、一時的な場所にインストールしてください。
mkdir -p ~/.hermes/cache/scratch/cdp-tools && cd ~/.hermes/cache/scratch/cdp-tools && npm i chrome-remote-interface
NODE_PATH=~/.hermes/cache/scratch/cdp-tools/node_modules node ~/.hermes/cache/scratch/cdp-debug.js
Hermes ui-tui のデバッグ
TUI は Ink + tsx で構築されています。よくあるシナリオは 2 つあります。
開発環境で単一の Ink コンポーネントをデバッグする
ui-tui/package.json には npm run dev(tsx --watch)があります。tsx を直接実行して --inspect-brk を追加します。
cd <hermes-agent-repo>/ui-tui
npm run build # produce dist/ once so transpile isn't needed on first load
node --inspect-brk dist/entry.js
# In another terminal:
node inspect -p <node pid>
その後、debug> で次を実行します。
sb('dist/app.js', 220) # or wherever the suspect render is
cont
一時停止したら、repl に入り、props、state の ref、useInput ハンドラーの値などを調べます。
実行中の hermes --tui をデバッグする
TUI は Python CLI から Node を起動します。最も簡単な方法は次のとおりです。
# 1. Launch TUI
hermes --tui &
TUI_PID=$(pgrep -f 'ui-tui/dist/entry' | head -1)
# 2. Enable inspector on that Node PID
kill -SIGUSR1 "$TUI_PID"
# 3. Find the WS URL
curl -s http://127.0.0.1:9229/json/list | jq -r '.[0].webSocketDebuggerUrl'
# 4. Attach
node inspect ws://127.0.0.1:9229/<uuid>
TUI を操作(ウィンドウに文字を入力)すると実行が進み続けます。デバッガーでは、任意の sb(...) でブレークポイントを設定して一時停止できます。
_SlashWorker/PTY 子プロセスをデバッグする
これらは Node ではなく Python です。python-debugpy スキルを使ってください。このスキルを使うのは、Node 部分(Ink UI、tui_gateway クライアント、ui-tui/ 内の tsx-run テスト)のみです。
デバッガーで Vitest のテストを実行する
cd <hermes-agent-repo>/ui-tui
# Run a single test file paused on entry
node --inspect-brk ./node_modules/vitest/vitest.mjs run --no-file-parallelism src/app/foo.test.tsx
別のターミナルで、node inspect -p <pid> を実行し、続けて sb('src/app/foo.tsx', 42)、cont を実行します。
ワーカーを 1 つだけにするため、--no-file-parallelism(vitest)または --runInBand(jest)を使用します。ワーカープールのデバッグは大変です。
ヒープスナップショットと CPU プロファイル(非対話型)
上記の CDP ドライバーで、Debugger を HeapProfiler / Profiler に置き換えます。
// CPU profile for 5 seconds
await client.Profiler.enable();
await client.Profiler.start();
await new Promise(r => setTimeout(r, 5000));
const { profile } = await client.Profiler.stop();
require('fs').writeFileSync('~/.hermes/cache/scratch/cpu.cpuprofile', JSON.stringify(profile));
// Open ~/.hermes/cache/scratch/cpu.cpuprofile in Chrome DevTools → Performance tab
// Heap snapshot
await client.HeapProfiler.enable();
const chunks = [];
client.HeapProfiler.addHeapSnapshotChunk(({ chunk }) => chunks.push(chunk));
await client.HeapProfiler.takeHeapSnapshot({ reportProgress: false });
require('fs').writeFileSync('~/.hermes/cache/scratch/heap.heapsnapshot', chunks.join(''));
よくある落とし穴
-
TS ソースの行番号が正しくない。 ブレークポイントは
.tsではなく、コンパイル後の JS でヒットします。(a)ビルドされたdist/*.jsにブレークポイントを設定するか、(b)ソースマップを有効にして(node --enable-source-maps)、sb('src/app.tsx', N)を使用します。ただし、ソースマップに対応した CDP クライアントでのみ使用できます。node inspectCLI は対応していません。 -
--inspectと--inspect-brkの違い。--inspectはインスペクターを起動しますが、一時停止はしません。接続が遅れると、最初のブレークポイントに到達する前にスクリプトが進んでしまいます。コードの実行前にブレークポイントを設定する必要がある場合は、--inspect-brkを使用します。 -
ポートの競合。 デフォルトは
9229です。複数の Node プロセスをインスペクトする場合は、--inspect=0(ランダムポート)を指定し、/json/listから実際の URL を読み取ります。curl -s http://127.0.0.1:9229/json/list # lists all inspectable targets on the host -
子プロセス。 親プロセスに
--inspectを指定しても、その子プロセスはインスペクトされません。すべての子プロセスに設定を引き継ぐには、NODE_OPTIONS='--inspect-brk' node parent.jsを使用します。すべての子プロセスにそれぞれ異なるポートが必要な点に注意してください(NODE_OPTIONS='--inspect'が引き継がれると、Node はポート番号を自動的に増やします)。 -
バックグラウンドプロセスの停止。 対象が一時停止しているときに
node inspectをCtrl+Cで終了しても、対象は一時停止したままです。先にcontを実行するか、対象を明示的にkillしてください。 -
エージェントのターミナル経由での
node inspectの実行。 これは PTY に対応した REPL です。Hermes では、terminal(pty=true)を使って起動するか、background=trueとprocess(action='submit', data='...')を組み合わせて起動してください。PTY を使わないフォアグラウンドモードは、単発のコマンドでは機能しますが、対話的なステップ実行には使えません。 -
セキュリティ。
--inspect=0.0.0.0:9229は、任意のコードを実行できる状態にします。隔離されたネットワークを使用している場合を除き、常に127.0.0.1(デフォルト)にバインドしてください。
検証チェックリスト
デバッグセッションの設定後、次の点を確認します。
-
curl -s http://127.0.0.1:9229/json/listが、想定した対象だけを返す - 最初のブレークポイントで実際に停止する(停止しない場合は、
--inspect-brkを指定し忘れたか、実行完了後に接続した可能性があります) - 一時停止中に表示されるソースの一覧に正しいファイルが表示される(不一致の場合はソースマップに問題があります。「よくある落とし穴」1 を参照)
-
replでexec process.pidを実行すると、接続するつもりだった PID が返される
すぐに使えるレシピ
「行 X でこの変数が undefined なのはなぜですか?」
node --inspect-brk script.js &
node inspect -p $!
# debug>
sb('script.js', X)
cont
# paused. Now:
repl
> myVariable
> Object.keys(this)
「この関数に至る呼び出し経路は?」
debug> sb('suspectFn')
debug> cont
# paused on entry
debug> bt
「この非同期処理の連鎖が停止します。どこで止まっていますか?」
# Start with --inspect (no -brk), let it run to the hang, then:
debug> pause
debug> bt
# Now you see the stuck frame