agentsclimarketplace

Debug node

Skill spotcircuit/rebar/.claude/skills/software-development/debug-node

Debug Node.js (and tsx/TypeScript) interactively via `node inspect` REPL or scripted Chrome DevTools Protocol (CDP) clients. Use when console.log is insufficient — set real breakpoints, walk the call stack, dump locals/closures, evaluate expressions in paused frames, attach to a running process, or capture CPU/heap profiles. Pairs with rebar's CDP-based browser-harness.sh and complements superpowers:systematic-debugging (which is language-agnostic process; this is the Node-specific tooling reference).From its SKILL.md

Install
npx -y skills add spotcircuit/rebar --skill debug-node

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 3 stars3 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

7.4 KB, as published. Nobody here has run it

debug-node

When to use

  • A Node/tsx test fails and you need intermediate state that logs can't reveal.
  • A long-running Node process (dev server, worker) misbehaves and can't be restarted clean.
  • You need to inspect a closure value or step through an async chain.
  • You want to capture a CPU profile or heap snapshot from a running process.

Don't use for: anything console.log resolves in under a minute. Reach for this when the payoff justifies the setup.

Related skills

  • superpowers:systematic-debugging — the general debugging process (hypothesis → minimal repro → bisect). This skill is the Node-side tooling you use inside that process.
  • debug-py — Python equivalent.
  • scripts/browser/browser-harness.sh — rebar's CDP harness for Chrome. The CDP patterns below transfer directly: same chrome-remote-interface client, same Debugger/Runtime domains.

Pick one

ToolWhen
node inspect REPLAlways available, zero install. Best for quick poking.
CDP via chrome-remote-interfaceScriptable. Best for automating many breakpoints, capturing scope across runs, or driving from an agent loop.

Start with node inspect.

node inspect REPL reference

Launch paused on first line:

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

debug> prompt:

CommandAction
c / contcontinue
n / nextstep over
s / stepstep into
o / outstep out
pausepause running code
sb('file.js', 42)breakpoint at file.js:42
sb(42)breakpoint at line 42 of current file
sb('functionName')break on function entry
cb('file.js', 42)clear breakpoint
breakpointslist breakpoints
btbacktrace (call stack)
list(5)source ±5 lines around current position
watch('expr')evaluate expr on every pause
repldrop into REPL in current scope (Ctrl+C to exit)
exec exprevaluate expression once
restart / kill / .exitrestart / kill / quit

Inside repl sub-mode you can read locals/closure variables freely.

Attach to a running process

# Enable inspector on an existing PID
kill -SIGUSR1 <pid>
# Node prints: Debugger listening on ws://127.0.0.1:9229/<uuid>

node inspect -p <pid>
# or
node inspect ws://127.0.0.1:9229/<uuid>

Start with the inspector from launch:

node --inspect script.js               # listen, don't pause
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:

node --inspect-brk --import tsx script.ts

Programmatic CDP

Same client (chrome-remote-interface) that powers rebar's scripts/browser/browser-harness.sh. Reuse the patterns:

npm i -g chrome-remote-interface
node --inspect-brk=9229 target.js &

Driver script (/tmp/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}`);

    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();
  await Debugger.setBreakpointByUrl({
    urlRegex: '.*app\\.tsx$',
    lineNumber: 119, // 0-indexed
    columnNumber: 0,
  });
  await Runtime.runIfWaitingForDebugger();
})();

If chrome-remote-interface isn't in the project, install to a throwaway location:

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

Vitest / Jest under the debugger

node --inspect-brk ./node_modules/vitest/vitest.mjs run --no-file-parallelism src/foo.test.tsx
# in another terminal:
node inspect -p <pid>

Use --no-file-parallelism (vitest) or --runInBand (jest). Debugging a worker pool is painful.

Heap snapshots & CPU profiles

// CPU profile, 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 in Chrome DevTools → Performance
// 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. TS line numbers. Breakpoints hit emitted JS, not .ts. Either break in dist/*.js, or run with --enable-source-maps and use a CDP client that follows sourcemaps. node inspect CLI does not.
  2. --inspect vs --inspect-brk. Plain --inspect doesn't pause; your script can race past the first breakpoint if you attach late.
  3. Port collisions. Default 9229. Use --inspect=0 for a random port and discover via curl -s http://127.0.0.1:9229/json/list.
  4. Child processes. --inspect doesn't propagate. Use NODE_OPTIONS='--inspect-brk' node parent.js (Node auto-increments ports for children).
  5. Background pauses. Ctrl+C from node inspect while paused leaves the target paused. cont first or kill explicitly.
  6. Security. --inspect=0.0.0.0:* exposes RCE. Bind to 127.0.0.1 unless on an isolated network.

Verification checklist

  • curl -s http://127.0.0.1:9229/json/list returns the target you expect.
  • First breakpoint actually hits.
  • Source listing matches the file you intended (mismatch → sourcemap issue).
  • exec process.pid in repl returns the PID you meant to attach to.

One-shot recipes

"Why is this variable undefined at line X?"

node --inspect-brk script.js &
node inspect -p $!
# debug>
sb('script.js', X)
cont
# paused.
repl
> myVariable
> Object.keys(this)

"What's the call path into this function?"

debug> sb('suspectFn')
debug> cont
debug> bt

"Async chain hangs — where?"

# Start with --inspect (no -brk), let it hang, then:
debug> pause
debug> bt

Keep looking

Skills are one crate of 627,801. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.