{"id":13454031,"url":"https://github.com/samverschueren/listr","last_synced_at":"2025-04-23T20:53:10.357Z","repository":{"id":9081755,"uuid":"60807171","full_name":"SamVerschueren/listr","owner":"SamVerschueren","description":"Terminal task list","archived":false,"fork":false,"pushed_at":"2022-08-28T16:33:28.000Z","size":805,"stargazers_count":3291,"open_issues_count":63,"forks_count":109,"subscribers_count":21,"default_branch":"master","last_synced_at":"2025-04-21T06:08:22.496Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/SamVerschueren.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":".github/funding.yml","license":"license","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":".github/security.md","support":null},"funding":{"tidelift":"npm/listr"}},"created_at":"2016-06-09T21:07:30.000Z","updated_at":"2025-04-05T12:08:27.000Z","dependencies_parsed_at":"2022-07-10T13:47:45.342Z","dependency_job_id":null,"html_url":"https://github.com/SamVerschueren/listr","commit_stats":null,"previous_names":[],"tags_count":21,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SamVerschueren%2Flistr","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SamVerschueren%2Flistr/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SamVerschueren%2Flistr/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SamVerschueren%2Flistr/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/SamVerschueren","download_url":"https://codeload.github.com/SamVerschueren/listr/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250514765,"owners_count":21443208,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-07-31T08:00:50.457Z","updated_at":"2025-04-23T20:53:10.338Z","avatar_url":"https://github.com/SamVerschueren.png","language":"JavaScript","funding_links":["https://tidelift.com/funding/github/npm/listr","https://tidelift.com/subscription/pkg/npm-listr?utm_source=npm-listr\u0026utm_medium=referral\u0026utm_campaign=enterprise\u0026utm_term=repo"],"categories":["Packages","包","Uncategorized","Command-line utilities","目录"],"sub_categories":["Command-line utilities","命令行工具","Uncategorized"],"readme":"# listr\n\n[![GitHub Actions](https://github.com/SamVerschueren/listr/workflows/Node/badge.svg)](https://github.com/SamVerschueren/listr/actions)\n[![Coverage Status](https://codecov.io/gh/SamVerschueren/listr/branch/master/graph/badge.svg)](https://codecov.io/gh/SamVerschueren/listr)\n\n\u003e Terminal task list\n\n\u003cimg src=\"media/screenshot.gif\"\u003e\n\n## Install\n\n```\n$ npm install --save listr\n```\n\n\n## Usage\n\n```js\nimport execa from 'execa';\nimport Listr from 'listr';\n\nconst tasks = new Listr([\n\t{\n\t\ttitle: 'Git',\n\t\ttask: () =\u003e {\n\t\t\treturn new Listr([\n\t\t\t\t{\n\t\t\t\t\ttitle: 'Checking git status',\n\t\t\t\t\ttask: () =\u003e execa.stdout('git', ['status', '--porcelain']).then(result =\u003e {\n\t\t\t\t\t\tif (result !== '') {\n\t\t\t\t\t\t\tthrow new Error('Unclean working tree. Commit or stash changes first.');\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\ttitle: 'Checking remote history',\n\t\t\t\t\ttask: () =\u003e execa.stdout('git', ['rev-list', '--count', '--left-only', '@{u}...HEAD']).then(result =\u003e {\n\t\t\t\t\t\tif (result !== '0') {\n\t\t\t\t\t\t\tthrow new Error('Remote history differ. Please pull changes.');\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t], {concurrent: true});\n\t\t}\n\t},\n\t{\n\t\ttitle: 'Install package dependencies with Yarn',\n\t\ttask: (ctx, task) =\u003e execa('yarn')\n\t\t\t.catch(() =\u003e {\n\t\t\t\tctx.yarn = false;\n\n\t\t\t\ttask.skip('Yarn not available, install it via `npm install -g yarn`');\n\t\t\t})\n\t},\n\t{\n\t\ttitle: 'Install package dependencies with npm',\n\t\tenabled: ctx =\u003e ctx.yarn === false,\n\t\ttask: () =\u003e execa('npm', ['install'])\n\t},\n\t{\n\t\ttitle: 'Run tests',\n\t\ttask: () =\u003e execa('npm', ['test'])\n\t},\n\t{\n\t\ttitle: 'Publish package',\n\t\ttask: () =\u003e execa('npm', ['publish'])\n\t}\n]);\n\ntasks.run().catch(err =\u003e {\n\tconsole.error(err);\n});\n```\n\n\n## Task\n\nA `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.\n\n```js\nconst tasks = new Listr([\n\t{\n\t\ttitle: 'Success',\n\t\ttask: () =\u003e 'Foo'\n\t},\n\t{\n\t\ttitle: 'Failure',\n\t\ttask: () =\u003e {\n\t\t\tthrow new Error('Bar')\n\t\t}\n\t}\n]);\n```\n\n\n### Promises\n\nA `task` can also be async by returning a `Promise`. If the promise resolves, the task completed successfully, if it rejects, the task failed.\n\n```js\nconst tasks = new Listr([\n\t{\n\t\ttitle: 'Success',\n\t\ttask: () =\u003e Promise.resolve('Foo')\n\t},\n\t{\n\t\ttitle: 'Failure',\n\t\ttask: () =\u003e Promise.reject(new Error('Bar'))\n\t}\n]);\n```\n\n\u003e Tip: Always reject a promise with some kind of `Error` object.\n\n### Observable\n\n\u003cimg src=\"media/observable.gif\" width=\"250\" align=\"right\"\u003e\n\nA `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\ntask. Please note that only the last line of the output is rendered.\n\n```js\nimport {Observable} from 'rxjs';\n\nconst tasks = new Listr([\n\t{\n\t\ttitle: 'Success',\n\t\ttask: () =\u003e {\n\t\t\treturn new Observable(observer =\u003e {\n\t\t\t\tobserver.next('Foo');\n\n\t\t\t\tsetTimeout(() =\u003e {\n\t\t\t\t\tobserver.next('Bar');\n\t\t\t\t}, 2000);\n\n\t\t\t\tsetTimeout(() =\u003e {\n\t\t\t\t\tobserver.complete();\n\t\t\t\t}, 4000);\n\t\t\t});\n\t\t}\n\t},\n\t{\n\t\ttitle: 'Failure',\n\t\ttask: () =\u003e Promise.reject(new Error('Bar'))\n\t}\n]);\n```\n\nYou can use the `Observable` package you feel most comfortable with, like [RxJS](https://www.npmjs.com/package/rxjs) or [zen-observable](https://www.npmjs.com/package/zen-observable).\n\n### Streams\n\nIt's also possible to return a [`ReadableStream`](https://nodejs.org/api/stream.html#stream_class_stream_readable). The stream will be converted to an `Observable` and handled as such.\n\n```js\nimport fs from 'fs';\nimport split from 'split';\n\nconst list = new Listr([\n\t{\n\t\ttitle: 'File',\n\t\ttask: () =\u003e fs.createReadStream('data.txt', 'utf8')\n\t\t\t.pipe(split(/\\r?\\n/, null, {trailing: false}))\n\t}\n]);\n```\n\n### Skipping tasks\n\n\u003cimg src=\"media/skipped.png\" width=\"250\" align=\"right\"\u003e\n\nOptionally specify a `skip` function to determine whether a task can be skipped.\n\n- If the `skip` function returns a truthy value or a `Promise` that resolves to a truthy value then the task will be skipped.\n- If the returned value is a string it will be displayed as the reason for skipping the task.\n- 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.\n- If the `skip` function throws or returns a `Promise` that rejects, the task (and the whole build) will fail.\n\n```js\nconst tasks = new Listr([\n\t{\n\t\ttitle: 'Task 1',\n\t\ttask: () =\u003e Promise.resolve('Foo')\n\t},\n\t{\n\t\ttitle: 'Can be skipped',\n\t\tskip: () =\u003e {\n\t\t\tif (Math.random() \u003e 0.5) {\n\t\t\t\treturn 'Reason for skipping';\n\t\t\t}\n\t\t},\n\t\ttask: () =\u003e 'Bar'\n\t},\n\t{\n\t\ttitle: 'Task 3',\n\t\ttask: () =\u003e Promise.resolve('Bar')\n\t}\n]);\n```\n\n\u003e Tip: You can still skip a task while already executing the `task` function with the [task object](#task-object).\n\n## Enabling tasks\n\nBy 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.\n\n```js\nconst tasks = new Listr([\n\t{\n\t\ttitle: 'Install package dependencies with Yarn',\n\t\ttask: (ctx, task) =\u003e execa('yarn')\n\t\t\t.catch(() =\u003e {\n\t\t\t\tctx.yarn = false;\n\n\t\t\t\ttask.skip('Yarn not available, install it via `npm install -g yarn`');\n\t\t\t})\n\t},\n\t{\n\t\ttitle: 'Install package dependencies with npm',\n\t\tenabled: ctx =\u003e ctx.yarn === false,\n\t\ttask: () =\u003e execa('npm', ['install'])\n\t}\n]);\n```\n\nIn 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](https://github.com/SamVerschueren/listr#context) object to `false`, the second task will automatically be enabled and will be executed.\n\n\u003e Note: This does not work in combination with [concurrent](https://github.com/SamVerschueren/listr#concurrent) tasks.\n\n\n## Context\n\nA 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.\n\n```js\nconst tasks = new Listr([\n\t{\n\t\ttitle: 'Task 1',\n\t\tskip: ctx =\u003e ctx.foo === 'bar',\n\t\ttask: () =\u003e Promise.resolve('Foo')\n\t},\n\t{\n\t\ttitle: 'Can be skipped',\n\t\tskip: () =\u003e {\n\t\t\tif (Math.random() \u003e 0.5) {\n\t\t\t\treturn 'Reason for skipping';\n\t\t\t}\n\t\t},\n\t\ttask: ctx =\u003e {\n\t\t\tctx.unicorn = 'rainbow';\n\t\t}\n\t},\n\t{\n\t\ttitle: 'Task 3',\n\t\ttask: ctx =\u003e Promise.resolve(`${ctx.foo} ${ctx.bar}`)\n\t}\n]);\n\ntasks.run({\n\tfoo: 'bar'\n}).then(ctx =\u003e {\n\tconsole.log(ctx);\n\t//=\u003e {foo: 'bar', unicorn: 'rainbow'}\n});\n```\n\n\n## Task object\n\nA 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.\n\n```js\nconst tasks = new Listr([\n\t{\n\t\ttitle: 'Install package dependencies with Yarn',\n\t\ttask: (ctx, task) =\u003e execa('yarn')\n\t\t\t.catch(() =\u003e {\n\t\t\t\tctx.yarn = false;\n\n\t\t\t\ttask.title = `${task.title} (or not)`;\n\t\t\t\ttask.skip('Yarn not available');\n\t\t\t})\n\t},\n\t{\n\t\ttitle: 'Install package dependencies with npm',\n\t\tskip: ctx =\u003e ctx.yarn !== false \u0026\u0026 'Dependencies already installed with Yarn',\n\t\ttask: (ctx, task) =\u003e {\n\t\t\ttask.output = 'Installing dependencies...';\n\n\t\t\treturn execa('npm', ['install'])\n\t\t}\n\t}\n]);\n\ntasks.run();\n```\n\n\n## Custom renderers\n\nIt'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.\n\n```js\nclass CustomRenderer {\n\n\tconstructor(tasks, options) { }\n\n\tstatic get nonTTY() {\n\t\treturn false;\n\t}\n\n\trender() { }\n\n\tend(err) { }\n}\n\nmodule.exports = CustomRenderer;\n```\n\n\u003e Note: A renderer is not passed through to the subtasks, only to the main task. It is up to you to handle that case.\n\nThe `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.\n\n### Observables\n\nEvery task is an observable. The task emits three different events and every event is an object with a `type` property.\n\n1. The state of the task has changed (`STATE`).\n2. The task outputted data (`DATA`).\n3. The task returns a subtask list (`SUBTASKS`).\n4. The task's title changed (`TITLE`).\n5. The task became enabled or disabled (`ENABLED`).\n\nThis allows you to flexibly build your UI. Let's render every task that starts executing.\n\n```js\nclass CustomRenderer {\n\n\tconstructor(tasks, options) {\n\t\tthis._tasks = tasks;\n\t\tthis._options = Object.assign({}, options);\n\t}\n\n\tstatic get nonTTY() {\n\t\treturn true;\n\t}\n\n\trender() {\n\t\tfor (const task of this._tasks) {\n\t\t\ttask.subscribe(event =\u003e {\n\t\t\t\tif (event.type === 'STATE' \u0026\u0026 task.isPending()) {\n\t\t\t\t\tconsole.log(`${task.title} [started]`);\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t}\n\n\tend(err) { }\n}\n\nmodule.exports = CustomRenderer;\n```\n\nIf you want more complex examples, take a look at the [update](https://github.com/SamVerschueren/listr-update-renderer) and [verbose](https://github.com/SamVerschueren/listr-verbose-renderer) renderers.\n\n\n## API\n\n### Listr([tasks], [options])\n\n#### tasks\n\nType: `object[]`\n\nList of tasks.\n\n##### title\n\nType: `string`\n\nTitle of the task.\n\n##### task\n\nType: `Function`\n\nTask function.\n\n##### skip\n\nType: `Function`\n\nSkip function. Read more about [skipping tasks](#skipping-tasks).\n\n#### options\n\nAny renderer specific options. For instance, when using the `update-renderer`, you can pass in all of its [options](https://github.com/SamVerschueren/listr-update-renderer#options).\n\n##### concurrent\n\nType: `boolean` `number`\u003cbr\u003e\nDefault: `false`\n\nSet to `true` if you want to run tasks in parallel, set to a number to control the concurrency. By default it runs tasks sequentially.\n\n##### exitOnError\n\nType: `boolean`\u003cbr\u003e\nDefault: `true`\n\nSet to `false` if you don't want to stop the execution of other tasks when one or more tasks fail.\n\n##### renderer\n\nType: `string` `object`\u003cbr\u003e\nDefault: `default`\u003cbr\u003e\nOptions: `default` `verbose` `silent`\n\nRenderer that should be used. You can either pass in the name of the known renderer, or a class of a custom renderer.\n\n##### nonTTYRenderer\n\nType: `string` `object`\u003cbr\u003e\nDefault: `verbose`\n\nThe 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.\n\n### Instance\n\n#### add(task)\n\nReturns the instance.\n\n##### task\n\nType: `object` `object[]`\n\nTask object or multiple task objects.\n\n#### run([context])\n\nStart executing the tasks. Returns a `Promise` for the context object.\n\n##### context\n\nType: `object`\u003cbr\u003e\nDefault: `Object.create(null)`\n\nInitial context object.\n\n\n## Related\n\n- [ora](https://github.com/sindresorhus/ora) - Elegant terminal spinner\n- [cli-spinners](https://github.com/sindresorhus/cli-spinners) - Spinners for use in the terminal\n\n\n## License\n\nMIT © [Sam Verschueren](https://github.com/SamVerschueren)\n\n\n---\n\n\u003cdiv align=\"center\"\u003e\n\t\u003cb\u003e\n\t\t\u003ca href=\"https://tidelift.com/subscription/pkg/npm-listr?utm_source=npm-listr\u0026utm_medium=referral\u0026utm_campaign=enterprise\u0026utm_term=repo\"\u003eGet professional support for this package with a Tidelift subscription\u003c/a\u003e\n\t\u003c/b\u003e\n\t\u003cbr\u003e\n\t\u003csub\u003e\n\t\tTidelift helps make open source sustainable for maintainers while giving companies\u003cbr\u003eassurances about security, maintenance, and licensing for their dependencies.\n\t\u003c/sub\u003e\n\u003c/div\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsamverschueren%2Flistr","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsamverschueren%2Flistr","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsamverschueren%2Flistr/lists"}