@push.rocks/smartconsole
Typed console output, structured colors, tables and Markdown across backend and browser runtimes. The backend surface also provides command routing, interactive questions, and live task progress in one coordinated terminal session.
Issue Reporting and Security
For reporting bugs, issues, or security vulnerabilities, please visit community.foss.global/. This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a code.foss.global/ account to submit Pull Requests directly.
Install and choose a surface
pnpm add @push.rocks/smartconsole
| Import | Runtime | Capabilities |
|---|---|---|
@push.rocks/smartconsole |
Node.js, Deno, Bun | Output, ANSI, terminal tables, CLI, prompts, tasks |
@push.rocks/smartconsole/web |
Browser window or worker | Output, CSS console arguments, native inspection/tables, collapsed groups |
@push.rocks/smartconsole/iso |
Backend or browser | Common output, structured colors, inspection, tables, Markdown, groups |
/iso stays restricted even when imported on a backend. Unsupported members and
options are absent from its types. JavaScript callers receive errors for
unsupported options. Wrong-runtime entrypoints throw; unknown hosts are rejected.
Browser bundlers must honor the browser export condition. The browser and
portable browser dependency graphs contain no Node builtins or terminal prompts.
import { SmartConsole, color } from '@push.rocks/smartconsole/iso';
const out = new SmartConsole();
await out.success(color.green('Ready'));
await out.inspect({ connected: true });
await out.markdown('# Status\n\n- [x] Connected\n- [ ] Synchronized');
await out.group('Details', async scope => {
await scope.info('Output inside the group');
});
await out.dispose();
Common output
log, info, success, warn, error, and debug accept strings or
IStyledText, with spaces between arguments. Use inspect(value) for arbitrary
objects. { debug: false } suppresses debug output. Methods return promises;
output is ordered and backend stream completion is awaited. flush() observes
pending output and reports stream failures. dispose() drains output and releases
resources; repeated disposal returns the same promise. Writes after disposal fail.
Groups serialize their callback with surrounding output. Use the supplied scope
inside a group, including for nested groups. Awaiting the parent console from
inside its own group would wait on that group. Scoped output exposes the common
output API and becomes invalid when the callback finishes. Browser groups also
accept { collapsed: true } on the browser console.
During a prompt, ordinary output is accepted into a session buffer and rendered
after the prompt finishes. This allows an asynchronous validator to await logging.
flush() during an active prompt throws; await the prompt before flushing.
Structured colors
import { color } from '@push.rocks/smartconsole/iso';
const label = color.concat(
color.green('Saved'),
' ',
color.text('Work account', { foreground: 'orange', bold: true }),
);
const custom = color.rgb('Custom', { r: 80, g: 160, b: 240 });
const plain = color.plain(label);
Named colors are black, blue, brown, cyan, green, orange, pink, red,
and white. Styles support foreground/background colors, bold, dim, italic,
underline and strikethrough. RGB channels are integers from 0 through 255.
IStyledText is serializable: { type: 'smartconsole.text', segments: [...] }.
It carries text and styles, never ANSI or CSS. Control characters in user text
are rendered visibly; tabs and newlines remain layout characters. A JSON transport
can preserve this value and pass it back to an output method without an encoder
in the transporting application.
The root additionally exposes color.toAnsi(value): string. /web additionally
exposes color.toConsoleArgs(value): [string, ...string[]] for use with
console.log(...args). /iso exposes neither conversion. Browser formatting
uses fixed format placeholders, so percent signs in user text remain literal.
Tables
await out.table([
{ account: 'Work', usage: 25 },
{ account: 'Personal', usage: undefined },
], {
columns: [
{ key: 'account', title: 'Account', value: row => row.account },
{ key: 'usage', title: 'Usage', value: row => row.usage == null ? undefined : `${row.usage}%` },
],
missingText: 'Unavailable',
emptyText: 'No accounts.',
});
Column order is explicit; keys and titles must be unique. Common cell values are
string, number, boolean, null or undefined. Null/undefined use missingText.
Native browser tables preserve numeric and boolean values. Cell styling belongs
only to backend tables because native browser consoles cannot color individual
table cells.
Backend tables add width, border: 'none' | 'ascii' | 'unicode', and
overflow: 'wrap' | 'truncate'. Backend columns add width,
align: 'left' | 'right' | 'center', and style: row => ITextStyle. Multiline
cells, Unicode graphemes and display widths are handled before color encoding.
Impossible explicit widths throw.
Backend columns may supply render: row => TText for mixed styles within a cell;
value remains its scalar representation. A table theme accepts header,
border, and alternateRow text styles. These options are rejected by /web
and /iso, whose tables retain native scalar cells.
Markdown and inspection
Markdown is parsed through @push.rocks/smartmarkdown/iso. CommonMark/GFM
headings, paragraphs, emphasis, links, lists, task lists, quotes, code blocks,
tables, references and footnotes are rendered as console text. Frontmatter is
omitted. headingPrefix: false hides heading markers. Backend Markdown also
accepts width; browser Markdown tables use formatted lines to preserve styles.
HTML and code stay inert, images become alt text plus URL, and destinations are
never fetched. This package does not render a DOM terminal or execute Markdown.
Code fences with a recognized language use Lowlight's common grammars for syntax
highlighting. Unknown or omitted languages retain their code text without syntax
coloring. Set highlight: false to disable highlighting. All three surfaces accept
a Markdown theme with heading, code, marker styles and tokens keyed by
Highlight.js token names such as keyword, string, number, and comment.
Backend inspection accepts { depth, showHidden }; browser inspection passes
the original object to native console inspection. Portable inspection uses each
host's normal object representation without platform-specific options.
Backend terminal options
import { SmartConsole } from '@push.rocks/smartconsole';
const out = new SmartConsole({
colors: 'auto',
interactive: 'auto',
symbols: 'auto',
});
Backend options can inject Node-compatible input, output, and errorOutput
streams. Defaults are stdin, stdout and stderr. Color modes are auto, always
and never; interaction modes use the same names. Automatic rendering produces
plain output and append-only task updates without a capable TTY. Forced color or
interactive rendering on an incapable output throws. Automatic color respects
NO_COLOR; automatic interaction respects CI and TERM=dumb. Task symbols can
be auto, unicode, or ascii.
Consoles sharing an output stream share its session and must agree on explicit terminal settings. The session owns write ordering, stream backpressure, task redraws and prompt arbitration. Disposal releases its resources without closing caller-owned streams or terminating the application.
CLI commands
const out = new SmartConsole();
out.cli.configure({ name: 'accounts', version: '1.0.0', description: 'Manage accounts' });
out.cli.command({
name: 'list', aliases: ['ls'], description: 'Show accounts',
options: { limit: { type: 'number', default: 10, aliases: ['n'] } },
}, async ({ options, args }) => {
await out.log(`Listing up to ${options.limit} accounts`, ...args);
});
out.cli.default({}, async () => { await out.log('Choose a command with --help.'); });
await out.cli.run(); // Runtime user arguments, including Deno compiled binaries.
await out.dispose();
run(argv) accepts user arguments only, without executable or script paths.
Command options follow the command. Leading options belong to the default
command. help [command], --help/-h and --version/-v settle normally.
Unknown commands, unknown options, missing required values, invalid values and
handler failures reject. allowUnknownOptions: true explicitly permits forwarding
options to another tool.
Option types are string, number, boolean, and strings, with aliases,
defaults, required flags and descriptions. The handler receives typed options,
positional args, and parsed argv (including the command in _[0]).
dispatch(command, argv) invokes a registered handler programmatically. Each
command has one awaited handler. onEvent(listener) provides observational
start/finish/error events and returns an unsubscribe function; observers never
control command completion. takeObserverErrors() reports observer failures.
Interactive questions
const account = await out.prompts.ask({
type: 'list', name: 'account', message: 'Choose an account',
choices: [{ name: 'Work', value: 'work' }, { name: 'Personal', value: 'personal' }],
});
const save = await out.prompts.ask({ type: 'confirm', name: 'save', message: 'Save this account?', default: true });
Supported types: input, confirm, list, rawlist, expand, checkbox,
password, and editor. ask() returns the typed answer directly. Choices have
name, value, optional disabled, and an expand-only unique key (h is
reserved). Passwords accept mask; editors accept waitForUserInput. Editor
prompts use the user's editor and a disposable temporary file; Deno requires
read/write/env/run permissions for this operation.
Validators may return a boolean, an error message, or a promise of either.
askAll(questions, options) returns answers keyed by question name. add(questions)
and runQueue(options) support incrementally assembled queues. Names must be
unique within a queue. A failed question rejects the queue.
Prompt options accept signal for cancellation and an explicit nonInteractive
policy. The default is { mode: 'error' }. { mode: 'defaults' } uses only
declared defaults; { mode: 'answers', answers: { save: true } } uses only supplied
answers. Missing or invalid values fail; environment variables never silently
approve a question. Prompts serialize across consoles sharing the terminal, pause
live tasks, and reject with PromptCancelledError on cancellation. Disposal
cancels active and pending prompts and restores input ownership.
Tasks and progress
const parent = out.tasks.create({ job: 'Synchronize' });
const child = parent.create({ job: 'Read accounts', showTimer: true });
await child.run(async task => {
await task.setProgress(1, 2);
await task.log('First account read');
await task.setProgress(2, 2);
}, { successMessage: 'Accounts read' });
await parent.complete('Ready');
await out.dispose();
Tasks expose status, elapsed time, progress, logs and errors. Use log/update,
setProgress, setTimerEnabled, setSpinnerEnabled, complete, fail, or
attachError(error, { keepOpen: true }). run() awaits work, completes on success,
and records and rethrows failures; errorKeepOpen keeps a failed operation visible
as an active task. Complete children before completing their parent. Failing a
parent fails its running children.
Options include rows, logLimit, showTimer, showSpinner, spinnerFrames,
and spinnerIntervalMs. Progress requires 0 <= current <= total and a positive
total. tasks.clear() stops rendering and invalidates existing handles. Finished
or invalidated tasks reject further updates. Timers are stopped when unnecessary
and on disposal; they do not keep a backend process alive by themselves.
Terminal user interfaces
The backend out.tui provides composable screens. /web and /iso expose no TUI
members or types. Screens require interactive TTY input/output and raw input;
starting a screen in a pipe, CI, or an incapable terminal throws.
const table = out.tui.table({
rows: [{ id: 'work', usage: 25 }, { id: 'personal', usage: 80 }],
rowKey: row => row.id,
columns: [
{ key: 'id', title: 'Account', value: row => row.id },
{ key: 'usage', title: 'Usage', value: row => row.usage },
],
onActivate: async (row, screen) => {
if (await screen.confirm(`Use ${row.id}?`)) await activateAccount(row.id);
},
});
await out.tui.run({
view: out.tui.column([
out.tui.text('Accounts', { height: 1 }),
out.tui.panel('Saved accounts', table),
out.tui.text('Tab: focus · Enter: activate · q: quit', { height: 1 }),
]),
keys: { q: screen => screen.close() },
});
await out.dispose();
Create stateful widgets once and reuse them across renders. view can also be a
function returning the current layout. Call screen.invalidate() after external
updates such as table.setRows(rows) or text.setText(value); keyboard actions
and terminal resizing redraw automatically. Changed terminal rows alone are
written. Rows keep selection by their unique rowKey, including after refresh.
Table options accept selectedKey to select an initial row without changing row
order. It must identify an existing initial row; unknown keys throw. Omitting it
selects the first row. Initial selection does not invoke onSelect.
Widget factories:
| Factory | Behavior |
|---|---|
text(value, size?), viewer(value, size?) |
Styled text; viewer adds scrolling |
markdown(source, options?, size?) |
Scrollable themed Markdown |
table(options) |
Typed rows, rich columns, sorting, filtering, selection |
row(children, size?), column(children, size?) |
Horizontal/vertical layout |
panel(title, content, size?) |
Bordered titled panel |
tabs([{title, content}], size?) |
Switchable content; arrows on the tab header |
tree(nodes, onSelect?, size?) |
Expandable nodes with unique IDs and styled labels |
input(options) |
Editable text/password, grapheme-aware cursor, validation |
checkbox(label, checked?, size?) |
Boolean control |
button(label, action, size?) |
Awaited action |
form(fields, submit, label?, size?) |
Validates input fields before submission |
progress(label, current, total, size?) |
Progress bar with setProgress() |
logs |
Bounded scrollable log widget populated by console output |
Sizes accept positive integer width (at least 2), height, and flex. Flexible
children share remaining space. Terminal sizes that cannot fit the view show an
explicit resize message and keep the screen available for resizing or quitting.
TUI tables use single-line cells with truncation and keep the column header
visible while scrolling. Use static out.table() for multiline cells.
Tab/Shift-Tab moves focus. Tables support arrows, Home/End, PageUp/PageDown, Enter,
/ to edit a filter, s to cycle sort columns, and S to reverse sorting. Escape
clears a filter; Enter keeps it. Tables expose selected, setRows, setFilter
and sort(columnKey, descending?). A one-column table also serves as a selection
list. Trees use Left/Right to collapse/expand and Enter to activate. Viewers scroll
with arrows, Home/End and PageUp/PageDown.
run accepts theme (border, heading, selected, muted), signal, keys,
and an awaited onReady(screen) callback. Shortcuts use keys such as q,
ctrl+r, or alt+r; plain character shortcuts do not steal input while a text
field or table filter is being edited. Ctrl-C closes the screen. The context
provides close(), invalidate(), focus(widget), signal, and
confirm(message, {confirmLabel?, cancelLabel?}). Confirmations default to Cancel;
Left/Right or Tab changes the choice, Enter accepts it, and Escape cancels it.
Actions are awaited; further management keystrokes are ignored during an action.
Confirmation dialogs keep accepting input while the action awaits their answer.
Closing aborts the context signal and waits for running callbacks to finish, so
applications can complete necessary cleanup. A callback must not await disposal
of its own console. Errors reject run() after terminal restoration.
The terminal session arbitrates screens and prompts, pauses task redraws, and
buffers ordinary output while a screen is open. Use TUI dialogs inside a screen;
ordinary prompts are rejected. flush() is unavailable until the screen closes.
The latest 1000 buffered log entries are replayed afterward, with an explicit
discard count if exceeded; the logs widget retains 500 entries. Raw mode, input
listeners and the alternate screen are restored on close, abort, input EOF and
callback failure. Caller-owned streams remain open. Applications may pass an
AbortSignal to integrate their own process signal handling.
Migration
This package owns the functionality previously in consolecolor, smartinteract
and smartcli; it has no runtime dependency on those packages or on smartlog.
Use structured colors and output methods, prompts.ask() instead of answer
wrappers, and one awaited CLI handler instead of command subscriptions. Logging
transports remain the responsibility of smartlog.
License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the license file.
Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
Company Information
Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany
For any legal inquiries or further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.