Package Exports
- @shoru/listrx
Readme
๐ ListrX
Beautiful CLI task management with dynamic subtask injection
Installation ยท Quick Start ยท API ยท Examples
โจ Features
| Feature | Description | |
|---|---|---|
| ๐ฏ | Dynamic Subtasks | Add and nest subtasks at runtime |
| ๐ | Lifecycle Hooks | setup โ task โ afterEach โ finally |
| โจ | Ora-like API | Familiar succeed(), fail(), warn(), info() methods |
| ๐ก | Animated Spinners | Beautiful tree-structured output with colors |
| ๐ | Error Handling | Built-in retry, skip, and rollback support |
| ๐คซ | Console Safe | Intercepts logs without breaking the display |
| ๐งช | Test Friendly | Silent renderer for CI/testing |
๐ฆ Installation
npm install @shoru/listrxRequires Node.js 18+
๐ Quick Start
import { createTask } from '@shoru/listrx';
const task = createTask({ title: '๐ Deploy' });
task.add({ title: 'Build', task: async () => await build() });
task.add({ title: 'Test', task: async () => await test() });
task.add({ title: 'Upload', task: async () => await upload() });
await task.complete();โ ๐ Deploy
โโโ โ Build
โโโ โ Test
โโโ โ Upload๐ API Reference
Exports
import { createTask, loader } from '@shoru/listrx';loader(title?)
Simple ora-like spinner for quick operations.
const spinner = loader('Working...').start();
spinner.text = 'Still working...';
spinner.color = 'yellow';
spinner.succeed('Complete'); // โ | Also: fail(), warn(), info(), stop()createTask(config)
Full-featured task with subtask support and lifecycle hooks.
const task = createTask({
title: 'My Task', // Required
// ๐ Lifecycle hooks
setup: async (ctx, task) => {}, // Runs once, first
task: async (ctx, task, type) => {}, // Runs after setup (type: 'initial' | 'auto' | 'retry')
afterEach: async (ctx, completedSubtask, mainTask) => {}, // After each subtask
finally: async (ctx, task) => {}, // Runs last, once
// โ๏ธ Execution options
options: { concurrent: false, exitOnError: true },
// โฑ๏ธ Auto behaviors (for watch mode / streaming)
autoExecute: 500, // Run task X ms after last add() - task stays open
autoComplete: 2000, // Complete X ms after idle - task closes
// ๐ Error handling
retry: { tries: 3, delay: 1000 },
skip: (ctx) => false,
rollback: async (ctx, task) => {},
// ๐จ Display
showTimer: false,
spinnerColor: 'cyan',
rendererOptions: { renderer: 'default' } // 'default' | 'simple' | 'silent'
});๐ Lifecycle Execution Order
setup (once) โ task โ subtasks โ finally
โ โ
afterEach afterEach (per subtask)| Hook | Runs | Purpose |
|---|---|---|
setup |
Once | Initialize context, setup resources |
task |
Once (or per autoExecute) |
Main work before subtasks |
afterEach |
Per subtask | Track progress, logging |
finally |
Once | Cleanup, final message |
๐ท๏ธ Execution Type
The task function receives a third parameter type indicating how it's being executed:
task: async (ctx, task, type) => {
// type: 'initial' | 'auto' | 'retry'
}| Type | When | Description |
|---|---|---|
'initial' |
First execution | Regular call on first attempt |
'auto' |
autoExecute trigger |
Called when autoExecute timer fires |
'retry' |
Retry attempts | Called on retry after failure |
const task = createTask({
title: 'Smart Task',
retry: { tries: 3, delay: 1000 },
task: async (ctx, task, type) => {
if (type === 'retry') {
task.output = 'Retrying with fallback strategy...';
return fallbackMethod();
}
return primaryMethod();
}
});โฑ๏ธ Auto Behaviors
For watch mode or streaming scenarios where subtasks arrive over time:
| Property | Behavior |
|---|---|
autoExecute |
Triggers task (setup only once) after X ms of no new subtasks. Task stays open. |
autoComplete |
Triggers finally and closes task after X ms of complete idle. |
๐ ๏ธ Task Methods
// Add subtasks (single or batch)
const sub = task.add({ title: 'Step 1', task: async () => {} });
const [a, b] = task.add([{ title: 'A' }, { title: 'B' }]);
// Nest subtasks
const parent = task.add({ title: 'Parent' });
parent.add({ title: 'Child' });
// Control
await task.complete(); // Finish task (runs finally)
task.forceShutdown('Reason'); // Abort immediately
// Subscribe to events
task.state$((state) => {}); // 'pending' | 'processing' | 'completed' | 'failed'
task.subtasks$((subtask) => {}); // Called when subtask is added๐ Subtask Control
Inside a task function, control the subtask state:
task.add({
title: 'Check',
task: async (ctx, task, type) => {
task.title = 'Checking...'; // Update title
task.output = 'Step 1 of 3'; // Show status line
task.spinnerColor = 'yellow';
// Handle different execution types
if (type === 'retry') {
task.output = 'Retrying...';
}
// Final states (ora-like)
task.succeed('All good'); // โ green
task.fail('Error'); // โ red
task.warn('Warning'); // โ yellow
task.info('Note'); // โน blue
}
});๐ Task Properties
task.state // 'pending' | 'processing' | 'completed' | 'failed'
task.title // Task title
task.ctx // Shared context object
task.promise // Awaitable completion promise
task.subtaskCount // Total subtask count
task.isPending / isProcessing / isCompleted / isFailed๐จ Spinner Colors
type SpinnerColor =
| 'black' | 'red' | 'green' | 'yellow' | 'blue'
| 'magenta' | 'cyan' | 'white' | 'gray' | 'grey'
| 'redBright' | 'greenBright' | 'yellowBright'
| 'blueBright' | 'magentaBright' | 'cyanBright' | 'whiteBright';๐ก Examples
๐๏ธ Nested Tasks
const task = createTask({ title: '๐๏ธ Build' });
const frontend = task.add({ title: 'Frontend' });
frontend.add({ title: 'TypeScript', task: compileTs });
frontend.add({ title: 'CSS', task: bundleCss });
const backend = task.add({ title: 'Backend' });
backend.add({ title: 'Compile', task: compile });
await task.complete();โ ๐๏ธ Build
โโโ โ Frontend
โ โโโ โ TypeScript
โ โโโ โ CSS
โโโ โ Backend
โโโ โ Compile๐ Lifecycle Hooks
const task = createTask({
title: 'Pipeline',
setup: async (ctx) => {
ctx.startTime = Date.now();
ctx.completed = 0;
},
task: async (ctx, task, type) => {
task.output = 'Loading config...';
ctx.config = await loadConfig();
},
afterEach: async (ctx, completedSubtask, mainTask) => {
ctx.completed++;
mainTask.output = `Progress: ${ctx.completed}/${mainTask.childCount}`;
},
finally: async (ctx, task) => {
const duration = Date.now() - ctx.startTime;
task.succeed(`Done in ${duration}ms`);
}
});
task.add({ title: 'Fetch', task: fetchData });
task.add({ title: 'Process', task: processData });
await task.complete();๐ Watch Mode
Use autoExecute and autoComplete for file watchers or streaming data:
const task = createTask({
title: 'File Watcher',
autoExecute: 500, // Batch files, run task 500ms after last change
autoComplete: 5000, // Finish 5s after idle
setup: async (ctx) => {
ctx.batches = 0; // Runs once
},
task: async (ctx, task, type) => {
ctx.batches++; // Runs each autoExecute trigger
task.output = `Processing batch #${ctx.batches}`;
// type will be 'initial' for first batch, 'auto' for subsequent
if (type === 'auto') {
task.output = `Auto-processing batch #${ctx.batches}`;
}
},
finally: async (ctx, task) => {
task.succeed(`Processed ${ctx.batches} batches`);
}
});
watcher.on('change', (file) => {
task.add({ title: file, task: () => compile(file) });
});
await task.promise;
// Timeline example:
// 0-200ms - files added
// 700ms - autoExecute โ setup + task (type: 'initial', batch #1)
// 1000ms - more files added
// 1500ms - autoExecute โ task only (type: 'auto', batch #2)
// 6500ms - autoComplete โ finally, task closesโก Concurrent Execution
const task = createTask({
title: 'Process Images',
options: { concurrent: true }
});
images.forEach(img => {
task.add({ title: img.name, task: () => processImage(img) });
});
await task.complete();๐ Error Handling
task.add({
title: 'Upload',
task: async (ctx, task, type) => {
if (type === 'retry') {
task.output = 'Retrying with exponential backoff...';
} else {
task.output = 'Uploading...';
}
await upload();
},
retry: { tries: 3, delay: 1000 }, // Retry on failure
skip: (ctx) => ctx.offline && 'No connection', // Skip with reason
rollback: async (ctx, task) => { // Cleanup on failure
await cleanup();
}
});๐ Smart Retry Logic
const task = createTask({
title: 'API Call',
retry: { tries: 3, delay: 2000 },
task: async (ctx, task, type) => {
switch (type) {
case 'initial':
task.output = 'Attempting primary endpoint...';
return await callPrimaryAPI();
case 'retry':
task.output = 'Falling back to secondary endpoint...';
return await callSecondaryAPI();
case 'auto':
task.output = 'Auto-refresh triggered...';
return await refreshData();
}
}
});
await task.complete();โฑ๏ธ Timer Display
const task = createTask({ title: 'Build', showTimer: true });
task.add({ title: 'Compile', task: compile });
await task.complete();โ Build [2.3s]
โโโ โ Compile [2.1s]๐งช Testing (Silent Renderer)
const task = createTask({
title: 'Test',
rendererOptions: { renderer: 'silent' }
});
task.add({ title: 'Step', task: async () => results.push(1) });
await task.complete();
expect(task.state).toBe('completed');๐ฅ๏ธ Renderers
| Renderer | Output | Use Case |
|---|---|---|
'default' |
Animated spinners | Interactive terminals |
'simple' |
Plain text | CI/CD, logs |
'silent' |
None | Testing |
createTask({
rendererOptions: {
renderer: process.env.CI ? 'simple' : 'default'
}
});๐ค Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create feature branch
git checkout -b feature/amazing-feature - Commit changes
git commit -m 'Add amazing feature' - Push
git push origin feature/amazing-feature - Open Pull Request
This project is licensed under the MIT License โ see the LICENSE file for details.
Made with โค๏ธ for the Node.js CLI community