Buckets:
| name: node-inspect-debugger | |
| description: "Debug Node.js via --inspect + Chrome DevTools Protocol CLI." | |
| version: 1.0.0 | |
| author: Hermes Agent | |
| license: MIT | |
| platforms: [linux, macos, windows] | |
| metadata: | |
| hermes: | |
| tags: [debugging, nodejs, node-inspect, cdp, breakpoints, ui-tui] | |
| related_skills: [systematic-debugging, python-debugpy, debugging-hermes-tui-commands] | |
| # Node.js Inspect Debugger | |
| ## 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. | |
| ## 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. | |
| ## Quick Reference: `node inspect` REPL | |
| Launch paused on first line: | |
| ```bash | |
| node inspect path/to/script.js | |
| # or with 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>`. | |
| ## Attaching to a Running Process | |
| When the process is already running (e.g. a long-lived dev server or the TUI gateway): | |
| ```bash | |
| # 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> | |
| ``` | |
| 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 | |
| # or older tsx | |
| node --inspect-brk -r tsx/cjs script.ts | |
| ``` | |
| ## Programmatic CDP (scripting from terminal) | |
| 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 | |
| # 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 | |
| ``` | |
| ## Debugging Hermes ui-tui | |
| The TUI is built Ink + tsx. Two common scenarios: | |
| ### Debugging a single Ink component under dev | |
| `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 | |
| # 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. | |
| ### Debugging a running `hermes --tui` | |
| The TUI spawns Node from the Python CLI. Easiest path: | |
| ```bash | |
| # 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> | |
| ``` | |
| Interacting with the TUI (typing in its window) continues to advance execution; your debugger can pause it on a breakpoint at any `sb(...)`. | |
| ### Debugging `_SlashWorker` / PTY child processes | |
| 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. | |
| ## Running Vitest Tests Under the Debugger | |
| ```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. | |
| ## Heap Snapshots & CPU Profiles (Non-interactive) | |
| 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('')); | |
| ``` | |
| ## Common Pitfalls | |
| 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. | |
| ## Verification Checklist | |
| 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 | |
| ## One-Shot Recipes | |
| **"Why is this variable undefined at line X?"** | |
| ```bash | |
| node --inspect-brk script.js & | |
| node inspect -p $! | |
| # debug> | |
| sb('script.js', X) | |
| cont | |
| # paused. Now: | |
| repl | |
| > myVariable | |
| > Object.keys(this) | |
| ``` | |
| **"What's the call path into this function?"** | |
| ``` | |
| debug> sb('suspectFn') | |
| debug> cont | |
| # paused on entry | |
| debug> bt | |
| ``` | |
| **"This async chain hangs — where?"** | |
| ``` | |
| # Start with --inspect (no -brk), let it run to the hang, then: | |
| debug> pause | |
| debug> bt | |
| # Now you see the stuck frame | |
| ``` | |
Xet Storage Details
- Size:
- 10.9 kB
- Xet hash:
- 48348378f8694dfb317e08a5363a4f763f73f5ec91e44815716fc414e9c6b43e
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.