Terminal task list

Related tags

Command Line listr
Overview

listr

GitHub Actions Coverage Status

Terminal task list

Install

$ npm install --save listr

Usage

const execa = require('execa');
const Listr = require('listr');

const tasks = new Listr([
	{
		title: 'Git',
		task: () => {
			return new Listr([
				{
					title: 'Checking git status',
					task: () => execa.stdout('git', ['status', '--porcelain']).then(result => {
						if (result !== '') {
							throw new Error('Unclean working tree. Commit or stash changes first.');
						}
					})
				},
				{
					title: 'Checking remote history',
					task: () => execa.stdout('git', ['rev-list', '--count', '--left-only', '@{u}...HEAD']).then(result => {
						if (result !== '0') {
							throw new Error('Remote history differ. Please pull changes.');
						}
					})
				}
			], {concurrent: true});
		}
	},
	{
		title: 'Install package dependencies with Yarn',
		task: (ctx, task) => execa('yarn')
			.catch(() => {
				ctx.yarn = false;

				task.skip('Yarn not available, install it via `npm install -g yarn`');
			})
	},
	{
		title: 'Install package dependencies with npm',
		enabled: ctx => ctx.yarn === false,
		task: () => execa('npm', ['install'])
	},
	{
		title: 'Run tests',
		task: () => execa('npm', ['test'])
	},
	{
		title: 'Publish package',
		task: () => execa('npm', ['publish'])
	}
]);

tasks.run().catch(err => {
	console.error(err);
});

Task

A task can return different values. If a task returns, it means the task was completed successfully. If a task throws an error, the task failed.

const tasks = new Listr([
	{
		title: 'Success',
		task: () => 'Foo'
	},
	{
		title: 'Failure',
		task: () => {
			throw new Error('Bar')
		}
	}
]);

Promises

A task can also be async by returning a Promise. If the promise resolves, the task completed successfully, if it rejects, the task failed.

const tasks = new Listr([
	{
		title: 'Success',
		task: () => Promise.resolve('Foo')
	},
	{
		title: 'Failure',
		task: () => Promise.reject(new Error('Bar'))
	}
]);

Tip: Always reject a promise with some kind of Error object.

Observable

A task can also return an Observable. The thing about observables is that it can emit multiple values and can be used to show the output of the task. Please note that only the last line of the output is rendered.

const {Observable} = require('rxjs');

const tasks = new Listr([
	{
		title: 'Success',
		task: () => {
			return new Observable(observer => {
				observer.next('Foo');

				setTimeout(() => {
					observer.next('Bar');
				}, 2000);

				setTimeout(() => {
					observer.complete();
				}, 4000);
			});
		}
	},
	{
		title: 'Failure',
		task: () => Promise.reject(new Error('Bar'))
	}
]);

You can use the Observable package you feel most comfortable with, like RxJS or zen-observable.

Streams

It's also possible to return a ReadableStream. The stream will be converted to an Observable and handled as such.

const fs = require('fs');
const split = require('split');

const list = new Listr([
	{
		title: 'File',
		task: () => fs.createReadStream('data.txt', 'utf8')
			.pipe(split(/\r?\n/, null, {trailing: false}))
	}
]);

Skipping tasks

Optionally specify a skip function to determine whether a task can be skipped.

  • If the skip function returns a truthy value or a Promise that resolves to a truthy value then the task will be skipped.
  • If the returned value is a string it will be displayed as the reason for skipping the task.
  • If the skip function returns a falsey value or a Promise that resolves to a falsey value then the task will be executed as normal.
  • If the skip function throws or returns a Promise that rejects, the task (and the whole build) will fail.
const tasks = new Listr([
	{
		title: 'Task 1',
		task: () => Promise.resolve('Foo')
	},
	{
		title: 'Can be skipped',
		skip: () => {
			if (Math.random() > 0.5) {
				return 'Reason for skipping';
			}
		},
		task: () => 'Bar'
	},
	{
		title: 'Task 3',
		task: () => Promise.resolve('Bar')
	}
]);

Tip: You can still skip a task while already executing the task function with the task object.

Enabling tasks

By default, every task is enabled which means that every task will be executed. However, it's also possible to provide an enabled function that returns whether the task should be executed or not.

const tasks = new Listr([
	{
		title: 'Install package dependencies with Yarn',
		task: (ctx, task) => execa('yarn')
			.catch(() => {
				ctx.yarn = false;

				task.skip('Yarn not available, install it via `npm install -g yarn`');
			})
	},
	{
		title: 'Install package dependencies with npm',
		enabled: ctx => ctx.yarn === false,
		task: () => execa('npm', ['install'])
	}
]);

In the above example, we try to run yarn first, if that fails we will fall back to npm. However, at first only the Yarn task will be visible. Because we set the yarn flag of the context object to false, the second task will automatically be enabled and will be executed.

Note: This does not work in combination with concurrent tasks.

Context

A context object is passed as argument to every skip and task function. This allows you to create composable tasks and change the behaviour of your task depending on previous results.

const tasks = new Listr([
	{
		title: 'Task 1',
		skip: ctx => ctx.foo === 'bar',
		task: () => Promise.resolve('Foo')
	},
	{
		title: 'Can be skipped',
		skip: () => {
			if (Math.random() > 0.5) {
				return 'Reason for skipping';
			}
		},
		task: ctx => {
			ctx.unicorn = 'rainbow';
		}
	},
	{
		title: 'Task 3',
		task: ctx => Promise.resolve(`${ctx.foo} ${ctx.bar}`)
	}
]);

tasks.run({
	foo: 'bar'
}).then(ctx => {
	console.log(ctx);
	//=> {foo: 'bar', unicorn: 'rainbow'}
});

Task object

A special task object is passed as second argument to the task function. This task object lets you change the title while running your task, you can skip it depending on some results or you can update the task's output.

const tasks = new Listr([
	{
		title: 'Install package dependencies with Yarn',
		task: (ctx, task) => execa('yarn')
			.catch(() => {
				ctx.yarn = false;

				task.title = `${task.title} (or not)`;
				task.skip('Yarn not available');
			})
	},
	{
		title: 'Install package dependencies with npm',
		skip: ctx => ctx.yarn !== false && 'Dependencies already installed with Yarn',
		task: (ctx, task) => {
			task.output = 'Installing dependencies...';

			return execa('npm', ['install'])
		}
	}
]);

tasks.run();

Custom renderers

It's possible to write custom renderers for Listr. A renderer is an ES6 class that accepts the tasks that it should render, and the Listr options object. It has two methods, the render method which is called when it should start rendering, and the end method. The end method is called when all the tasks are completed or if a task failed. If a task failed, the error object is passed in via an argument.

class CustomRenderer {

	constructor(tasks, options) { }

	static get nonTTY() {
		return false;
	}

	render() { }

	end(err) { }
}

module.exports = CustomRenderer;

Note: A renderer is not passed through to the subtasks, only to the main task. It is up to you to handle that case.

The nonTTY property returns a boolean indicating if the renderer supports non-TTY environments. The default for this property is false if you do not implement it.

Observables

Every task is an observable. The task emits three different events and every event is an object with a type property.

  1. The state of the task has changed (STATE).
  2. The task outputted data (DATA).
  3. The task returns a subtask list (SUBTASKS).
  4. The task's title changed (TITLE).
  5. The task became enabled or disabled (ENABLED).

This allows you to flexibly build your UI. Let's render every task that starts executing.

class CustomRenderer {

	constructor(tasks, options) {
		this._tasks = tasks;
		this._options = Object.assign({}, options);
	}

	static get nonTTY() {
		return true;
	}

	render() {
		for (const task of this._tasks) {
			task.subscribe(event => {
				if (event.type === 'STATE' && task.isPending()) {
					console.log(`${task.title} [started]`);
				}
			});
		}
	}

	end(err) { }
}

module.exports = CustomRenderer;

If you want more complex examples, take a look at the update and verbose renderers.

API

Listr([tasks], [options])

tasks

Type: object[]

List of tasks.

title

Type: string

Title of the task.

task

Type: Function

Task function.

skip

Type: Function

Skip function. Read more about skipping tasks.

options

Any renderer specific options. For instance, when using the update-renderer, you can pass in all of its options.

concurrent

Type: boolean number
Default: false

Set to true if you want to run tasks in parallel, set to a number to control the concurrency. By default it runs tasks sequentially.

exitOnError

Type: boolean
Default: true

Set to false if you don't want to stop the execution of other tasks when one or more tasks fail.

renderer

Type: string object
Default: default
Options: default verbose silent

Renderer that should be used. You can either pass in the name of the known renderer, or a class of a custom renderer.

nonTTYRenderer

Type: string object
Default: verbose

The renderer that should be used if the main renderer does not support TTY environments. You can either pass in the name of the renderer, or a class of a custom renderer.

Instance

add(task)

Returns the instance.

task

Type: object object[]

Task object or multiple task objects.

run([context])

Start executing the tasks. Returns a Promise for the context object.

context

Type: object
Default: Object.create(null)

Initial context object.

Related

  • ora - Elegant terminal spinner
  • cli-spinners - Spinners for use in the terminal

License

MIT © Sam Verschueren


Get professional support for this package with a Tidelift subscription
Tidelift helps make open source sustainable for maintainers while giving companies
assurances about security, maintenance, and licensing for their dependencies.
Comments
  • Completing all concurrent tasks if one errors.

    Completing all concurrent tasks if one errors.

    I'm running a set of tasks concurrently. At the moment, if one of the tasks fails, the whole execution is stopped.

    Is it possible to have them all continue to completion, whether failed or passed?

    Thanks

    enhancement 
    opened by philcockfield 26
  • Fix failing tests

    Fix failing tests

    Changes

    • Dropped support for Node.js 8
    • Fixed lint with latest XO
    • Fixed AVA usages
    • Removed AppVeyor and Travis CI configuration files
    • Started using GitHub Actions
    opened by LitoMore 23
  • Automatically fallback in non-TTY environments

    Automatically fallback in non-TTY environments

    This has been playing in my head for quite a while now. So we have support for custom (3rd party) renderers like the current embedded update-renderer and the verbose-renderer. The default renderer is the update renderer, also in a non-TTY environment which doesn't work well with ANSI escape codes.

    This means that you will have to write something like this.

    new Listr([
        // tasks go here
    ], {
        renderer: process.isTTY ? 'update': 'verbose'
    });
    

    I want to get rid of this and automatically fall back to the verbose renderer if Listr internally detects that it is not running in a TTY. This is perfectly possible and I believe that we should really do this. It doesn't make sense to render fancy stuff with log-update for instance of it doesn't work correctly.

    But what should be done with custom renderers then? Let's first recap what a custom renderer is and how it can be used. If you look into the custom renderers section of the readme, you can see that the basic structure looks like this.

    class CustomRenderer {
    
        constructor(tasks, options) { }
    
        render() { }
    
        end(err) { }
    }
    

    We can then use it and pass it in as renderer.

    const custom = require('custom-renderer');
    
    new Listr([
        // tasks go here
    ], {
        renderer: custom
    });
    

    What should we do now when executed in a non-TTY environment?

    Option 1. Always use the verbose renderer

    We could always use the verbose renderer by default. The drawback is that you will have to live with it that the output always looks the same.

    Option 2. Custom renderer lets us know that it supports those environments

    The custom renderer could set a property indicating if it supports non-TTY environments.

    class CustomRenderer {
    
        constructor(tasks, options) { }
    
        get nonTTY() {
            return true;
        }
    
        render() { }
    
        end(err) { }
    }
    

    If Listr detects that it isn't running in a TTY, it will look check if nonTTY returns true, if that is the case it could safely use that renderer. If it returns a falsy value, it will fall back to the verbose renderer.

    Option 3. Fallback renderer

    If you pass in a custom renderer, it will automatically use that renderer in all environments. Except if you pass in another renderer as fallback renderer.

    new Listr([
        // tasks go here
    ], {
        renderer: custom,
        fallbackRenderer: 'verbose'
    });
    

    Options to be decided. Could as well be something like

    new Listr([
        // tasks go here
    ], {
        renderer: {
            default: custom,
            noTTY: 'verbose'
        }
    });
    

    Any feedback or input on these ideas would be more then appreciated!

    // @sindresorhus @jfmengels @kevva @andywer @okonet

    enhancement 
    opened by SamVerschueren 23
  • Add support for skipping tasks

    Add support for skipping tasks

    skipping

    This is an attempt at adding support for skipping tasks. The API is that a task function is passed a "task" object that has a skip() function on it. At any point in your task execution if you invoke skip() (optionally with a reason) it will abort that task, flag it as skipped and the reason will remain present in the task list.

    const list = new Listr([
        {
            title: 'foo',
            task: task => {
                task.skip('foo bar');
            }
        }
    ]);
    
    screen shot 2016-07-20 at 8 13 46 pm

    Feedback welcome on the implementation, output or API. If/once you're happy with it then I can polish things up, add a few more tests, docs etc.

    Fixes #15.

    opened by unkillbob 15
  • List is sometimes pushed up

    List is sometimes pushed up

    When running np on https://github.com/sindresorhus/sindre-playground the list jumps up when "Running tests" is showing output. It should rather push the list down.

    bug

    bug 
    opened by sindresorhus 15
  • Unexpected peer dependency?

    Unexpected peer dependency?

    It seems like the latest version (0.14.0) introduced a new peer dependency. I get this error when I try to load listr in a new project:

    $ node -p "require('listr')"
    /home/targos/test/node_modules/any-observable/register.js:29
    		throw new Error('Cannot find any-observable implementation nor' +
    		^
    
    Error: Cannot find any-observable implementation nor global.Observable. You must install polyfill or call require("any-observable/register") with your preferred implementation, e.g. require("any-observable/register")('rxjs') on application load prior to any require("any-observable").
        at loadImplementation (/home/targos/test/node_modules/any-observable/register.js:29:9)
        at register (/home/targos/test/node_modules/any-observable/loader.js:32:18)
        at Object.<anonymous> (/home/targos/test/node_modules/any-observable/index.js:2:39)
        at Module._compile (internal/modules/cjs/loader.js:654:30)
        at Object.Module._extensions..js (internal/modules/cjs/loader.js:665:10)
        at Module.load (internal/modules/cjs/loader.js:566:32)
        at tryModuleLoad (internal/modules/cjs/loader.js:506:12)
        at Function.Module._load (internal/modules/cjs/loader.js:498:3)
        at Module.require (internal/modules/cjs/loader.js:598:17)
        at require (internal/modules/cjs/helpers.js:11:18)
    
    bug 
    opened by targos 14
  • Retrieve errors when `exitOnError` is set to false

    Retrieve errors when `exitOnError` is set to false

    From @okonet

    I've just tried to integrate it and can't wrap my head on how do I display all errors when all tasks complete. When using default renderer, it just continues silently now, so I was expecting the error argumemt in the catch to contain all my errors. Looking at the code it is not the case. Is there a way of accessing it somehow after the execution? Should it be some internal state I have to take care of? Please point me in the right direction.

    Because exitOnError is set to false, at the end, the .catch function is not being invoked and you end up in then. Because the result of the promise is the context object, we could add an errors array and push all the errors into that list. A downside of this approach though is that we should make it a read-only property so that people can't overwrite that property.

    @okonet @frederickfogerty thoughts?

    enhancement help wanted 
    opened by SamVerschueren 14
  • Any way to preserve colors in the output?

    Any way to preserve colors in the output?

    I'm trying to incorporate this module to lint-staged: https://github.com/okonet/lint-staged/pull/32 and running into a few issues. I'd like to have all errors from subsequent tasks (i.e. linters) be outputted to the console "as is".

    So, the "right" way of doing it ATM is like:

    return new Promise((resolve, reject) => {
                            execa(res.bin, res.args)
                                .then(() => {
                                    resolve(`${linter} passed!`)
                                })
                                .catch(err => {
                                    reject(`
    🚨  ${linter} found some errors. Please fix them and try committing again.
    
    ${err.stdout}
    `
                                    )
                                })
                        })
    

    See https://github.com/okonet/lint-staged/blob/47099e87d059f79ebc7d8bbe5c0e80069704f5bf/src/runScript.js#L14-L27

    This works fine, but the output looses all the colors. This sucks!

    To preserve colors, it seems there is no other way as spawning a process with stdio: "inherit" option passed in. This makes output colored, but at the same time breaks the output of this module. It now appears twice and (worst thing) it cuts some of the stdout output from the task.

    2016-07-23 at 11 37

    I'm wondering what would be a better work around this.

    See also #16

    bug 
    opened by okonet 13
  • Add Task on the run

    Add Task on the run

    Hi,

    I'm having a slightly different use case: I would like to add task to Listr while it's already running (my task list is unknown when the program start).

    On my first tries, the 2nd task is rendered but the spinner never shows and the list complete before the return of the second promise. My guess is that it use a reduce to run the tasks and it won't accept new entries while reducing.

    I'm working on a recursive approach that would shift the array until null. Do you think it can work? Any advice? Thanks

    opened by JbIPS 10
  • 0.6.0 broke default rendering

    0.6.0 broke default rendering

    Hey @SamVerschueren, I am glad for the new renderers!

    But I just experienced an issue when trying to use 0.6.0 for npm-launch. Seems like it doesn't render much anymore when using the default renderer. Maybe it's rendering the listr instance as completed before it actually is?

    listr-0 5 0

    listr-0 6 0

    bug 
    opened by andywer 9
  • Updating a task in place

    Updating a task in place

    I am stoked about this module. I think it has potential to be the way to represent any non-trivial CLI behavior. I can imagine it being used in Yeoman, test runners, etc.

    One thing I think we will need is to address some of what @jamestalmage says in https://github.com/sindresorhus/np/issues/30#issuecomment-225275715.

    when I'm installing something that has a long build step as part of it's install ... I want to see what's causing the hold up

    That is to say, tasks have an associated status that is meaningful in some cases. For npm, a progress bar may be useful, but even just seeing which dependency is currently being installed would help explain long installs at a quick glance.

    To achieve this, I imagine a method that would allow me to update the task description in place. Or maybe just characters to the right of the task description if you want to be opinionated about it and protect end users from crazy mutations to the task list.

    opened by sholladay 9
  • Update ansi-regex dependency

    Update ansi-regex dependency

    opened by marcello33 0
  • Spinners not displaying in output

    Spinners not displaying in output

    Hi all,

    I am using Listr as part of the lint-staged package. Just recently on one of my machines, I have stopped seeing the spinners and instead, I get an output like the below. Does anyone know what might cause this to happen?

    [STARTED] Preparing lint-staged...
    [SUCCESS] Preparing lint-staged...
    [STARTED] Running tasks for staged files...
    [STARTED] package.json — 2 files
    [STARTED] *?(test|spec).{js,jsx,ts,tsx} — 0 file
    [STARTED] *.{css,scss,md,html,json,yml,yaml} — 1 file
    [SKIPPED] *?(test|spec).{js,jsx,ts,tsx} — no files
    [STARTED] yarn pretty:fix
    [SUCCESS] yarn pretty:fix
    [SUCCESS] *.{css,scss,md,html,json,yml,yaml} — 1 file
    [SUCCESS] package.json — 2 files
    [SUCCESS] Running tasks for staged files...
    [STARTED] Applying modifications from tasks...
    [SUCCESS] Applying modifications from tasks...
    [STARTED] Cleaning up temporary files...
    [SUCCESS] Cleaning up temporary files...
    

    Thanks,

    D

    opened by damienbutt 0
  • Ugly output on task failure

    Ugly output on task failure

    I am throwing an error in my task, as so.

    image

    And as you can see, I am receiving ugly output as a result - for some reason the stack trace is coming through alongside everything else, on top of the expected behaviour from Listr.

    image

    I have made no changes to Listr's configuration, why could this be happening?

    opened by newtykins 1
  • (Async)Iterator support

    (Async)Iterator support

    I love using listr for handling complex trees of async tasks, and I especially love the way it handles Observables/ReadableStreams by rendering the last emitted value below the task title.

    Recently, I've been learning about (and experimenting with) the AsyncIteratable pattern which has been supported in node.js since v10.

    While AsyncIterables behave subtly differently to Observables and ReadableStreams, they at least have in common that they asynchronously yield zero or more values before completing.

    Accordingly, it would be amazing for listr to accept AsyncIterables as a task callback's return value, much as it already supports Observables and ReadableStreams.

    opened by jimmed 0
Releases(v0.14.3)
  • v0.14.3(Oct 27, 2019)

    • Bump dependencies 5157edfcaaf32af74dd6e56d2b195b429b70e9b1
    • Tweak documentation (#109) 927b2daf8eb93a4e9ade36d487563206c66597eb
    • Add notes about Observables libraries (#103) 6d19ba711e0b2a53aa80f957aff182e4306e3f84
    • Fix tests (#123) 2856259d38ddf35b3bfac2935eb9c4dfcad3ec1b
    • Fix example (#124) 792604c898da703a3012dc164ad606c532988670
    Source code(tar.gz)
    Source code(zip)
Owner
Sam Verschueren
Sam Verschueren
Terminal ui for discord with interactive terminal

dickord why No fucking clue i was bored or something. why does it look dogshit Try and find a node module that supports terminal functions like trauma

Hima 3 Nov 7, 2022
Sublime-like terminal-based text editor

slap ?? slap is a Sublime-like terminal-based text editor that strives to make editing from the terminal easier. It has: first-class mouse support (ev

slap 6.1k Jan 1, 2023
A terminal-to-gif recorder minus the headaches.

ttystudio A terminal-to-gif recorder minus the headaches. Record your terminal and compile it to a GIF or APNG without any external dependencies, bash

Christopher Jeffrey (JJ) 3.2k Dec 23, 2022
rtail(1) - Terminal output to the browser in seconds, using UNIX pipes.

rtail(1) Terminal output to the browser in seconds, using UNIX pipes. rtail is a command line utility that grabs every line in stdin and broadcasts it

Kilian Ciuffolo 1.6k Jan 6, 2023
Pipeable javascript. Quickly filter, map, and reduce from the terminal

Pipeable JavaScript - another utility like sed/awk/wc... but with JS! Quickly filter, map and reduce from the command line. Features a streaming API.

Daniel St. Jules 410 Dec 10, 2022
Translations with speech synthesis in your terminal as a node package

Normit Normit is an easy way to translate stuff in your terminal. You can check out its Ruby gem version termit. Installation npm install normit -g Us

Paweł Urbanek 234 Jan 1, 2023
Terminal recorder: Record your termial session into HTML

terminal-recorder Terminal recorder allows you to record your bash session, and export it to html so then you can share it with your friends. GitHub P

Cristian Cortez 104 Mar 3, 2022
📜 Create mutable log lines into the terminal, and give life to your logs!

Because Logging can be pretty and fun Installation $ npm install draftlog What it does It allows you to re-write a line of your log after being writt

Ivan Seidel 1.2k Dec 31, 2022
Display images in the terminal

terminal-image Display images in the terminal Works in any terminal that supports colors. In iTerm, the image will be displayed in full resolution, si

Sindre Sorhus 905 Dec 25, 2022
:rainbow: Beautiful color gradients in terminal output

gradient-string Beautiful color gradients in terminal output Install $ npm i gradient-string Usage const gradient = require('gradient-string'); cons

Boris K 864 Jan 3, 2023
Create clickable links in the terminal

terminal-link Create clickable links in the terminal Install $ npm install terminal-link Usage import terminalLink from 'terminal-link'; const link

Sindre Sorhus 539 Dec 31, 2022
Reliably get the terminal window size

term-size Reliably get the terminal window size Because process.stdout.columns doesn't exist when run non-interactively, for example, in a child proce

Sindre Sorhus 132 Oct 11, 2022
Truncate a string to a specific width in the terminal

cli-truncate Truncate a string to a specific width in the terminal Gracefully handles ANSI escapes. Like a string styled with chalk. It also supports

Sindre Sorhus 78 Oct 10, 2022
Execute shell commands in terminal

Execute shell commands in terminal

skanehira 9 Dec 11, 2021
Add a hungry turtle to your terminal and feed it every time you mistype 'npm' as 'nom'

Nom Does this ever happen to you? You happily code away on a project, navigating the command line like a pro, testing, error logging, installing packa

Meike Hankewicz 5 Apr 26, 2022
Just a minimal library to do some terminal stuff.

Termctl A simple library to do some basic terminal stuff. Usage const termctl = require("termctl"); Note: We have tested this on Linux Mint and Window

Biraj 4 Sep 28, 2021
A terminal for a more modern age

Downloads: Latest release Repositories: Debian/Ubuntu-based, RPM-based Latest nightly build This README is also available in: Korean 简体中文 Tabby (forme

null 41.8k Dec 30, 2022
DataENV is a cli tool that allows you to save data temporarily using your terminal.

DataEnv CLI Instllation npm install -g dataenv Usage Table of Contents LocalStorage npx dataenv save Parameters npx dataenv show Parameters npx dataen

PGamerX 2 Feb 5, 2022