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/logs

v0.2.0

Published

application log capture for terminal tests: the termwright:log diagnostics channel, ingress redaction, and pino/winston/consola/OpenTelemetry bridges

Downloads

71

Readme

@termwright/logs

Application log capture for terminal tests.

A TUI cannot print diagnostics to the screen without corrupting its own render, so applications write them to a logger instead — where a test can no longer see them. This package carries those records to termwright over the termwright:log diagnostics channel, so log.error(...) becomes assertable test state rather than an invisible side effect.

Two properties make it safe to leave wired up in production code:

  • Zero cost when nobody is listening. publishLog checks for subscribers before it touches its input, so a thunk is never invoked and an input object is never even read.
  • Secrets are redacted at ingress, before any subscriber sees a record — not at render time, because by then the record has already been handed out.

Install

pnpm add @termwright/logs

The logger bridges are optional peers: nothing is imported unless you import the matching subpath, so a project using pino never pulls in winston.

Usage

import { publishLog, subscribeToLogs, hasLogSubscribers } from '@termwright/logs';

// --- In the application: one line, no termwright dependency required. ---
import { channel } from 'node:diagnostics_channel';
channel('termwright:log').publish({ level: 'error', message: 'payment failed' });

// Or, with this package, for normalisation and redaction at the source:
publishLog({ level: 'warn', message: 'cache miss', attrs: { key: 'user:42' } });

// Expensive context? Guard it — the thunk never runs without a listener.
publishLog(() => ({ level: 'debug', message: expensiveDump() }));
if (hasLogSubscribers()) { /* ... */ }

// --- In the harness: collect records. ---
const records: LogRecord[] = [];
const stop = subscribeToLogs((record) => records.push(record), {
  onInvalid: (detail) => console.warn('dropped a log record:', detail),
});

// Every record is already valid per @termwright/protocol and already redacted.
stop();

Bridges

import pino from 'pino';
import { termwrightDestination } from '@termwright/logs/pino';
const logger = pino({ level: 'trace' }, termwrightDestination());

import winston from 'winston';
import { createWinstonTransport } from '@termwright/logs/winston';
winston.createLogger({ transports: [createWinstonTransport()] });

import { createConsola } from 'consola';
import { termwrightReporter } from '@termwright/logs/consola';
createConsola({ reporters: [termwrightReporter()] });

import { LoggerProvider } from '@opentelemetry/sdk-logs';
import { TermwrightLogRecordProcessor } from '@termwright/logs/otel';
new LoggerProvider({ processors: [new TermwrightLogRecordProcessor()] });

Each bridge implements the library's documented extension point structurally and imports nothing from it, so it cannot drift from the version you installed. The bridge tests run against the real libraries rather than doubles — which is how two integration bugs were caught: winston reads transport.log.length to spot legacy transports (so the method must exist), and consola carries the message in args[0], not in message.

The channel is a public contract

termwright:log is a documented name, not an implementation detail. Anything may publish to it with no dependency on termwright, and the subscriber side normalises, redacts and validates whatever arrives. A publisher may therefore send a pino-shaped object, a winston-shaped one, or the protocol's own LogRecord; all three land as a valid record.

Because the channel is public, redaction runs on both sides: at publish time for records that went through this package, and again on receive for records that did not. Redaction is idempotent.

Redaction

Default coverage: credential-shaped values anywhere in text (bearer tokens, JWTs, GitHub/AWS/Slack/OpenAI keys, private-key blocks, credentials embedded in a URL) and any attribute whose key looks like a credential (password, token, authorization, apiKey, cookie, …, including dotted paths like req.headers.cookie).

Deliberately not covered: generic high-entropy strings. Redacting anything that "looks random" destroys git SHAs, request ids, checksums and trace ids — the things that make a log worth reading — while protecting nothing a format rule misses. Configure keyPattern/valuePatterns if your secrets have a house shape.

subscribeToLogs(handler, {
  redaction: { keyPattern: /ssn|iban/i, replacement: '***' },
});

Record shape

Records are LogRecord from @termwright/protocol: ts (epoch ms), level (tracefatal), message, seq, and optional attrs, logger, revision. Normalisation never fails — it coerces, flattens nested attributes with dot notation, and truncates — because dropping a diagnostic for being slightly the wrong shape is worse than shortening it.

seq is assigned by the publisher and preserved on receive, so a gap means records were dropped upstream rather than that two counters disagree.

Development

pnpm --filter @termwright/logs build
pnpm --filter @termwright/logs typecheck
pnpm --filter @termwright/logs test
pnpm --filter @termwright/logs test:hostile   # same suites, 128 MB heap cap