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

@j-o-r/cli

v3.1.0

Published

This project provides a command line interface (CLI) for creating interactive dialogs

Readme

Interactive CLI Framework

A CLI framework for building interactive command-line dialogs, built around one principle:

The terminal is a single-cursor resource with exactly one owner at a time. When the user owns it (free text, confirm, choose), nothing else may write — the cursor is occupied by the pending input. When the app owns it, it writes freely. The library — not the app — manages the turn alternation.

Features

  • Turn-based terminal ownership — the loop alternates user's turn / app's turn automatically; apps never re-arm input manually
  • Buffered output — writes during the user's turn are buffered and flushed when the app's turn begins; nothing prints into the input line, nothing is silently lost
  • One ask() primitive with confirm() / choose() wrappers — nestable
  • Role-based output styling (log, system, error, assistant, tool, custom)
  • Custom key mappings — a mapped key runs its handler as an app turn (live output; questions work inside handlers)
  • Programmatic inputinject() submits a line as if typed (initial context, agent handoff)
  • Built-in >edit< / >paste< / >#!bash< commands (opt-in subpath)
  • Spinner, clear, exclusive-TTY execution for editors and pagers
  • Zero runtime dependencies in the core; no side effects at import

Installation

npm install @j-o-r/cli

The core is dependency-free. The optional built-in commands need @j-o-r/sh and @j-o-r/cache (optional peer dependencies):

npm install @j-o-r/sh @j-o-r/cache   # only for the builtins subpath

Quick Start

import cli from '@j-o-r/cli';

cli.onInput(async (text) => {          // required: the app's turn
  cli.role('assistant');
  cli.write(`echo: ${text}`);
});

cli.start({ prompt: '> ' });           // starts the turn loop

The loop is: user's turn (ask) → app's turn (onInput) → repeat. Input is re-armed automatically — there is no focus() call and no mode juggling.

The buffered-output rule

Output follows terminal ownership:

  • App's turnwrite / log / error print live, with the current role's prefix.
  • User's turn — the same calls are buffered (e.g. a setTimeout firing while a question is pending) and flushed in order when the app's turn begins. Nothing prints into the input line; nothing is dropped.

The spinner is a no-op during the user's turn.

Questions

ask is the single input primitive; confirm and choose build on it. All three are nestable inside onInput (and inside key handlers). All resolve undefined when interrupted (key handler / stop()).

cli.onInput(async (text) => {
  const name = await cli.ask('What is your name? ');        // string
  const ok   = await cli.confirm(`${name}, are you sure? `); // boolean (y/yes/n/no)
  const pick = await cli.choose('Why? ', ['a', 'b', 'c']);   // selected item

  cli.log({ name, ok, pick });
});

ask(prompt, validate) accepts an optional validator: { ok: true, value } accepts, { ok: false } erases the line and re-asks.

API Reference

| Method | Description | |--------|-------------| | onInput(fn) | Register the input handler — the app's turn (required before start) | | onExit(fn) | Register the exit handler (default: process.exit) | | onKeys(keys) | Register key mappings ({ name, ctrl, meta, shift, handler }); active during the user's turn — a mapped key runs its handler as an app turn (live output, working questions). Ctrl+u / Ctrl+e are reserved | | setRole(name, prefix, color) | Register or overwrite an output role | | handleExit() | Opt-in signal handling: SIGINT/SIGTERM/SIGQUIT/SIGHUP run onExit; uncaught exceptions/rejections print loudly and exit(1) | | start({ prompt }) | Start the turn loop (prompt defaults to '> ') | | stop() | End the loop and release the terminal; does not process.exit | | role(name) | Set the current output role (styling state only — nothing prints until the next write) | | write(text, newline = true) | Write at the current role (buffered during the user's turn) | | log(any) | One-line log-role shortcut | | error(err) | One-line error-role shortcut; Error objects print their stack | | clear() | Clear the screen | | startSpinner() / stopSpinner() | Spinner control (stopped automatically when the user's turn begins; startSpinner() is a no-op during the user's turn) | | ask(prompt, validate?) | Free-text question → string (undefined when interrupted) | | confirm(prompt) | Yes/no question → boolean | | choose(prompt, items) | Numbered-list selection → chosen item (undefined for an empty list) | | execute(fn) | Run fn with exclusive TTY ownership (editors, pagers); restores CLI state afterwards | | addCommand(name, fn) | Register a >name args< command (single-line input only); a returned non-empty string replaces the input line and becomes the app's input | | inject(text) | Inject a programmatic input line — echoed as if typed, processed as an app turn; only queues during an app/key-handler turn |

createCli() is exported as a named export for additional isolated instances; the default export is one shared instance (no side effects at import).

Keys

cli.onKeys([
  { name: 'd', ctrl: true, handler: async () => { cli.stop(); process.exit(0); } },
  { name: 'r', ctrl: true, handler: async () => cli.clear() }
]);

A mapped key ends the user's turn and runs the handler as an app turn: output prints live, ask/confirm/choose work inside handlers, and the loop waits for the handler before re-arming input. A handler that needs the exclusive terminal (editor, pager) wraps its work in cli.execute(...).

Custom roles

cli.setRole('debug', '[DBG] ', 'cyan');   // color: ANSI name from the built-in table
cli.role('debug');
cli.write('Debug message');               // prefixed [DBG] in cyan

The built-in user role is special: its color styles every typed input line (prompt + typed echo, and the "as if typed" echoes of inject() and command results) — input never inherits a previously active role's color. The default is reset; override with e.g. cli.setRole('user', '> ', 'brightWhite'). The role's prefix is used only for role('user') output writes; the input prompt text comes from start({ prompt }) / ask(prompt).

Built-in commands

Registration is opt-in — import the subpath once:

import '@j-o-r/cli/builtins';

Without it, >...< in user input is plain text passed to onInput.

  • >edit< — the editor as a multiline input field; the edited text becomes the input. Works inline: This is my prompt but i want to >edit<
  • >paste< — clipboard content (xclip) into the input, reviewed in the editor
  • >#!bash cmd args< — run a shell command, embed its output in a fenced block inside the input, reviewed in the editor

Every builtin ends in an editor review (exclusive TTY via execute); the reviewed text replaces the original input line (on a TTY the echoed line is erased first — best effort: a wrapped line erases only its last row) and is handed to onInput as if typed. A >cmd< marker is recognized on a single input line only — multiline input is plain text. Editor resolution is lazy (first use, no shell calls at import): ~/.selected_editor$EDITORwhich vimwhich nano; when no editor is found the command shows a user-facing error and re-asks.

Shell safety: >#!bash< executes raw shell. Arguments come straight from user input — never feed it untrusted input.

Exit handling

Nothing is registered globally at import or first use. Call handleExit() to opt in: signals run your onExit handler (default process.exit), and crashes print the error before exiting (a 5 s force-exit backstops hanging cleanup). stop() only ends the loop — the app owns the process lifecycle.

Migrating from v2 to v3

v3 replaces mode-based focus juggling with the turn model. Mechanical mapping:

| v2 | v3 | |----|----| | cli.inputHandler = async (input) => ... | cli.onInput(async (text) => ...) | | cli.exitHandler = async (code) => ... | cli.onExit(async (code) => ...) | | cli.focus('assistant') | cli.role('assistant') (styling only — no printing, no turn switch) | | cli.focus() to re-arm input | delete — the loop re-arms automatically | | cli.question(prompt) | await cli.ask(prompt) | | cli.yesNo(prompt) | await cli.confirm(prompt) | | cli.select(prompt, choices) | await cli.choose(prompt, items) | | cli.registerKeyMappings(keys) | cli.onKeys(keys) (same mapping shape) | | programmatic input (manual inputHandler(s) calls) | cli.inject(text) — echoed as if typed, processed as an app turn | | cli.start(mode, input) | cli.start({ prompt }) | | auto-registered process hooks | cli.handleExit() — opt-in | | MODES / restoreMode / the input flag | gone — output roles + turn state replace them |

Behavioral changes to watch:

  • Output during the user's turn is buffered, not dropped (v2 silently dropped log output while asking). Expect deferred log/error output to appear when the app's turn begins.
  • stop() no longer exits the process; pair it with your own exit logic (e.g. in a Ctrl+d key handler or onExit).
  • Unhandled rejections are loud when handleExit() is used (v2 swallowed them silently).
  • Built-ins moved to the @j-o-r/cli/builtins subpath; without that import, >edit<-style input is plain text.
  • @j-o-r/toolset is no longer used; @j-o-r/sh and @j-o-r/cache are optional peer dependencies (only the builtins need them).

License

Apache-2.0 — see LICENSE.