node-inspect-debugger

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Node.js Inspect Debugger

Node.js Inspect 调试器

Overview

概述

When
console.log
isn't enough, drive Node's built-in V8 inspector programmatically from the terminal. You get real breakpoints, step in/over/out, call-stack walking, local/closure scope dumps, and arbitrary expression evaluation in the paused frame.
Two tools, pick one:
  • node inspect
    — built-in, zero install, CLI REPL. Best for quick poking.
  • ndb
    / CDP via
    chrome-remote-interface
    — scriptable from Node/Python; best when you want to automate many breakpoints, collect state across runs, or debug non-interactively from an agent loop.
Prefer
node inspect
first.
It's always available and the REPL is fast.
console.log
不足以排查问题时,可以通过终端以编程方式调用 Node 内置的 V8 调试器。你可以设置真实断点、单步进入/跳过/退出、遍历调用栈、查看局部/闭包作用域信息,以及在暂停的帧中执行任意表达式求值。
提供两种工具,任选其一:
  • node inspect
    —— 内置工具,无需额外安装,CLI 交互式解释器。适合快速调试。
  • ndb
    / 基于
    chrome-remote-interface
    的 CDP
    —— 可通过 Node/Python 编写脚本;适合需要自动化设置多个断点、跨运行周期收集状态,或通过代理循环进行非交互式调试的场景。
优先推荐使用
node inspect
。它随时可用,且交互式解释器响应迅速。

When to Use

使用场景

  • A Node test fails and you need to see intermediate state
  • ui-tui crashes or behaves wrong and you want to inspect React/Ink state pre-render
  • tui_gateway child processes (
    _SlashWorker
    , PTY bridge workers) misbehave
  • You need to inspect a value in a closure that
    console.log
    can't reach without patching
  • Perf: attach to a running process to capture a CPU profile or heap snapshot
Don't use for: things
console.log
solves in under a minute. Breakpoint-driven debugging is heavier; use it when the payoff is real.
  • Node 测试用例失败,需要查看中间状态
  • ui-tui 崩溃或行为异常,想要检查 React/Ink 渲染前的状态
  • tui_gateway 子进程(
    _SlashWorker
    、PTY 桥接工作进程)行为异常
  • 需要查看
    console.log
    无法触及的闭包中的值,且不想修改代码
  • 性能分析:附加到运行中的进程以捕获 CPU 分析报告或堆快照
不建议使用的场景:
console.log
能在一分钟内解决的问题。基于断点的调试开销更大,仅在能获得实际收益时使用。

Quick Reference:
node inspect
REPL

node inspect
交互式解释器速查

Launch paused on first line:
bash
node inspect path/to/script.js
启动并在第一行暂停:
bash
node inspect path/to/script.js

or with tsx

或配合 tsx 使用

node --inspect-brk $(which tsx) path/to/script.ts

The `debug>` prompt accepts:

| Command | Action |
|---|---|
| `c` or `cont` | continue |
| `n` or `next` | step over |
| `s` or `step` | step into |
| `o` or `out` | step out |
| `pause` | pause running code |
| `sb('file.js', 42)` | set breakpoint at file.js line 42 |
| `sb(42)` | set breakpoint at line 42 of current file |
| `sb('functionName')` | break when function is called |
| `cb('file.js', 42)` | clear breakpoint |
| `breakpoints` | list all breakpoints |
| `bt` | backtrace (call stack) |
| `list(5)` | show 5 lines of source around current position |
| `watch('expr')` | evaluate expr on every pause |
| `watchers` | show watched expressions |
| `repl` | drop into REPL in current scope (Ctrl+C to exit REPL) |
| `exec expr` | evaluate expression once |
| `restart` | restart script |
| `kill` | kill the script |
| `.exit` | quit debugger |

**In the `repl` sub-mode:** type any JS expression, including access to locals/closure variables. `Ctrl+C` exits back to `debug>`.
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')` | 每次暂停时计算指定表达式 |
| `watchers` | 显示所有监视的表达式 |
| `repl` | 进入当前作用域的交互式解释器(按 Ctrl+C 退出) |
| `exec expr` | 执行一次指定表达式 |
| `restart` | 重启脚本 |
| `kill` | 终止脚本 |
| `.exit` | 退出调试器 |

**在 `repl` 子模式下:** 可输入任意 JS 表达式,包括访问局部变量/闭包变量。按 `Ctrl+C` 返回 `debug>` 提示符。

Attaching to a Running Process

附加到运行中的进程

When the process is already running (e.g. a long-lived dev server or the TUI gateway):
bash
undefined
当进程已在运行时(例如长期运行的开发服务器或 TUI 网关):
bash
undefined

1. Send SIGUSR1 to enable the inspector on an existing process

1. 发送 SIGUSR1 信号,为现有进程启用调试器

kill -SIGUSR1 <pid>
kill -SIGUSR1 <pid>

Node prints: Debugger listening on ws://127.0.0.1:9229/<uuid>

Node 会输出:Debugger listening on ws://127.0.0.1:9229/<uuid>

2. Attach the debugger CLI

2. 附加调试器 CLI

node inspect -p <pid>
node inspect -p <pid>

or by URL

或通过 URL 附加

node inspect ws://127.0.0.1:9229/<uuid>

To start a process with the inspector from the beginning:

```bash
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
For TypeScript via tsx:
bash
node --inspect-brk --import tsx script.ts
node inspect ws://127.0.0.1:9229/<uuid>

从启动时就启用调试器:

```bash
node --inspect script.js           # 在 127.0.0.1:9229 监听,保持运行
node --inspect-brk script.js       # 监听并在第一行暂停
node --inspect=0.0.0.0:9230 script.ts   # 自定义主机:端口
配合 tsx 调试 TypeScript:
bash
node --inspect-brk --import tsx script.ts

or older tsx

或旧版本 tsx

node --inspect-brk -r tsx/cjs script.ts
undefined
node --inspect-brk -r tsx/cjs script.ts
undefined

Programmatic CDP (scripting from terminal)

程序化 CDP(终端脚本调试)

When you want to automate — set many breakpoints, capture scope state, script a repro — use
chrome-remote-interface
:
bash
npm i -g chrome-remote-interface        # or project-local
当需要自动化操作——设置多个断点、捕获作用域状态、编写复现脚本——时,使用
chrome-remote-interface
bash
npm i -g chrome-remote-interface        # 或安装到项目本地

Start your target:

启动目标进程:

node --inspect-brk=9229 target.js &

Driver script (save as `/tmp/cdp-debug.js`):

```javascript
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();
})();
Run it:
bash
node /tmp/cdp-debug.js
Hermes-specific note:
chrome-remote-interface
is NOT in
ui-tui/package.json
. Install it to a throwaway location if you don't want to dirty the project:
bash
mkdir -p /tmp/cdp-tools && cd /tmp/cdp-tools && npm i chrome-remote-interface
NODE_PATH=/tmp/cdp-tools/node_modules node /tmp/cdp-debug.js
node --inspect-brk=9229 target.js &

驱动脚本(保存为 `/tmp/cdp-debug.js`):

```javascript
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(`已暂停:${reason} @ ${top.url}:${top.location.lineNumber + 1}`);

    // 遍历作用域获取局部变量
    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);
        }
      }
    }

    // 在暂停的帧中执行表达式
    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();

  // 通过 URL 正则 + 行号设置断点
  await Debugger.setBreakpointByUrl({
    urlRegex: '.*app\\.tsx$',
    lineNumber: 119,       // 从0开始计数
    columnNumber: 0,
  });

  await Runtime.runIfWaitingForDebugger();
})();
运行脚本:
bash
node /tmp/cdp-debug.js
Hermes 专属说明:
chrome-remote-interface
不在
ui-tui/package.json
中。如果不想污染项目依赖,可安装到临时目录:
bash
mkdir -p /tmp/cdp-tools && cd /tmp/cdp-tools && npm i chrome-remote-interface
NODE_PATH=/tmp/cdp-tools/node_modules node /tmp/cdp-debug.js

Debugging Hermes ui-tui

调试 Hermes ui-tui

The TUI is built Ink + tsx. Two common scenarios:
TUI 基于 Ink + tsx 构建。以下是两种常见场景:

Debugging a single Ink component under dev

开发模式下调试单个 Ink 组件

ui-tui/package.json
has
npm run dev
(tsx --watch). Add
--inspect-brk
by running tsx directly:
bash
cd /home/bb/hermes-agent/ui-tui
npm run build    # produce dist/ once so transpile isn't needed on first load
node --inspect-brk dist/entry.js
ui-tui/package.json
中有
npm run dev
(tsx --watch)。直接运行 tsx 并添加
--inspect-brk
bash
cd /home/bb/hermes-agent/ui-tui
npm run build    # 先执行一次构建生成 dist/,避免首次加载时需要转译
node --inspect-brk dist/entry.js

In another terminal:

在另一个终端中:

node inspect -p <node pid>

Then inside `debug>`:
sb('dist/app.js', 220) # or wherever the suspect render is cont

When it pauses, `repl` → inspect `props`, state refs, `useInput` handler values, etc.
node inspect -p <node pid>

然后在 `debug>` 提示符中:
sb('dist/app.js', 220) # 或设置到可疑的渲染代码位置 cont

暂停后,输入 `repl` → 检查 `props`、状态引用、`useInput` 处理器值等。

Debugging a running
hermes --tui

调试运行中的
hermes --tui

The TUI spawns Node from the Python CLI. Easiest path:
bash
undefined
TUI 会从 Python CLI 启动 Node 进程。最简单的方法:
bash
undefined

1. Launch TUI

1. 启动 TUI

hermes --tui & TUI_PID=$(pgrep -f 'ui-tui/dist/entry' | head -1)
hermes --tui & TUI_PID=$(pgrep -f 'ui-tui/dist/entry' | head -1)

2. Enable inspector on that Node PID

2. 为该 Node PID 启用调试器

kill -SIGUSR1 "$TUI_PID"
kill -SIGUSR1 "$TUI_PID"

3. Find the WS URL

3. 获取 WS URL

curl -s http://127.0.0.1:9229/json/list | jq -r '.[0].webSocketDebuggerUrl'
curl -s http://127.0.0.1:9229/json/list | jq -r '.[0].webSocketDebuggerUrl'

4. Attach

4. 附加调试器

node inspect ws://127.0.0.1:9229/<uuid>

Interacting with the TUI (typing in its window) continues to advance execution; your debugger can pause it on a breakpoint at any `sb(...)`.
node inspect ws://127.0.0.1:9229/<uuid>

与 TUI 交互(在其窗口中输入内容)会继续执行代码;你的调试器可以通过 `sb(...)` 设置的断点随时暂停它。

Debugging
_SlashWorker
/ PTY child processes

调试
_SlashWorker
/ PTY 子进程

Those are Python, not Node — use the
python-debugpy
skill for them. Only Node portions (Ink UI, tui_gateway client, tsx-run tests under
ui-tui/
) use this skill.
这些是 Python 进程,而非 Node 进程——请使用
python-debugpy
技能进行调试。只有 Node 相关部分(Ink UI、tui_gateway 客户端、
ui-tui/
下的 tsx-run 测试)使用本技能。

Running Vitest Tests Under the Debugger

在调试器下运行 Vitest 测试

bash
cd /home/bb/hermes-agent/ui-tui
bash
cd /home/bb/hermes-agent/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

In another terminal: `node inspect -p <pid>`, then `sb('src/app/foo.tsx', 42)`, `cont`.

Use `--no-file-parallelism` (vitest) or `--runInBand` (jest) so only one worker exists — debugging a pool is painful.
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`。

使用 `--no-file-parallelism`(vitest)或 `--runInBand`(jest)确保仅存在一个工作进程——调试进程池会非常麻烦。

Heap Snapshots & CPU Profiles (Non-interactive)

堆快照与 CPU 分析报告(非交互式)

From the CDP driver above, swap Debugger for
HeapProfiler
/
Profiler
:
javascript
// 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('/tmp/cpu.cpuprofile', JSON.stringify(profile));
// Open /tmp/cpu.cpuprofile in Chrome DevTools → Performance tab
javascript
// Heap snapshot
await client.HeapProfiler.enable();
const chunks = [];
client.HeapProfiler.addHeapSnapshotChunk(({ chunk }) => chunks.push(chunk));
await client.HeapProfiler.takeHeapSnapshot({ reportProgress: false });
require('fs').writeFileSync('/tmp/heap.heapsnapshot', chunks.join(''));
在上述 CDP 驱动脚本中,将 Debugger 替换为
HeapProfiler
/
Profiler
javascript
undefined

Common Pitfalls

CPU 分析(持续5秒)

  1. Wrong line numbers in TS source. Breakpoints hit the emitted JS, not the
    .ts
    . Either (a) break in the built
    dist/*.js
    , or (b) enable sourcemaps (
    node --enable-source-maps
    ) and use
    sb('src/app.tsx', N)
    — but only with CDP clients that follow sourcemaps.
    node inspect
    CLI does not.
  2. --inspect
    vs
    --inspect-brk
    .
    --inspect
    starts the inspector but doesn't pause; your script races past your first breakpoint if you attach too late. Use
    --inspect-brk
    when you need to set breakpoints before any code runs.
  3. Port collisions. Default is
    9229
    . If multiple Node processes are inspecting, pass
    --inspect=0
    (random port) and read the actual URL from
    /json/list
    :
    bash
    curl -s http://127.0.0.1:9229/json/list   # lists all inspectable targets on the host
  4. Child processes.
    --inspect
    on a parent does NOT inspect its children. Use
    NODE_OPTIONS='--inspect-brk' node parent.js
    to propagate to every child; be aware they all need unique ports (Node auto-increments when
    NODE_OPTIONS='--inspect'
    is inherited).
  5. Background kills. If you
    Ctrl+C
    out of
    node inspect
    while the target is paused, the target stays paused. Either
    cont
    first, or
    kill
    the target explicitly.
  6. Running
    node inspect
    through an agent terminal.
    It's a PTY-friendly REPL. In Hermes, launch it with
    terminal(pty=true)
    or
    background=true
    +
    process(action='submit', data='...')
    . Non-PTY foreground mode will work for one-shot commands but not for interactive stepping.
  7. Security.
    --inspect=0.0.0.0:9229
    exposes arbitrary code execution. Always bind to
    127.0.0.1
    (the default) unless you have an isolated network.
await client.Profiler.enable(); await client.Profiler.start(); await new Promise(r => setTimeout(r, 5000)); const { profile } = await client.Profiler.stop(); require('fs').writeFileSync('/tmp/cpu.cpuprofile', JSON.stringify(profile));

Verification Checklist

在 Chrome DevTools 的 Performance 面板中打开 /tmp/cpu.cpuprofile

After setting up a debug session, verify:
  • curl -s http://127.0.0.1:9229/json/list
    returns exactly the target you expect
  • First breakpoint actually hits (if it doesn't, you likely missed
    --inspect-brk
    or attached after execution completed)
  • Source listing at pause shows the right file (mismatch = sourcemap issue, see pitfall 1)
  • exec process.pid
    in
    repl
    returns the PID you meant to attach to

```javascript

One-Shot Recipes

堆快照

"Why is this variable undefined at line X?"
bash
node --inspect-brk script.js &
node inspect -p $!
await client.HeapProfiler.enable(); const chunks = []; client.HeapProfiler.addHeapSnapshotChunk(({ chunk }) => chunks.push(chunk)); await client.HeapProfiler.takeHeapSnapshot({ reportProgress: false }); require('fs').writeFileSync('/tmp/heap.heapsnapshot', chunks.join(''));
undefined

debug>

常见陷阱

sb('script.js', X) cont
  1. TS 源代码行号不匹配。断点命中的是编译后的 JS,而非
    .ts
    文件。解决方法:(a) 在编译后的
    dist/*.js
    中设置断点;或 (b) 启用 sourcemap(
    node --enable-source-maps
    )并使用
    sb('src/app.tsx', N)
    ——但仅适用于支持 sourcemap 的 CDP 客户端。
    node inspect
    CLI 不支持此功能。
  2. --inspect
    --inspect-brk
    的区别
    --inspect
    启动调试器但不暂停;如果附加过晚,脚本会跳过你的第一个断点。如果需要在代码运行前设置断点,请使用
    --inspect-brk
  3. 端口冲突。默认端口是
    9229
    。如果多个 Node 进程启用调试,传递
    --inspect=0
    (随机端口)并从
    /json/list
    读取实际 URL:
    bash
    curl -s http://127.0.0.1:9229/json/list   # 列出主机上所有可调试的目标
  4. 子进程调试。父进程使用
    --inspect
    不会自动启用子进程的调试。使用
    NODE_OPTIONS='--inspect-brk' node parent.js
    将调试配置传播给所有子进程;注意它们都需要唯一端口(当继承
    NODE_OPTIONS='--inspect'
    时,Node 会自动递增端口)。
  5. 后台进程终止问题。如果在目标进程暂停时按
    Ctrl+C
    退出
    node inspect
    ,目标进程会保持暂停状态。请先执行
    cont
    恢复运行,或显式
    kill
    目标进程。
  6. 通过代理终端运行
    node inspect
    。它是兼容 PTY 的交互式解释器。在 Hermes 中,使用
    terminal(pty=true)
    background=true
    +
    process(action='submit', data='...')
    启动。非 PTY 前台模式仅适用于一次性命令,不适用于交互式单步调试。
  7. 安全问题
    --inspect=0.0.0.0:9229
    会暴露任意代码执行权限。除非处于隔离网络环境,否则始终绑定到默认的
    127.0.0.1

paused. Now:

验证清单

repl
myVariable Object.keys(this)

**"What's the call path into this function?"**
debug> sb('suspectFn') debug> cont
设置调试会话后,请验证以下内容:
  • curl -s http://127.0.0.1:9229/json/list
    返回的目标与预期完全一致
  • 第一个断点确实触发(如果未触发,可能是遗漏了
    --inspect-brk
    或在执行完成后才附加调试器)
  • 暂停时显示的源代码文件正确(不匹配则是 sourcemap 问题,参见陷阱1)
  • repl
    中执行
    exec process.pid
    返回的 PID 与要附加的进程一致

paused on entry

一键式调试方案

debug> bt

**"This async chain hangs — where?"**
“为什么第X行的变量是 undefined?”
bash
node --inspect-brk script.js &
node inspect -p $!

Start with --inspect (no -brk), let it run to the hang, then:

debug>

debug> pause debug> bt
sb('script.js', X) cont

Now you see the stuck frame

已暂停。现在:

undefined
repl
myVariable Object.keys(this)

**“调用这个函数的路径是什么?”**
debug> sb('suspectFn') debug> cont

在函数入口处暂停

debug> bt

**“这个异步链卡住了——在哪里?”**

使用 --inspect 启动(不带 -brk),让它运行到卡住状态,然后:

debug> pause debug> bt

现在可以看到卡住的帧

undefined