Research
Security News
Threat Actor Exposes Playbook for Exploiting npm to Build Blockchain-Powered Botnets
A threat actor's playbook for exploiting the npm ecosystem was exposed on the dark web, detailing how to build a blockchain-powered botnet.
Terminal task list reborn! Create beautiful CLI interfaces via easy and logical to implement task lists that feel alive and interactive.
The listr2 npm package is a powerful utility for creating beautiful and interactive command-line interfaces (CLI) with tasks lists. It allows developers to manage and execute multiple tasks in a structured and visually appealing way. It supports concurrent tasks, nested tasks, and customizable rendering, making it suitable for complex CLI applications.
Basic Task Execution
This feature allows for the execution of a series of tasks, each represented by a title and a task function. The tasks are executed sequentially, and the progress is visually represented in the CLI.
const { Listr } = require('listr2');
const tasks = new Listr([
{
title: 'Task 1',
task: () => Promise.resolve('First task result')
},
{
title: 'Task 2',
task: () => Promise.resolve('Second task result')
}
]);
tasks.run().catch(err => console.error(err));
Concurrent Tasks
This feature demonstrates how to run tasks concurrently. By setting the 'concurrent' option to true, tasks within the same group are executed in parallel, improving performance for independent tasks.
const { Listr } = require('listr2');
const tasks = new Listr([
{
title: 'Concurrent Tasks',
task: () => {
return new Listr([
{
title: 'Task 1',
task: () => Promise.resolve('First concurrent task result')
},
{
title: 'Task 2',
task: () => Promise.resolve('Second concurrent task result')
}
], { concurrent: true });
}
}
]);
tasks.run().catch(err => console.error(err));
Nested Tasks
This feature showcases the ability to nest tasks within other tasks. It is useful for structuring complex workflows where tasks have sub-tasks, allowing for better organization and readability.
const { Listr } = require('listr2');
const tasks = new Listr([
{
title: 'Parent Task',
task: () => {
return new Listr([
{
title: 'Nested Task 1',
task: () => Promise.resolve('First nested task result')
},
{
title: 'Nested Task 2',
task: () => Promise.resolve('Second nested task result')
}
]);
}
}
]);
tasks.run().catch(err => console.error(err));
Ora is a terminal spinner library that provides a similar visual feedback mechanism as listr2 but focuses solely on indicating the progress of ongoing tasks without the structured task list approach. It's simpler for use cases that require only a spinner.
Inquirer.js is a comprehensive library for creating interactive CLI prompts. While it doesn't manage tasks like listr2, it complements it by handling user inputs in a structured way, making it suitable for CLI applications that require both user interaction and task management.
Chalk is a styling library that allows developers to customize the terminal text appearance. Although it doesn't offer task management functionalities like listr2, it's often used alongside listr2 to enhance the visual output of tasks by adding colors and styles.
Create beautiful CLI interfaces via easy and logical to implement task lists that feel alive and interactive.
This is the expanded and re-written in Typescript version of the beautiful plugin by Sam Verschueren called Listr.
It breaks backward compatibility with Listr after v1.3.12, albeit refactoring requires only moving renderer options to their own key, concerning the conversation on the original repository. You can find the README of compatible version here. Keep in mind that it will not get further bug fixes.
Check out examples/
folder in the root of the repository for the code in demo or follow through with this README.
# Install the latest supported version
npm install listr2
yarn add listr2
# Install listr compatabile version
npm install listr2@1.3.12
yarn add listr2@1.3.12
Create a new task list. It will returns a Listr class.
import { Listr } from 'listr2'
interface Ctx {
/* some variables for internal use */
}
const tasks = new Listr<Ctx>([
/* tasks */
], { /* options */ })
Then you can run this task lists as a async function and it will return the context that is used.
try {
await tasks.run()
} catch (e) {
// it will collect all the errors encountered if { exitOnError: false } is set as an option
// elsewise it will throw the first error encountered as expected
console.error(e)
}
export interface ListrTask<Ctx, Renderer extends ListrRendererFactory> {
// A title can be given or omitted. For default renderer if the title is omitted,
title?: string
// A task can be a sync or async function that returns a string, readable stream or an observable or plain old void
// if it does actually return string, readable stream or an observable, task output will be refreshed with each data push
task: (ctx: Ctx, task: ListrTaskWrapper<Ctx, Renderer>) => void | ListrTaskResult<Ctx>
// to skip the task programmatically, skip can be a sync or async function that returns a boolean or string
// if string is returned it will be showed as the skip message, else the task title will be used
skip?: boolean | string | ((ctx: Ctx) => boolean | string | Promise<boolean> | Promise<string>)
// to enable the task programmatically, this will show no messages comparing to skip and it will hide the tasks enabled depending on the context
// enabled can be external boolean, a sync or async function that returns a boolean
// pay in mind that context enabled functionallity might depend on other functions to finish first, therefore the list shall be treated as a async function
enabled?: boolean | ((ctx: Ctx) => boolean | Promise<boolean>)
// this will change depending on the available options on the renderer
// these renderer options are per task basis and does not affect global options
options?: ListrGetRendererTaskOptions<Renderer>
}
export interface ListrOptions<Ctx = ListrContext> {
// how many tasks can be run at the same time.
// false or 1 for synhronous task list, true or Infinity for compelete parallel operation, a number for limitting tasks that can run at the same time
// defaults to false
concurrent?: boolean | number
// it will silently fail or throw out an error
// defaults to false
exitOnError?: boolean
// inject a context from another operation
// defaults to any
ctx?: Ctx
// to have graceful exit on signal terminate and to inform the renderer all the tasks awaiting or processing are failed
// defaults to true
registerSignalListeners?: boolean
// select the renderer or inject a class yourself
// defaults to 'default' which is a updating renderer
renderer?: 'default' | 'verbose' | 'silent' | ListrRendererFactory
// renderer options depends on the selected renderer
rendererOptions?: ListrGetRendererOptions<T>
// renderer will fallback to the nonTTYRenderer on non-tty environments as the name suggest
// defaults to verbose
nonTTYRenderer?: 'default' | 'verbose' | 'silent' | ListrRendererFactory
// options for the non-tty renderer
nonTTYrendererOptions?: ListrGetRendererOptions<T>
}
Context is the variables that are shared across the task list. Even though external variables can be used to do the same operation, context gives a self-contained way to process internal tasks.
A successful task will return the context back for further operation.
You can also manually inject a context variable preset depending on the prior operations through the task options.
If all tasks are in one big Listr list you do not have to inject context manually to the child tasks since it is automatically injected as in the original.
If an outside variable wants to be injected inside the Listr itself it can be done in two ways.
const ctx: Ctx = {}
const tasks = new Listr<Ctx>([
/* tasks */
], { ctx })
try {
await tasks.run({ctx})
} catch (e) {
console.error(e)
}
Any task can return a new Listr. But rather than calling it as new Listr
to get the full autocompeletion features depending on the parent task's selected renderer, it is a better idea to call it through the Task
itself by task.newListr()
.
Please refer to examples section for more detailed and further examples.
new Listr<Ctx>([
{
title: 'This task will execute.',
task: (ctx, task): Listr => task.newListr([
{
title: 'This is a subtask.',
task: async (): Promise<void> => {
await delay(3000)
}
}
])
}
], { concurrent: false })
You can change indivudual settings of the renderer on per-subtask basis.
This includes renderer options as well as Listr options like exitOnError
, concurrent
to be set on a per subtask basis independent of the parent task, while it will always use the most adjacent setting.
new Listr<Ctx>([
{
title: 'This task will execute.',
task: (ctx, task): Listr => task.newListr([
{
title: 'This is a subtask.',
task: async (): Promise<void> => {
await delay(3000)
}
},
{
title: 'This is an another subtask.',
task: async (): Promise<void> => {
await delay(2000)
}
}
], { concurrent: true, rendererOptions: { collapse: true } })
},
{
title: 'This task will execute.',
task: (ctx, task): Listr => task.newListr([
{
title: 'This is a subtask.',
task: async (): Promise<void> => {
await delay(3000)
}
},
{
title: 'This is an another subtask.',
task: async (): Promise<void> => {
await delay(2000)
}
}
], { concurrent: true, rendererOptions: { collapse: false } })
}
], { concurrent: false })
Please refer to Throw Errors Section for more detailed and further examples on how to handle silently failing errors.
The input module uses the beautiful enquirer.
So with running a task.prompt
function, you can get access to any enquirer default prompts as well as using a custom enquirer prompt.
To get an input you can assign the task a new prompt in an async function and write the response to the context.
It is not advisable to run prompts in a concurrent task because multiple prompts will clash and overwrite each other's console output and when you do keyboard movements it will apply to them both.
Prompts, since their rendering is getting passed as a data output will render multiple times in verbose renderer since verbose renderer is not terminal-updating intended to be used in nonTTY environments. It will work anyhow albeit it might not look great.
Prompts can either have a title or not but they will always be rendered at the end of the current console while using the default renderer.
Please refer to examples section for more detailed and further examples.
To access the prompts just utilize the task.prompt
jumper function. The first argument takes in one of the default enquirer prompts as a string or you can also pass in a custom enquirer prompt class as well, while the second argument is the options for the given prompt.
Prompts always rendered at the bottom of the tasks when using the default renderer with one line return in between it and the tasks.
Please note that I rewrote the types for enquirer, since some of them was failing for me. So it may have a chance of having some mistakes in it since I usually do not use all of them.
new Listr<Ctx>([
{
task: async (ctx, task): Promise<boolean> => ctx.input = await task.prompt<boolean>('Toggle', { message: 'Do you love me?' })
},
{
title: 'This task will get your input.',
task: async (ctx, task): Promise<void> => {
ctx.input = await task.prompt<boolean>('Toggle', { message: 'Do you love me?' })
// do something
if (ctx.input === false) {
throw new Error(':/')
}
}
}
], { concurrent: false })
You can either use a custom prompt out of the npm registry or custom-created one as long as it works with enquirer, it will work expectedly. Instead of passing in the prompt name use the not-generated class.
new Listr<Ctx>([
{
title: 'Custom prompt',
task: async (ctx, task): Promise<void> => {
ctx.testInput = await task.prompt(EditorPrompt, {
message: 'Write something in this enquirer custom prompt.',
initial: 'Start writing!',
validate: (response): boolean | string => {
// i do declare you valid!
return true
}
})
}
}
], { concurrent: false })
I am planning to move enquirer to peer dependencies as an optional install, so this will likely go away in the near future.
If you want to directly run it, and do not want to create a jumper function you can do as follows.
import { createPrompt } from 'listr2'
await createPrompt('Input', { message: 'Hey what is that?' }, { cancelCallback: () => { throw new Error('You made me mad now. Just should have answered me!') }})
Tasks can be enabled depending on the variables programmatically. This enables to skip them depending on the context. Not enabled tasks will never show up in the default renderer, but when or if they get enabled they will magically appear.
Please pay attention to asynchronous operation while designing a context enabled task list since it does not await for any variable in the context.
Please refer to examples section for more detailed and further examples.
new Listr<Ctx>([
{
title: 'This task will execute.',
task: (ctx): void => {
ctx.skip = true
}
},
{
title: 'This task will never execute.',
enabled: (ctx): boolean => !ctx.skip,
task: (): void => {}
}
], { concurrent: false })
Skip is more or less the same with enable when used at Task
level. But the main difference is it will always render the given task. If it is skipped it renders it as skipped.
There are to main ways to skip a task. One is utilizing the Task
so that instead of enabled it will show a visual output while the other one is inside the task.
Please pay attention to asynchronous operation while designing a context skipped task list since it does not await for any variable in the context.
Please refer to examples section for more detailed and further examples.
Inside the task itself after some logic is done.
new Listr<Ctx>([
{
title: 'This task will execute.',
task: (ctx, task): void => {
task.skip('I am skipping this tasks for reasons.')
}
}
], { concurrent: false })
Through the task wrapper.
new Listr<Ctx>([
{
title: 'This task will execute.',
task: (ctx): void => {
ctx.skip = true
}
},
{
title: 'This task will never execute.',
skip: (ctx): boolean => ctx.skip,
task: (): void => {}
}
], { concurrent: false })
There are two rendering methods for the default renderer for skipping the task. The default behavior is to replace the task title with skip message if the skip function returns a string. You can select the other way around with rendererOptions: { collapseSkips: false }
for the default renderer to show the skip message under the task title.
Showing output from a task can be done in various ways.
To keep the output when the task finishes while using default renderer, you can set { persistentOutput: true }
in the Task
.
new Listr<Ctx>([
{
title: 'This task will execute.',
task: async (ctx, task): Promise<void> => {
task.output = 'I will push an output. [0]'
},
options: { persistentOutput: true }
}
], { concurrent: false })
Please refer to examples section for more detailed and further examples.
This will show the output in a small bar that can only show the last output from the task.
new Listr<Ctx>([
{
title: 'This task will execute.',
task: async (ctx, task): Promise<void> => {
task.output = 'I will push an output. [0]'
await delay(500)
task.output = 'I will push an output. [1]'
await delay(500)
task.output = 'I will push an output. [2]'
await delay(500)
}
}
], { concurrent: false })
If task output to the bottom bar is selected, it will create a bar at the end of the tasks leaving one line return space in between. The bottom bar can only be used in the default renderer.
Items count that is desired to be showed in the bottom bar can be set through Task
option bottomBar
.
true
it will only show the last output from the task.Infinity
, it will keep all the output.new Listr<Ctx>([
{
title: 'This task will execute.',
task: async (ctx, task): Promise<void> => {
task.output = 'I will push an output. [0]'
await delay(500)
task.output = 'I will push an output. [1]'
await delay(500)
task.output = 'I will push an output. [2]'
await delay(500)
},
options: {
bottomBar: Infinity
}
}
], { concurrent: false })
Since observables and streams are supported they can also be used to generate output.
new Listr<Ctx>([
{
// Task can also handle and observable
title: 'Observable test.',
task: (): Observable<string> =>
new Observable((observer) => {
observer.next('test')
delay(500)
.then(() => {
observer.next('changed')
return delay(500)
})
.then(() => {
observer.complete()
})
})
}
], { concurrent: false })
You can throw errors out of the tasks to show they are insuccessful. While this gives a visual output on the terminal, it also handles how to handle tasks that are failed. The default behaviour is any of the tasks have failed, it will deem itself as unsuccessful and exit. This behaviour can be changed with exitOnError
option.
new Listr<Ctx>([
{
title: 'This task will fail.',
task: async (): Promise<void> => {
await delay(2000)
throw new Error('This task failed after 2 seconds.')
}
},
{
title: 'This task will never execute.',
task: (ctx, task): void => {
task.title = 'I will change my title if this executes.'
}
}
], { concurrent: false })
new Listr<Ctx>([
{
title: 'This task will fail.',
task: async (): Promise<void> => {
await delay(2000)
throw new Error('This task failed after 2 seconds.')
}
},
{
title: 'This task will execute.',
task: (ctx, task): void => {
task.title = 'I will change my title since it is concurrent.'
}
}
], { concurrent: true })
exitOnError
option.new Listr<Ctx>([
{
title: 'This task will fail.',
task: async (): Promise<void> => {
await delay(2000)
throw new Error('This task failed after 2 seconds.')
}
},
{
title: 'This task will execute.',
task: (ctx, task): void => {
task.title = 'I will change my title if this executes.'
}
}
], { concurrent: false, exitOnError: false })
exitOnError
is subtask based so you can change it on the fly for given set of subtasks.new Listr<Ctx>([
{
title: 'This task will execute and not quit on errors.',
task: (ctx, task): Listr => task.newListr([
{
title: 'This is a subtask.',
task: async (): Promise<void> => {
throw new Error('I have failed [0]')
}
},
{
title: 'This is yet an another subtask and it will run.',
task: async (ctx, task): Promise<void> => {
task.title = 'I have succeeded.'
}
}
], { exitOnError: false })
},
{
title: 'This task will execute.',
task: (): void => {
throw new Error('I will exit on error since I am a direct child of parent task.')
}
}
], { concurrent: false, exitOnError: true })
try {
const context = await task.run()
} catch(e) {
logger.fail(e)
// which will show the last error
}
task.err
which is an array of all the errors encountered.const task = new Listr(...)
logger.fail(task.err)
// will show all of the errors that are encountered through execution
public message: string
public errors?: Error[]
public context?: any
Task manager is a great way to create a custom-tailored Listr class once and then utilize it more than once.
Please refer to examples section for more detailed and further examples.
export function TaskManagerFactory<T = any> (override?: ListrBaseClassOptions): Manager<T> {
const myDefaultOptions: ListrBaseClassOptions = {
concurrent: false,
exitOnError: false,
rendererOptions: {
collapse: false,
collapseSkips: false
}
}
return new Manager({ ...myDefaultOptions, ...override })
}
export class MyMainClass {
private tasks = TaskManagerFactory<Ctx>()
constructor () {
this.run()
}
private async run (): Promise<void> {
// CODE WILL GO HERE IN THIS EXAMPLE
}
}
this.tasks.add([
{
title: 'A task running manager [0]',
task: async (): Promise<void> => {
throw new Error('Do not dare to run the second task.')
}
},
{
title: 'This will never run first one failed.',
task: async (): Promise<void> => {
await delay(2000)
}
}
], { exitOnError: true, concurrent: false })
try {
const ctx = await this.tasks.runAll()
} catch (e) {
this.logger.fail(e)
}
concurrency
, exitOnError
and so on.this.tasks.add([
{
title: 'Some task that will run in sequential execution mode. [0]',
task: async (): Promise<void> => {
await delay(2000)
}
},
{
title: 'Some task that will run in sequential execution mode. [1]',
task: async (): Promise<void> => {
await delay(2000)
}
},
this.tasks.indent([
{
title: 'This will run in parallel. [0]',
task: async (): Promise<void> => {
await delay(2000)
}
},
{
title: 'This will run in parallel. [1]',
task: async (): Promise<void> => {
await delay(2000)
}
}
])
], { concurrent: true })
await this.tasks.run([
{
title: 'I will survive, dont worry',
task: (): void => {
throw new Error('This will not crash since exitOnError is set to false eventhough default setting in Listr is false.')
}
}
])
await this.tasks.run([
{
title: 'I will survive, dont worry',
task: (): void => {
throw new Error('This will not crash since exitOnError is set to false eventhough default setting in Listr is false.')
}
}
])
this.logger.data(this.tasks.err.toString())
// will yield: ListrError: Task failed without crashing. with the error details in the object
try {
await this.tasks.newListr([
{
title: 'I will die now, goodbye my freinds.',
task: (): void => {
throw new Error('This will not crash since exitOnError is set to false eventhough default setting in Listr is false.')
}
}
]).run()
} catch (e) {
this.logger.fail(e)
}
await this.tasks.run([
{
task: async (ctx): Promise<void> => {
// start the clock
ctx.runTime = Date.now()
}
},
{
title: 'Running',
task: async (): Promise<void> => {
await delay(1000)
}
},
{
task: async (ctx, task): Promise<string> => task.title = this.tasks.getRuntime(ctx.runTime)
}
], { concurrent: false })
// outputs: "1.001s" in seconds
For default renderer, all tasks that do not have titles will be hidden from the visual task list and executed behind. You can still set task.title
inside the task wrapper programmatically afterward, if you so desire.
Since tasks can have subtasks as in the form of Listr classes again, if a task without a title does have subtasks with the title it will be rendered one less level indented. So you can use this operation to change the individual options of the set of tasks like exitOnError
or concurrency
or even render properties, like while you do want collapse parent's subtasks after completed but do not want this for a given set of subtasks.
For verbose renderer, since it is not updating, it will show tasks that do not have a title as Task without title.
When the interrupt signal is caught Listr will render for one last time therefore you will always have clean exits. This registers event listener process.on('exit')
, therefore it will use a bit more of CPU cycles depending on the Listr task itself.
You can disable this default behavior by passing in the options for the root task { registerSignalListeners: false }
.
For testing purposes you can use the verbose renderer by passing in the option of { renderer: 'verbose' }
. This will generate text-based and linear output which is required for testing.
If you want to change the logger of the verbose renderer you can do that by passing a class implementing Logger
class which is exported from the index and passing it through as a renderer option with { renderer: 'verbose', rendererOptions: { logger: MyLoggerClass } }
.
Verbose renderer will always output predicted output with no fancy features.
On | Output |
---|---|
Task Started | [STARTED] ${TASK TITLE ?? 'Task without title.'} |
Task Failure | [FAILED] ${TASK TITLE ?? 'Task without title.'} |
Task Skipped | [SKIPPED] ${TASK TITLE ?? 'Task without title.'} |
Task Successful | [SUCCESS] ${TASK TITLE ?? 'Task without title.'} |
Spit Output | [DATA] ${TASK OUTPUT} |
Title Change | [TITLE] ${NEW TITLE} |
There are three main renderers which are 'default', 'verbose' and 'silent'. Default renderer is the one that can be seen in the demo, which is an updating renderer. But if the environment advirteses itself as non-tty it will fallback to the verbose renderer automatically. Verbose renderer is a text based renderer. It uses the silent renderer for the subtasks since the parent task already started a renderer. But silent renderer can also be used for processes that wants to have no output but just a task list.
Depending on the selected renderer, rendererOptions
as well as the options
in the Task
will change accordingly. It defaults to default renderer as mentioned with the fallback to verbose renderer on non-tty environments.
public static rendererOptions: {
indentation?: number
clearOutput?: boolean
showSubtasks?: boolean
collapse?: boolean
collapseSkips?: boolean
} = {
indentation: 2,
clearOutput: false,
showSubtasks: true,
collapse: true,
collapseSkips: true
}
public static rendererTaskOptions: {
bottomBar?: boolean | number
persistentOutput?: boolean
}
public static rendererOptions: { useIcons?: boolean, logger?: new (...args: any) => Logger }
Creating a custom renderer with a beautiful interface can be done in one of two ways.
/* eslint-disable @typescript-eslint/no-empty-function */
import { ListrRenderer, ListrTaskObject } from 'listr2'
export class MyAmazingRenderer implements ListrRenderer {
// Designate this renderer as tty or nonTTY
public static nonTTY = true
// designate your renderer options that will be showed inside the `ListrOptions` as rendererOptions
public static rendererOptions: never
// designate your custom internal task-based options that will show as `options` in the task itself
public static rendererTaskOptions: never
// get tasks to be renderered and options of the renderer from the parent
constructor (public tasks: ListrTaskObject<any, typeof MyAmazingRenderer>[], public options: typeof MyAmazingRenderer['rendererOptions']) {}
// implement custom logic for render functionality
render (): void {}
// implement custom logic for end functionality
end (err): void {}
}
id: taskUUID
hasSubtasks(): boolean
isPending(): boolean
isSkipped(): boolean
isCompleted(): boolean
isEnabled(): boolean
isPrompt(): boolean
hasFailed(): boolean
hasTitle(): boolean
event
has event.type
which can either be SUBTASK
, STATE
, DATA
or TITLE
and event.data
depending on the event.type
. Take a look at verbose renderer since it is implemented this way.tasks?.forEach((task) => {
task.subscribe((event: ListrEvent) => {
...
Logging to a file can be done utilizing a module like winston. This can be obtained through using the verbose renderer and creating a custom logger class that implements Logger
which is exported from the index.
While calling a new Listr you can call it with { renderer: 'verbose', rendererOptions: { logger: MyLoggerClass } }
.
import { logLevels, Logger } from 'listr2'
export class MyLoggerClass implements Logger {
constructor (private options?: LoggerOptions) {}
/* CUSTOM LOGIC */
/* CUSTOM LOGIC */
/* CUSTOM LOGIC */
public fail (message: string): void {
message = this.parseMessage(logLevels.fail, message)
console.error(message)
}
public skip (message: string): void {
message = this.parseMessage(logLevels.skip, message)
console.warn(message)
}
public success (message: string): void {
message = this.parseMessage(logLevels.success, message)
console.log(message)
}
public data (message: string): void {
message = this.parseMessage(logLevels.data, message)
console.info(message)
}
public start (message: string): void {
message = this.parseMessage(logLevels.start, message)
console.log(message)
}
public title (message: string): void {
message = this.parseMessage(logLevels.title, message)
console.info(message)
}
}
To migrate from prior versions that are older than v1.3.12, which is advisable due to upcoming potential bug fixes:
new Listr<Ctx>([
{
task: async (ctx, task): Promise<void> => {
},
persistentOutput: true
}
], {
concurrent: false,
collapse: true
new Listr<Ctx>([
{
task: async (ctx, task): Promise<void> => {
},
options: { persistentOutput: true } // per task based options are moved to their own key
}
], {
concurrent: false,
rendererOptions: { collapse: false }
// global renderer options moved to their own key
})
let task: Listr<Ctx>
task = new Listr(..., { renderer: 'verbose' })
// this without the indication of verbose will now fail due to default renderer being 'default' for autocompleting goodness of the IDEs.
// So you have to overwrite it manually to 'verbose'.
// If it does not have a default you had to explicitly write { renderer: 'default' } everytime to have the auto complete feature
let task: Listr<Ctx, 'verbose'>
task = new Listr(..., { renderer: 'verbose' })
const task = new Listr(..., { renderer: 'test' })
const task = new Listr(..., { renderer: 'verbose' })
Useful types are exported from the root. It is written with Typescript, so it will work great with any modern IDE/Editor.
FAQs
Terminal task list reborn! Create beautiful CLI interfaces via easy and logical to implement task lists that feel alive and interactive.
The npm package listr2 receives a total of 14,695,856 weekly downloads. As such, listr2 popularity was classified as popular.
We found that listr2 demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Security News
A threat actor's playbook for exploiting the npm ecosystem was exposed on the dark web, detailing how to build a blockchain-powered botnet.
Security News
NVD’s backlog surpasses 20,000 CVEs as analysis slows and NIST announces new system updates to address ongoing delays.
Security News
Research
A malicious npm package disguised as a WhatsApp client is exploiting authentication flows with a remote kill switch to exfiltrate data and destroy files.