Installations
npm install @poppinss/cliui
Developer Guide
Typescript
No
Module System
ESM
Min. Node Version
>=18.16.0
Node Version
20.18.3
NPM Version
10.8.2
Score
97.8
Supply Chain
99.6
Quality
88.1
Maintenance
100
Vulnerability
100
License
Releases
Fix types for TaskCallback method
Updated on Mar 12, 2025
Update dependencies
Updated on Dec 24, 2024
Update dependencies
Updated on Mar 28, 2024
Add logger.flushLogs to clear in memory logs
Updated on Feb 16, 2024
Allow creating conditional tasks and a silent spinner
Updated on Dec 27, 2023
Persist minimal renderer output after all tasks have finished
Updated on Dec 26, 2023
Contributors
Unable to fetch Contributors
Languages
TypeScript (99.9%)
JavaScript (0.1%)
validate.email 🚀
Verify real, reachable, and deliverable emails with instant MX records, SMTP checks, and disposable email detection.
Developer
poppinss
Download Statistics
Total Downloads
4,810,170
Last Day
12,215
Last Week
64,571
Last Month
278,406
Last Year
2,389,154
GitHub Statistics
MIT License
125 Stars
133 Commits
5 Forks
6 Watchers
1 Branches
7 Contributors
Updated on Mar 12, 2025
Package Meta Information
Latest Version
6.4.3
Package Id
@poppinss/cliui@6.4.3
Unpacked Size
83.55 kB
Size
18.24 kB
File Count
24
NPM Version
10.8.2
Node Version
20.18.3
Published on
Mar 12, 2025
Total Downloads
Cumulative downloads
Total Downloads
4,810,170
Last Day
0.1%
12,215
Compared to previous day
Last Week
1.4%
64,571
Compared to previous week
Last Month
0.7%
278,406
Compared to previous month
Last Year
110.5%
2,389,154
Compared to previous year
Daily Downloads
Weekly Downloads
Monthly Downloads
Yearly Downloads
Dependencies
10
@poppinss/cliui
Opinionated UI KIT for Command Line apps
Why this package exists?
CLI UI is an opinionated UI Kit to log messages, render tables, display spinners, and much more. Following are some of the reasons for creating this package.
- First-class support for testing the UI kit output.
- Using a standard set of design elements without allowing them to be configurable. Choosing consistency over configurability.
Basic usage
Install the package from the npm registry as follows:
1npm i @poppinss/cliui 2 3# yarn lovers 4yarn add @poppinss/cliui
Import the cliui
and create an instance of it.
1import { cliui } from '@poppinss/cliui' 2const ui = cliui() 3 4ui.logger.info('This is an info message') 5// [ info ] This is an info message
Now, let's say you are testing a command and want to assert that an info message is logged during the command's execution.
Usually, you will have to hack into the process.stdout
stream and collect messages within memory (for assertion) and strip any colors (aka ansi sequences) from the output.
However, with this package, you can turn on the raw
mode to collect the logger messages within memory and turn off all color transformations.
1// RAW MODE ON 2const ui = cliui({ raw: true }) 3 4ui.logger.info('This is an info message') 5 6const logs = ui.logger.getRenderer().getLogs() 7assert.deepEqual(logs, [ 8 { 9 stream: 'stdout', 10 message: '[ cyan(info) ] This is an info message' 11 } 12])
Similarly, you can assert that an error message is logged to stderr
.
1const ui = cliui({ raw: true }) 2 3ui.logger.error('Something went wrong') 4 5const logs = ui.logger.getRenderer().getLogs() 6assert.deepEqual(logs, [ 7 { 8 stream: 'stderr', 9 message: '[ red(error) ] Something went wrong' 10 } 11])
Logger
The logger displays all the log messages with consistent styling. Following are the available logging methods.
1import { cliui } from '@poppinss/cliui' 2const ui = cliui() 3 4// Writes to stdout 5logger.debug('Something just happened') 6logger.info('This is an info message') 7logger.success('Account created') 8logger.warning('Running out of disk space') 9 10// Writes to stderr 11logger.error(new Error('Unable to write. Disk full')) 12logger.fatal(new Error('Unable to write. Disk full'))
You can also define the prefix
and suffix
for the log message. The prefix and suffix are displayed with lower opacity (the dim color transformation is used).
1logger.info('Install packages', { suffix: 'npm i --production' })
You can display current time as a prefix using the %time%
keyword.
1logger.info('Message with time prefix', { prefix: '%time%' })
Loading animation
You can display a log message with a loading animation using the logger.await
method. The method accepts the initial message to display alongside an optional prefix
or suffix
.
1const loader = logger.await('installing packages', { suffix: 'npm i' }) 2 3// Start animation 4loader.start() 5 6// Update the message 7loader.update('unpacking packages', { suffix: undefined }) 8 9// Stop loader 10loader.stop()
Spinner in silent mode
If you writing conditionals around the spinning animation to start it in certain conditions, then you might want to create the spinner in silent mode. In silent mode, the spinner will not output any logs.
1const loader = logger.await('installing packages', { 2 suffix: 'npm i', 3 silent: true // 👈 4}) 5 6// Prints nothing 7loader.start() 8 9// Prints nothing 10loader.update('unpacking packages', { suffix: undefined }) 11 12// Prints nothing 13loader.stop()
Preparing messages without writing them
You can also use the logger to just prepare the message (with colors and formatting) without writing it to the output stream. Just prefix the log message with prepare
and it will return a string value.
1const debugMessage = logger.prepareDebug('Something just happened')
2const infoMessage = logger.prepareInfo('This is an info message')
3const successMessage = logger.prepareSuccess('Account created')
4const warningMessage = logger.prepareWarning('Running out of disk space')
Testing logger output
First, you must instantiate the cliui
in raw mode to collect all logs messages within memory. And then you can access the logs using logger.getRenderer()
to write assertions.
1const ui = cliui({ raw: true }) 2 3ui.logger.info('Hello world') 4 5const logs = ui.logger.getRenderer().getLogs() 6console.log(logs)
You can also flush all logs by calling flushLogs
.
1ui.logger.info('Hello world') 2 3const logs = ui.logger.getRenderer().getLogs() 4console.log(logs.length); // 1 5ui.logger.getRenderer().flushLogs(); 6console.log(logs.length); // 0
Logger actions
Logger actions are pre-styled logs to display the outcome of an action. For example, the action can be to create/update or delete a file.
You can create an action by calling the logger.action
method and pass the message to display. Once, done perfoming the underlying operation, you can either mark the action as succeeded
, skipped
, or failed
.
1logger 2 .action('Creating config/auth.ts') 3 .displayDuration() 4 .succeeded() 5 6logger 7 .action('Updating .tsconfig.json') 8 .succeeded() 9 10logger 11 .action('Creating app/Models/User.ts') 12 .skipped('File already exists') 13 14logger 15 .action('Creating server.ts') 16 .failed(new Error('File already exists'))
Table
You can create a table using the ui.table
method. Under the hood, we are using cli-table3 but only expose some of its configuration options for consistency.
1const ui = cliui() 2const table = ui.table() 3 4table 5 .head([ 6 'Migration', 7 'Duration', 8 'Status', 9 ]) 10 .row([ 11 '1590591892626_tenants.ts', 12 '2ms', 13 'DONE' 14 ]) 15 .row([ 16 '1590595949171_entities.ts', 17 '2ms', 18 'DONE' 19 ]) 20 .render()
You can apply color transforms to any value when rendering the table. For example:
1table.row([ 2 '1590595949171_entities.ts', 3 '2', 4 ui.colors.green('DONE') 5])
Right-align columns
You can right-align the columns by defining them as objects and using the hAlign
property. Also, make sure to align the header column right as well.
1table 2 .head([ 3 'Migration', 4 'Batch' 5 { 6 content: 'Status', 7 hAlign: 'right' 8 }, 9 ]) 10 11table.row([ 12 '1590595949171_entities.ts', 13 '2', 14 { 15 content: ui.colors.green('DONE'), 16 hAlign: 'right' 17 } 18])
Render full width
You can render tables in full width (taking all the space of the terminal) by calling the table.fullWidth
method. In full-width mode:
- We will render all columns as per the size of the content.
- Except for the first column, which takes all the available space.
1table.fullWidth()
You can also change the column index for the fluid column (the one that takes all the space) by calling the table.fluidColumnIndex
method.
1table 2 .fullWidth() 3 .fluidColumnIndex(1)
Testing table output
First, you must instantiate the cliui
in raw mode to collect all logs messages within memory. And then you can access the table output using logger.getRenderer()
to write assertions.
1const ui = cliui({ raw: true }) 2const table = ui.table() 3 4table 5 .head(['Migration','Duration', 'Status']) 6 .row([ '1590591892626_tenants.ts', '2ms', 'DONE']) 7 .render() 8 9const logs = table.getRenderer().getLogs() 10console.log(logs)
Instructions
The instructions widget allows you to render a box with steps. Each step gets prefixed with an arrow >
.
1const ui = cliui() 2const instructions = ui.instructions() 3 4instructions 5 .add(`cd ${colors.cyan('hello-world')}`) 6 .add(`Run ${colors.cyan('node ace serve --watch')} to start the server`) 7 .render()
Sticker
The sticker widget is the same as the instructions
widget. But it does not prefix each line with an arrow >
.
1const ui = cliui() 2const sticker = ui.sticker() 3 4sticker 5 .add('Started HTTP server') 6 .add('') 7 .add(`Local address: ${colors.cyan('http://localhost:3333')}`) 8 .add(`Network address: ${colors.cyan('http://192.168.1.2:3333')}`) 9 .render()
Testing instructions and sticker output
First, you must instantiate the cliui
in raw mode to collect all logs messages within memory. And then you can access the instructions/sticker output using logger.getRenderer()
to write assertions.
1const ui = cliui({ raw: true }) 2const instructions = ui.instructions() 3 4instructions 5 .add(`cd ${colors.cyan('hello-world')}`) 6 .add(`Run ${colors.cyan('node ace serve --watch')} to start the server`) 7 .render() 8 9const logs = instructions.getRenderer().getLogs() 10console.log(logs)
Tasks
The tasks widget allows rendering a list of tasks to perform. Each task has an associated async callback to perform the task, report its progress, and also mark it as succeeded or failed.
- The return value of the callback function is used as the success message.
- You can throw an Error to mark the task as failed. Or, call the
task.error
method to prepare an error from a string value.
1const ui = cliui() 2const tasks = ui.tasks() 3 4await tasks 5 .add('clone repo', async (task) => { 6 return 'Completed' 7 }) 8 .add('update package file', async (task) => { 9 return task.error('Unable to update package file') 10 }) 11 .add('install dependencies', async (task) => { 12 return 'Installed' 13 }) 14 .run()
Reporting task progress
Instead of writing the task progress messages to the console directly, you recommend you call the task.update
method.
The method ensures to display of the latest log message only when using the minimal
renderer and logs all messages when using the verbose
renderer.
1const sleep = () => new Promise<void>((resolve) => setTimeout(resolve, 50)) 2 3tasks 4 .add('clone repo', async (task) => { 5 for (let i = 0; i <= 100; i = i + 2) { 6 await sleep() 7 task.update(`Downloaded ${i}%`) 8 } 9 10 return 'Completed' 11 })
Using verbose renderer
The verbose
renderer displays all the log messages instead of just the latest one. Also, the task output is rendered differently from the minimal renderer. Please, check the example file for the same.
You can create the tasks instance with a verbose
renderer as follows. The rest of the API is the same.
1const tasks = ui.tasks({ verbose: true })
Testing tasks output
First, you must instantiate the cliui
in raw mode to collect all logs messages within memory. And then you can access the tasks output using logger.getRenderer()
to write assertions.
1const ui = cliui({ raw: true }) 2const tasks = ui.tasks() 3 4await tasks 5 .add('clone repo', async (task) => { 6 return 'Completed' 7 }) 8 .add('update package file', async (task) => { 9 return task.error('Unable to update package file') 10 }) 11 .add('install dependencies', async (task) => { 12 return 'Installed' 13 }) 14 .run() 15 16const logs = tasks.getRenderer().getLogs() 17console.log(logs)

No vulnerabilities found.
Reason
no dangerous workflow patterns detected
Reason
no binaries found in the repo
Reason
0 existing vulnerabilities detected
Reason
license file detected
Details
- Info: project has a license file: LICENSE.md:0
- Info: FSF or OSI recognized license: MIT License: LICENSE.md:0
Reason
security policy file detected
Details
- Info: security policy file detected: github.com/poppinss/.github/docs/SECURITY.md:1
- Info: Found linked content: github.com/poppinss/.github/docs/SECURITY.md:1
- Info: Found disclosure, vulnerability, and/or timelines in security policy: github.com/poppinss/.github/docs/SECURITY.md:1
- Info: Found text in security policy: github.com/poppinss/.github/docs/SECURITY.md:1
Reason
7 commit(s) and 2 issue activity found in the last 90 days -- score normalized to 7
Reason
Found 3/30 approved changesets -- score normalized to 1
Reason
detected GitHub workflow tokens with excessive permissions
Details
- Warn: no topLevel permission defined: .github/workflows/checks.yml:1
- Warn: topLevel 'contents' permission set to 'write': .github/workflows/release.yml:4
- Warn: no topLevel permission defined: .github/workflows/stale.yml:1
- Info: no jobLevel write permissions found
Reason
no effort to earn an OpenSSF best practices badge detected
Reason
project is not fuzzed
Details
- Warn: no fuzzer integrations found
Reason
dependency not pinned by hash detected -- score normalized to 0
Details
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/labels.yml:10: update your workflow using https://app.stepsecurity.io/secureworkflow/poppinss/cliui/labels.yml/6.x?enable=pin
- Warn: third-party GitHubAction not pinned by hash: .github/workflows/labels.yml:11: update your workflow using https://app.stepsecurity.io/secureworkflow/poppinss/cliui/labels.yml/6.x?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/release.yml:13: update your workflow using https://app.stepsecurity.io/secureworkflow/poppinss/cliui/release.yml/6.x?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/release.yml:16: update your workflow using https://app.stepsecurity.io/secureworkflow/poppinss/cliui/release.yml/6.x?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/stale.yml:10: update your workflow using https://app.stepsecurity.io/secureworkflow/poppinss/cliui/stale.yml/6.x?enable=pin
- Warn: npmCommand not pinned by hash: .github/workflows/release.yml:28
- Info: 0 out of 4 GitHub-owned GitHubAction dependencies pinned
- Info: 0 out of 1 third-party GitHubAction dependencies pinned
- Info: 0 out of 1 npmCommand dependencies pinned
Reason
SAST tool is not run on all commits -- score normalized to 0
Details
- Warn: 0 commits out of 3 are checked with a SAST tool
Reason
branch protection not enabled on development/release branches
Details
- Warn: branch protection not enabled for branch '6.x'
Score
4.8
/10
Last Scanned on 2025-03-10
The Open Source Security Foundation is a cross-industry collaboration to improve the security of open source software (OSS). The Scorecard provides security health metrics for open source projects.
Learn More