npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@termwright/trace

v0.7.7

Published

trace format: asciicast v3 + events + semantics, readers/writers, HTML report

Downloads

2,032

Readme

@termwright/trace

The .twtrace archive format for termwright: a writer that records a live terminal session, a streaming reader that powers time travel in the runner UI, and a self-contained HTML failure report.

An archive is a directory (zippable for transport):

| File | Content | | ----------------- | ----------------------------------------------------------------------------------- | | meta.json | session id, command, viewport, platform, terminal profile, exit, crash, log summary | | session.cast | asciicast v3; test.step() titles become markers | | events.jsonl | inputs, resizes, steps, driver actions, assertions, crash | | semantics.jsonl | content-sized semantic keyframes and revision deltas | | logs.jsonl | application log entries | | timeline.jsonl | raw-time anchors and hidden-window transforms | | COMMITTED | versioned SHA-256 manifest written last before atomic publication |

The layout is normative in /CONTRACTS.md §Trace. Nothing outside this package reads or writes those files directly.

Install

pnpm add @termwright/trace

Requires Node >= 22. ESM only.

Recording

import { createTraceWriter } from '@termwright/trace';

const writer = createTraceWriter(harness, {
  dir: 'out/login.twtrace',
  command: ['node', 'app.js'],
  columns: 100,
  rows: 30,
  idleTimeLimit: 2,
});

writer.hide(); // keep setup noise out of the recording
await harness.waitForText('ready');
writer.show();

const step = writer.addStep('submit the form'); // → cast marker "submit the form"
await harness.getByRole('button', { name: 'Submit' }).click();
step.end('failed', 'button stayed disabled');

await writer.finalize();

The writer attaches to anything exposing sessionId and events — a TerminalHarness, or a fake in tests. Driver actions, application logs and crashes arrive on their own through those events; nothing above reports them by hand. recordAction exists only for work the driver cannot see, and calling it for a harness action would record that action twice.

Records append immediately to a private sibling staging directory through a bounded queue. Finalization drains and fsyncs it, writes COMMITTED, then atomically renames it into place. A crash or ENOSPC leaves an explicitly incomplete staging artifact; readers require and verify COMMITTED.

The two timelines

The disk format carries one canonical time and the reader exposes a derived one:

  • t — wall-clock milliseconds since recording started.
  • castOffset — reader-derived position on the recording: t after hide() windows were cut out and idle gaps compressed.

Persisting the derived value on every line made the old writer retain the whole run until finalization. Trace v4 stores raw t plus timeline.jsonl; readers derive castOffset lazily and reject the obsolete on-disk field.

Everything a player or UI seeks to is a castOffset.

Reading

import { openTrace } from '@termwright/trace';

const trace = await openTrace('out/login.twtrace'); // directory or zip

const state = await trace.stateAt(1_500);
state.castPrefix; // output to write into an emulator
state.columns; // viewport after resizes up to that point
state.nearestSemanticRevision; // newest tree at or before it
state.step; // the step covering that moment
state.logs; // preceding log entries, bounded

for await (const event of trace.events()) console.log(event.kind, event.castOffset);
for (const step of await trace.steps()) console.log(step.title, step.status);

await trace.close();

Failures come back with a code that says whose mistake it was: not-found when the path holds no archive, protocol-violation when it holds a broken one.

stateAt is the time-travel primitive: scrub to an offset, get everything needed to render that moment. packTrace(dir, file) and unpackTrace(file, dir) zip an archive for CI upload and read it back.

Frames, and why they line up

import { frameAt } from '@termwright/trace';

const frame = await frameAt(trace, 1_500);
frame.cell(3, 10); // a driver-shaped CellSnapshot
frame.text();

frameAt replays the output prefix back into a cell grid shaped like the driver's ScreenSnapshot, so a recorded moment can be inspected cell by cell or handed to @termwright/screenshot.

It measures characters with the profile the session used (meta.terminalProfile, captured from TerminalHarness.terminalProfile) through the shared emulator in @termwright/vt. That matters more than it sounds: when the session and its replay used different width tables, an emoji was two columns live and one on replay, and the screenshot quietly disagreed with the assertion. An archive naming a profile this build does not know is rejected rather than replayed with the wrong tables.

Application logs

A TUI cannot print diagnostics to the screen without corrupting the render, so logs.jsonl carries what the program said about itself: lines from a followed log file, and structured records from an adapter that negotiated the logs capability. Both land in one shape with a message field, so nothing has to branch on provenance before printing an entry.

if (trace.meta.logs !== undefined) {
  for await (const entry of trace.logs()) {
    console.log(entry.level ?? 'log', entry.label, entry.message);
  }
}

// Or just the window leading up to a moment, for a scrubbing UI:
const around = await trace.stateAt(1_500, { logWindow: 50 });

label is the display name of the stream; logger and path are kept separately, because filtering by channel (db.pool) or attributing a line to a file are different questions from "which stream do I render this under" — and a label may be shared between sources.

A followed file line carries no level. The driver does not infer one from the text and neither does this package: colouring a report by substring match is wrong often enough to be worse than no colour.

meta.logs summarises the file — count, per-level counts, sources — and reports how many entries were refused after the append-only maxLogEntries ceiling (10 000 by default).

Every log line and structured attribute passes the same artifact sanitizer before the append spool. Source-side redaction remains useful defense in depth, but is no longer the persistence boundary.

Semantic values and input/action payloads use artifactSecurity.mode: raw is the default, while redacted and none are explicit choices. Enable redacted before producing artifacts that leave the project; sensitive semantic values are then stored as typed withheld observations. Executable keyboard values never enter an ActionReceipt; receipts contain recorded projections only.

When the program dies on its own

A signal, or a non-zero exit nobody asked for, lands in meta.crash and is marked on the timeline in events.jsonl.

if (trace.meta.crash !== undefined) {
  console.error(trace.meta.crash.screenTail.join('\n'));
  const tree = await trace.crashSemantic(); // the tree current at the time
}

Crash tails, diagnostics and input previews pass the same policy as the live streams. raw stores them verbatim by default. redacted matches registered secrets across output chunks and ANSI style controls when explicitly enabled.

The report

import { generateHtmlReport } from '@termwright/trace';

await generateHtmlReport({
  outFile: 'out/report.html',
  results: [{ id: 't1', title: 'login', status: 'failed', tracePath: 'out/login.twtrace' }],
});

One HTML file that makes no network requests at all — the asciinema player is inlined from node_modules at generation time.

For a failing test it derives the screen before the failing step and at failure, renders both to styled HTML with the changed rows highlighted, lists the semantic changes as sentences (button "Submit" state changed to disabled), shows the crash panel and the logs from the failing step, and embeds the recording positioned on that step's marker. Failed driver actions sit on the timeline beside the steps with their error code, so a failure reads as "the click never landed, and here is why" rather than as a screen that did not change.

A test that passed keeps its archive too under trace: 'on': its section is collapsed, but it names the .twtrace path and shows the whole log.

Callers that already have the pieces can supply them instead of a trace — visual and semantic for a snapshot mismatch, crash when recording was off, screenshots for PNGs from @termwright/screenshot. The report embeds images; it never rasterises anything itself, which keeps a native renderer out of every test run.

Development

pnpm build && pnpm typecheck && pnpm test

Implementation decisions and open threads: NOTES.md.