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

@c9up/lumen

v0.1.1

Published

Lumen — terminal output for the Ream ecosystem: colours, logger, tables, boxes and task reports, with a raw mode that makes all of it assertable

Downloads

497

Readme

@c9up/lumen

Terminal output for the Ream ecosystem: colours, a logger with a fixed vocabulary, tables, boxes, steps and task reports — and a raw mode that makes all of it assertable.

No dependencies. Node built-ins only.

import { lumen } from '@c9up/lumen'

const ui = lumen()

ui.logger.success('3 migrations applied', { startTime })
ui.logger.warning('config/mail.ts is missing a sender')

const create = ui.logger.action('creating config/auth.ts')
try {
  await write()
  create.displayDuration().succeeded()
} catch (error) {
  create.failed(error)
}

ui.table().head(['Name', 'Batch']).row(['create_users_table', '1']).render()

Modes

A UI is in one of three modes, decided once:

| mode | colours | output | |---|---|---| | normal | escape codes | the terminal | | silent | none | the terminal | | raw | spelled out — dim(yellow(2 files)) | memory |

lumen() picks between normal and silent by asking the stream, so nothing downstream has to check again. NO_COLOR, FORCE_COLOR, TERM=dumb and CI detection are all handled there.

raw is for tests. An expected line stays a string a human can read:

const ui = lumen({ mode: 'raw' })
ui.logger.warning('careful')
expect(ui.getLogs()).toEqual(['[ yellow(warn) ] careful'])

Levels

The vocabulary is fixed on purpose — the same word, in the same colour, for the same meaning, in every package:

[ success ] green   [ error ] red     [ warn ] yellow
[ info ] blue       [ debug ] cyan    [ wait ] cyan

warning, error and fatal go to stderr, so a command's data output stays pipeable.

Each message takes a prefix (dim, %time% interpolated), a suffix (dim yellow, in parentheses), a silent flag, and a startTime rendered as the elapsed time — a Date.now(), a process.hrtime() tuple or a process.hrtime.bigint(), because an example copied from elsewhere passes the tuple and subtracting it from a number renders (NaNm NaNs).

Colours on their own

@c9up/lumen/colors carries no widget:

import { ansiColors } from '@c9up/lumen/colors'

const colors = ansiColors()
colors.dim.yellow('2 files')     // property chain
colors.dim().yellow('2 files')   // call chain — same thing

Each style closes with its own code rather than a blanket reset, so a nested call restores what the outer one was holding: red('a ' + dim('b') + ' c') stays red after the b.

Driving the output yourself

Every widget writes through a renderer, and every line can be built without writing it — logger.prepareInfo(msg), action.prepareSucceeded(), table.prepare(), box.prepare(), steps.prepare(). A spinner hands its frames over with tap(line => …), and logger.dummy() swallows the output of a stretch that would otherwise land in the middle of someone else's frame.

A task run is data as well as output:

const tasks = ui.tasks()
tasks.add('sync', async (task) => {
  task.update('42 files')
  return 'done'
})
tasks.tasks()[0].onUpdate((task) => report(task.getState(), task.getDuration()))
await tasks.run()
tasks.getState()   // 'succeeded' | 'failed'

addIf(condition, …) and addUnless(…) declare a step behind a flag, and each Task carries getState(), getDuration(), getError(), getSuccessMessage() and getLastLoggedLine().

NAMED DEVIATION — upstream builds a widget bare (new Table()) and wires the colours and the renderer afterwards, which makes forgetting either a silent failure. Here ui.table() hands them over at construction; useColors() / useRenderer() still swap them on a widget you built yourself.

Layout

@c9up/lumen/helpers has the primitives the widgets are built on — stringWidth, justify, wrap, truncate, terminalWidth. stringWidth ignores escape codes, gives a combining accent no width and counts a CJK glyph or an emoji as two columns; measuring with String.length is how a table with one emoji in it becomes ragged for every row below.

License

MIT