Beautiful CLI task management with dynamic subtask injection
npm install @shoru/listrxsetup โ task โ afterEach โ finally |
succeed(), fail(), warn(), info() methods |
bash
npm install @shoru/listrx
`
> Requires Node.js 18+
---
๐ Quick Start
`javascript
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
$3
`javascript
import { createTask, loader } from '@shoru/listrx';
`
---
$3
Simple ora-like spinner for quick operations.
`javascript
const spinner = loader('Working...').start();
spinner.text = 'Still working...';
spinner.color = 'yellow';
spinner.succeed('Complete'); // โ | Also: fail(), warn(), info(), stop()
`
---
$3
Full-featured task with subtask support and lifecycle hooks.
`javascript
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:
`javascript
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 |
`javascript
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. |
---
$3
`javascript
// 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
`
---
$3
Inside a task function, control the subtask state:
`javascript
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
}
});
`
---
$3
`javascript
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
`
---
$3
`typescript
type SpinnerColor =
| 'black' | 'red' | 'green' | 'yellow' | 'blue'
| 'magenta' | 'cyan' | 'white' | 'gray' | 'grey'
| 'redBright' | 'greenBright' | 'yellowBright'
| 'blueBright' | 'magentaBright' | 'cyanBright' | 'whiteBright';
`
---
๐ก Examples
$3
`javascript
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
`
---
$3
`javascript
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();
`
---
$3
Use autoExecute and autoComplete for file watchers or streaming data:
`javascript
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
`
---
$3
`javascript
const task = createTask({
title: 'Process Images',
options: { concurrent: true }
});
images.forEach(img => {
task.add({ title: img.name, task: () => processImage(img) });
});
await task.complete();
`
---
$3
`javascript
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();
}
});
`
---
$3
`javascript
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();
`
---
$3
`javascript
const task = createTask({ title: 'Build', showTimer: true });
task.add({ title: 'Compile', task: compile });
await task.complete();
`
`
โ Build [2.3s]
โโโ โ Compile [2.1s]
`
---
$3
`javascript
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 |
`javascript
createTask({
rendererOptions: {
renderer: process.env.CI ? 'simple' : 'default'
}
});
`
---
๐ค Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
1. Fork the repository
2. Create feature branch git checkout -b feature/amazing-feature
3. Commit changes git commit -m 'Add amazing feature'
4. Push git push origin feature/amazing-feature`