Package Exports
- @shoru/listrx
Readme
๐ ListrX
Beautiful CLI task management with dynamic subtask injection
A simple yet powerful Node.js library for creating CLI tasks with dynamically injectable subtasks. Built on top of listr2 for beautiful terminal output.
Installation โข Quick Start โข API Reference โข Examples
โจ Features
๐ฏ Core Capabilities
|
๐ ๏ธ Developer Experience
|
๐ฆ Installation
# npm
npm install @shoru/listrx
# yarn
yarn add @shoru/listrx
# pnpm
pnpm add @shoru/listrxRequirements
- Node.js 18.0.0 or higher
- ES Modules support
๐ Quick Start
Basic Example
import { createTask } from '@shoru/listrx';
// Create a task
const task = createTask({ title: '๐ Deploy Application' });
// Add subtasks dynamically
task.add({ title: 'Build project', task: async (ctx) => await build() });
task.add({ title: 'Run tests', task: async (ctx) => await test() });
task.add({ title: 'Upload files', task: async (ctx) => await upload() });
// Execute and wait for completion
await task.complete();Terminal Output:
โ ๐ Deploy Application
โโโ โ Build project
โโโ โ Run tests
โโโ โ Upload filesNested Subtasks
const task = createTask({ title: '๐๏ธ Build Project' });
// Create parent subtask and chain children
const frontend = task.add({ title: 'Frontend' });
frontend.add({ title: 'Compile TypeScript', task: compileTs });
frontend.add({ title: 'Bundle CSS', task: bundleCss });
const backend = task.add({ title: 'Backend' });
backend.add({ title: 'Compile', task: compileBackend });
backend.add({ title: 'Generate types', task: generateTypes });
await task.complete();Terminal Output:
โ ๐๏ธ Build Project
โโโ โ Frontend
โ โโโ โ Compile TypeScript
โ โโโ โ Bundle CSS
โโโ โ Backend
โโโ โ Compile
โโโ โ Generate types๐ API Reference
createTask(config)
Creates a new task instance.
import { createTask } from '@shoru/listrx';
const task = createTask({
// Required
title: 'My Task',
// Main task executor (optional)
task: async (ctx) => {
ctx.result = await doSomething();
},
// Execution mode (optional, default: 'before')
mode: 'before', // 'before' | 'after'
// Subtask execution options (optional)
options: {
concurrent: false, // Run subtasks in parallel
exitOnError: true // Stop on first error
},
// Auto behaviors (optional)
autoComplete: 500, // Auto-complete after idle duration (ms)
autoExecute: 500, // Auto-execute main task after no new subtasks (ms)
// Error handling (optional)
retry: { tries: 3, delay: 1000 },
rollback: async (ctx, task) => await cleanup(),
skip: (ctx) => ctx.shouldSkip,
// Display options (optional)
showTimer: true,
rendererOptions: {},
// Subtask defaults (optional)
defaultSubtaskOptions: {},
batchDebounceMs: 50
});Configuration Options
| Option | Type | Default | Description |
|---|---|---|---|
title |
string |
โ | Required. Display title for the task |
task |
(ctx) => Promise<any> |
โ | Main task executor function |
mode |
'before' | 'after' |
'before' |
When main task runs relative to subtasks |
options |
object |
{} |
Subtask execution options |
options.concurrent |
boolean |
false |
Run subtasks in parallel |
options.exitOnError |
boolean |
true |
Stop execution on first error |
autoComplete |
number |
โ | Auto-complete after ms of post-execution idle |
autoExecute |
number |
โ | Auto-execute after ms of no new subtasks |
retry |
{ tries, delay? } |
โ | Retry failed task |
rollback |
(ctx, task) => Promise |
โ | Rollback function on failure |
skip |
(ctx) => boolean | string |
โ | Skip condition |
showTimer |
boolean |
false |
Show execution duration |
defaultSubtaskOptions |
object |
{} |
Default options inherited by subtasks |
rendererOptions |
object |
{} |
listr2 renderer customization |
batchDebounceMs |
number |
50 |
Debounce time for batching subtask additions |
Execution Modes
Control when the main task executor runs relative to subtasks.
| Mode | Execution Order | Use Case |
|---|---|---|
'before' |
Main task โ Subtasks | Prepare data for subtasks |
'after' |
Subtasks โ Main task | Aggregate results from subtasks |
Mode: 'before' (Default)
The main task runs first, then subtasks execute. Use this when the main task prepares context for subtasks.
const task = createTask({
title: 'Deploy',
mode: 'before',
task: async (ctx) => {
// Runs FIRST - prepare configuration
ctx.config = await loadConfig();
ctx.version = await getVersion();
}
});
// Subtasks run AFTER main task, can use ctx.config
task.add({
title: 'Upload files',
task: async (ctx) => await upload(ctx.config)
});
task.add({
title: 'Tag release',
task: async (ctx) => await tagRelease(ctx.version)
});
await task.complete();Mode: 'after'
Subtasks run first, then the main task executes. Use this when you need to aggregate or finalize results.
const task = createTask({
title: 'Generate Report',
mode: 'after',
task: async (ctx) => {
// Runs LAST - aggregate all data
await generateReport(ctx.userData, ctx.salesData);
}
});
// Subtasks run FIRST, populate ctx
task.add({
title: 'Fetch user data',
task: async (ctx) => { ctx.userData = await fetchUsers(); }
});
task.add({
title: 'Fetch sales data',
task: async (ctx) => { ctx.salesData = await fetchSales(); }
});
await task.complete();Methods
task.add(config) / task.add([configs])
Add one or multiple subtasks. Returns the created subtask(s) for chaining.
// Add single subtask
const subtask = task.add({
title: 'My Subtask',
task: async (ctx) => { /* ... */ }
});
// Add multiple subtasks
const [sub1, sub2] = task.add([
{ title: 'Subtask 1', task: task1Fn },
{ title: 'Subtask 2', task: task2Fn }
]);
// Chain nested subtasks
const parent = task.add({ title: 'Parent' });
parent.add({ title: 'Child 1', task: child1Fn });
parent.add({ title: 'Child 2', task: child2Fn });
// Deep nesting
const level1 = task.add({ title: 'Level 1' });
const level2 = level1.add({ title: 'Level 2' });
const level3 = level2.add({ title: 'Level 3', task: deepTaskFn });Subtask Config Options:
| Option | Type | Description |
|---|---|---|
title |
string |
Required. Subtask title |
task |
(ctx) => Promise |
Subtask executor |
options |
object |
{ concurrent, exitOnError } for children |
skip |
(ctx) => boolean | string |
Skip condition |
retry |
{ tries, delay? } |
Retry configuration |
rollback |
(ctx, task) => Promise |
Rollback on failure |
task.complete()
Signal that no more subtasks will be added and wait for execution to finish.
const task = createTask({ title: 'My Task' });
task.add({ title: 'Step 1', task: step1Fn });
task.add({ title: 'Step 2', task: step2Fn });
// Wait for all tasks to complete
await task.complete();
console.log('All done!');
console.log('Final state:', task.state); // 'completed'โ ๏ธ After calling
complete(), no new subtasks can be added.
task.forceShutdown(reason?)
Immediately stop execution and fail the task.
const task = createTask({ title: 'Long Running Task' });
// Handle interruption
process.on('SIGINT', () => {
task.forceShutdown('User cancelled');
process.exit(1);
});
// Or with custom reason
setTimeout(() => {
task.forceShutdown('Timeout exceeded');
}, 30000);Event Listeners
task.state$(callback)
Subscribe to state changes. Returns an unsubscribe function.
const unsubscribe = task.state$((state) => {
console.log('State changed:', state);
// 'pending' โ 'processing' โ 'completed' | 'failed'
});
// Later, stop listening
unsubscribe();States:
| State | Description |
|---|---|
'pending' |
Task created, waiting for subtasks or execution |
'processing' |
Currently executing tasks |
'completed' |
All tasks finished successfully |
'failed' |
Execution failed or was force shutdown |
task.subtasks$(callback)
Subscribe to subtask additions. Returns an unsubscribe function.
const unsubscribe = task.subtasks$((subtask) => {
console.log('Subtask added:', subtask.title);
});
task.add({ title: 'Step 1', task: step1Fn });
// Console: "Subtask added: Step 1"
// Stop listening
unsubscribe();Properties
| Property | Type | Description |
|---|---|---|
task.state |
string |
Current state |
task.title |
string |
Task title |
task.mode |
string |
Execution mode |
task.task |
function |
Main task executor |
task.ctx |
object |
Shared context object |
task.promise |
Promise |
Completion promise |
task.subtaskCount |
number |
Total subtask count |
task.pendingSubtaskCount |
number |
Pending subtasks |
task.isPending |
boolean |
Is in pending state |
task.isProcessing |
boolean |
Is processing |
task.isCompleted |
boolean |
Is completed |
task.isFailed |
boolean |
Is failed |
Shared Context (ctx)
All task and subtask functions share the same ctx object, enabling data passing between them.
const task = createTask({
title: 'Data Pipeline',
mode: 'before',
task: async (ctx) => {
// Initialize context
ctx.startTime = Date.now();
ctx.items = [];
}
});
task.add({
title: 'Fetch data',
task: async (ctx) => {
ctx.rawData = await fetchData();
}
});
task.add({
title: 'Process data',
task: async (ctx) => {
ctx.items = ctx.rawData.map(transform);
}
});
task.add({
title: 'Save results',
task: async (ctx) => {
await saveItems(ctx.items);
ctx.duration = Date.now() - ctx.startTime;
}
});
await task.complete();
console.log(`Processed ${task.ctx.items.length} items in ${task.ctx.duration}ms`);Auto Behaviors
ListrX provides two auto behavior timers for hands-free task management.
autoComplete
Automatically completes the task after all subtasks finish and no new subtasks are added for the specified duration.
How it works:
- All current subtasks finish executing
- Timer starts (e.g., 500ms)
- If a new subtask is added during the timer:
- Timer is cancelled
- New subtask executes
- When done, timer restarts from step 2
- If timer expires with no new subtasks โ task completes
const task = createTask({
title: 'File Processor',
autoComplete: 1000 // Complete 1s after idle
});
// Initial subtasks
task.add({ title: 'Process file 1', task: processFile1 });
task.add({ title: 'Process file 2', task: processFile2 });
// Files finish โ timer starts (1000ms)
// If a new file arrives during the wait:
setTimeout(() => {
task.add({ title: 'Process file 3', task: processFile3 });
// Timer resets after file 3 completes
}, 500);
// Wait for auto-completion
await task.promise;Timeline visualization:
add(A) โ add(B) โ [A done] โ [B done] โ [timer: 1000ms] โ โ complete
โ
no new subtasks
add(A) โ [A done] โ [timer starts] โ add(B) โ [timer cancelled]
โ
[B done] โ [timer restarts] โ โ completeautoExecute
For mode: 'after' โ automatically executes the main task after no new subtasks have been added for the specified duration.
How it works:
- Subtasks are being added
- No new subtask added for the duration โ timer fires
- Any pending subtasks execute
- Main task executes
autoCompletetimer can then start (if configured)
const task = createTask({
title: 'Batch Processor',
mode: 'after',
autoExecute: 500, // Execute 500ms after last subtask added
task: async (ctx) => {
console.log(`Processing ${ctx.items.length} items`);
await generateSummary(ctx.items);
}
});
// Initialize context
task.ctx.items = [];
// Rapidly add subtasks
task.add({
title: 'Item 1',
task: async (ctx) => { ctx.items.push(await fetchItem(1)); }
});
task.add({
title: 'Item 2',
task: async (ctx) => { ctx.items.push(await fetchItem(2)); }
});
// 500ms after last add() with no new subtasks:
// 1. Subtasks execute
// 2. Main task executes
await task.promise;Combining Both
Use both timers for complete hands-free operation:
const task = createTask({
title: 'Watch & Build',
mode: 'after',
autoExecute: 500, // Run main task 500ms after last subtask added
autoComplete: 2000, // Complete 2s after everything finishes
task: async (ctx) => {
console.log(`Building ${ctx.files.length} files...`);
await buildProject(ctx.files);
}
});
task.ctx.files = [];
// File watcher integration
watcher.on('change', (file) => {
task.add({
title: `Compile ${file}`,
task: async (ctx) => {
ctx.files.push(file);
await compile(file);
}
});
});
// Flow:
// 1. Files change โ subtasks added
// 2. 500ms of no new files โ subtasks run โ main task runs
// 3. 2000ms of idle after main task โ auto-complete
// 4. If new file during step 3 โ restart from step 1Error Handling
Retry
Automatically retry failed subtasks:
task.add({
title: 'Flaky API call',
task: async (ctx) => await callFlakyApi(),
retry: {
tries: 3, // Retry up to 3 times
delay: 1000 // Wait 1s between retries
}
});Skip
Conditionally skip subtasks:
task.add({
title: 'Optional step',
task: async (ctx) => await optionalWork(),
skip: (ctx) => {
if (ctx.skipOptional) {
return 'Skipped by user request'; // Custom skip message
}
return false; // Don't skip
}
});Rollback
Execute cleanup on failure:
task.add({
title: 'Database migration',
task: async (ctx) => {
ctx.migrationId = await startMigration();
await runMigration();
},
rollback: async (ctx, task) => {
task.output = 'Rolling back migration...';
await rollbackMigration(ctx.migrationId);
}
});๐ Examples
Basic Sequential Tasks
import { createTask } from '@shoru/listrx';
async function deploy() {
const task = createTask({ title: '๐ Deploy Application' });
task.add({
title: 'Install dependencies',
task: async () => await exec('npm ci')
});
task.add({
title: 'Run tests',
task: async (ctx) => {
const result = await exec('npm test');
ctx.testsPassed = result.exitCode === 0;
}
});
task.add({
title: 'Build',
task: async () => await exec('npm run build'),
skip: (ctx) => !ctx.testsPassed && 'Tests failed'
});
task.add({
title: 'Deploy',
task: async () => await exec('npm run deploy'),
skip: (ctx) => !ctx.testsPassed && 'Tests failed'
});
await task.complete();
}Concurrent Subtasks
const task = createTask({
title: '๐ผ๏ธ Process Images',
options: { concurrent: true } // All subtasks run in parallel
});
const images = await getImages();
for (const image of images) {
task.add({
title: `Process ${image.name}`,
task: async () => {
await resize(image);
await optimize(image);
await upload(image);
}
});
}
await task.complete();Deep Nesting
const task = createTask({ title: '๐ข Enterprise Build' });
// Frontend
const frontend = task.add({ title: 'Frontend' });
const react = frontend.add({ title: 'React App' });
react.add({ title: 'Install deps', task: installReactDeps });
react.add({ title: 'Build', task: buildReact });
react.add({ title: 'Test', task: testReact });
const styles = frontend.add({ title: 'Styles' });
styles.add({ title: 'Compile SCSS', task: compileScss });
styles.add({ title: 'PostCSS', task: runPostcss });
// Backend
const backend = task.add({ title: 'Backend' });
const api = backend.add({ title: 'API Server' });
api.add({ title: 'Compile TypeScript', task: compileTs });
api.add({ title: 'Generate OpenAPI', task: generateOpenApi });
const workers = backend.add({ title: 'Workers' });
workers.add({ title: 'Build workers', task: buildWorkers });
await task.complete();Output:
โ ๐ข Enterprise Build
โโโ โ Frontend
โ โโโ โ React App
โ โ โโโ โ Install deps
โ โ โโโ โ Build
โ โ โโโ โ Test
โ โโโ โ Styles
โ โโโ โ Compile SCSS
โ โโโ โ PostCSS
โโโ โ Backend
โโโ โ API Server
โ โโโ โ Compile TypeScript
โ โโโ โ Generate OpenAPI
โโโ โ Workers
โโโ โ Build workersCross-Module Injection
Share a task across modules for decentralized subtask registration.
main.js:
import { createTask } from '@shoru/listrx';
import { registerAuthTasks } from './modules/auth.js';
import { registerDbTasks } from './modules/database.js';
import { registerCacheTasks } from './modules/cache.js';
// Create shared task with auto-complete
export const initTask = createTask({
title: '๐ Initialize Application',
autoComplete: 500 // Complete 500ms after all modules done
});
// Each module registers its subtasks
registerAuthTasks(initTask);
registerDbTasks(initTask);
registerCacheTasks(initTask);
// Wait for completion
await initTask.promise;
console.log('Application initialized!');modules/auth.js:
export function registerAuthTasks(task) {
const auth = task.add({ title: '๐ Auth Module' });
auth.add({
title: 'Load JWT keys',
task: async (ctx) => {
ctx.jwtKeys = await loadJwtKeys();
}
});
auth.add({
title: 'Initialize OAuth providers',
task: async () => await initOAuth()
});
}modules/database.js:
export function registerDbTasks(task) {
const db = task.add({ title: '๐๏ธ Database Module' });
db.add({
title: 'Connect to database',
task: async (ctx) => {
ctx.db = await connectDatabase();
},
retry: { tries: 3, delay: 1000 }
});
db.add({
title: 'Run migrations',
task: async (ctx) => await runMigrations(ctx.db)
});
}modules/cache.js:
export function registerCacheTasks(task) {
const cache = task.add({ title: '๐ฆ Cache Module' });
cache.add({
title: 'Connect to Redis',
task: async (ctx) => {
ctx.redis = await connectRedis();
}
});
cache.add({
title: 'Warm cache',
task: async (ctx) => await warmCache(ctx.redis, ctx.db)
});
}Main Task with Preparation (Mode: before)
const task = createTask({
title: '๐ Data Export',
mode: 'before',
task: async (ctx) => {
// Runs FIRST - setup
ctx.exportId = generateExportId();
ctx.outputDir = await createTempDir();
ctx.files = [];
console.log(`Starting export ${ctx.exportId}`);
}
});
// These run AFTER setup
task.add({
title: 'Export users',
task: async (ctx) => {
const file = await exportUsers(ctx.outputDir);
ctx.files.push(file);
}
});
task.add({
title: 'Export orders',
task: async (ctx) => {
const file = await exportOrders(ctx.outputDir);
ctx.files.push(file);
}
});
task.add({
title: 'Create archive',
task: async (ctx) => {
ctx.archive = await createZip(ctx.files);
}
});
await task.complete();
console.log(`Export complete: ${task.ctx.archive}`);Aggregation Pattern (Mode: after)
const task = createTask({
title: '๐ Generate Analytics Report',
mode: 'after',
task: async (ctx) => {
// Runs LAST - aggregate all data
const report = {
users: ctx.userStats,
sales: ctx.salesStats,
traffic: ctx.trafficStats,
generatedAt: new Date()
};
await saveReport(report);
await emailReport(report);
console.log('Report generated and sent!');
}
});
// These run FIRST - gather data in parallel
task.add({
title: 'Fetch user statistics',
task: async (ctx) => {
ctx.userStats = await fetchUserStats();
}
});
task.add({
title: 'Fetch sales statistics',
task: async (ctx) => {
ctx.salesStats = await fetchSalesStats();
}
});
task.add({
title: 'Fetch traffic statistics',
task: async (ctx) => {
ctx.trafficStats = await fetchTrafficStats();
}
});
await task.complete();File Watcher with Auto Behaviors
import { createTask } from '@shoru/listrx';
import { watch } from 'chokidar';
const task = createTask({
title: '๐๏ธ Watch & Build',
mode: 'after',
autoExecute: 300, // Build 300ms after last file change
autoComplete: 5000, // Complete after 5s of total inactivity
options: { concurrent: true },
task: async (ctx) => {
console.log(`\n๐ฆ Building ${ctx.changedFiles.length} files...`);
await runBuild(ctx.changedFiles);
ctx.changedFiles = []; // Reset for next batch
}
});
// Initialize
task.ctx.changedFiles = [];
// Watch for changes
const watcher = watch('./src/**/*.{ts,tsx}', {
ignoreInitial: true
});
watcher.on('change', (filePath) => {
task.add({
title: `Detected: ${filePath}`,
task: async (ctx) => {
ctx.changedFiles.push(filePath);
}
});
});
watcher.on('add', (filePath) => {
task.add({
title: `New file: ${filePath}`,
task: async (ctx) => {
ctx.changedFiles.push(filePath);
}
});
});
// Handle shutdown
process.on('SIGINT', async () => {
console.log('\nShutting down...');
await watcher.close();
task.forceShutdown('User interrupted');
process.exit(0);
});
// Keep running until auto-complete or shutdown
await task.promise;Database Migration with Rollback
const task = createTask({
title: '๐๏ธ Database Migration',
mode: 'before',
task: async (ctx) => {
ctx.migrationLog = [];
ctx.backupId = await createBackup();
console.log(`Backup created: ${ctx.backupId}`);
}
});
task.add({
title: 'Add users table',
task: async (ctx) => {
await db.query('CREATE TABLE users (...)');
ctx.migrationLog.push('users');
},
rollback: async (ctx) => {
await db.query('DROP TABLE IF EXISTS users');
}
});
task.add({
title: 'Add posts table',
task: async (ctx) => {
await db.query('CREATE TABLE posts (...)');
ctx.migrationLog.push('posts');
},
rollback: async (ctx) => {
await db.query('DROP TABLE IF EXISTS posts');
}
});
task.add({
title: 'Add indexes',
task: async (ctx) => {
await db.query('CREATE INDEX ...');
ctx.migrationLog.push('indexes');
},
retry: { tries: 2, delay: 500 }
});
task.add({
title: 'Seed data',
task: async (ctx) => {
await seedDatabase();
},
skip: (ctx) => process.env.SKIP_SEED === 'true' && 'Seeding disabled'
});
try {
await task.complete();
console.log('Migration completed:', task.ctx.migrationLog);
} catch (error) {
console.error('Migration failed, backup available:', task.ctx.backupId);
}State Monitoring & Logging
const task = createTask({
title: 'Long Running Process',
showTimer: true
});
// Log state changes
task.state$((state) => {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] State: ${state}`);
});
// Log subtask additions
task.subtasks$((subtask) => {
console.log(`Subtask registered: ${subtask.title}`);
});
// Add subtasks
task.add({ title: 'Step 1', task: step1 });
task.add({ title: 'Step 2', task: step2 });
task.add({ title: 'Step 3', task: step3 });
await task.complete();
console.log('Final stats:');
console.log(` Total subtasks: ${task.subtaskCount}`);
console.log(` Final state: ${task.state}`);CI/CD Pipeline
import { createTask } from '@shoru/listrx';
async function runPipeline() {
const task = createTask({
title: '๐ CI/CD Pipeline',
showTimer: true,
options: { exitOnError: true }
});
// Setup
const setup = task.add({ title: '๐ฆ Setup' });
setup.add({ title: 'Checkout code', task: checkout });
setup.add({ title: 'Install dependencies', task: npmInstall });
setup.add({ title: 'Setup environment', task: setupEnv });
// Quality
const quality = task.add({
title: '๐ Quality Checks',
options: { concurrent: true } // Run checks in parallel
});
quality.add({ title: 'Lint', task: runLint });
quality.add({ title: 'Type check', task: runTypeCheck });
quality.add({ title: 'Security audit', task: runAudit });
// Test
const test = task.add({ title: '๐งช Tests' });
test.add({
title: 'Unit tests',
task: runUnitTests,
options: { concurrent: true }
});
test.add({
title: 'Integration tests',
task: runIntegrationTests
});
test.add({
title: 'E2E tests',
task: runE2ETests,
retry: { tries: 2, delay: 5000 }
});
// Build
const build = task.add({
title: '๐จ Build',
options: { concurrent: true }
});
build.add({ title: 'Build frontend', task: buildFrontend });
build.add({ title: 'Build backend', task: buildBackend });
// Deploy
const deploy = task.add({ title: '๐ Deploy' });
deploy.add({
title: 'Deploy to staging',
task: deployStaging
});
deploy.add({
title: 'Run smoke tests',
task: runSmokeTests,
retry: { tries: 3, delay: 2000 }
});
deploy.add({
title: 'Deploy to production',
task: deployProd,
rollback: rollbackProd
});
try {
await task.complete();
console.log('\nโ
Pipeline completed successfully!');
} catch (error) {
console.error('\nโ Pipeline failed:', error.message);
process.exit(1);
}
}
runPipeline();Output:
โ ๐ CI/CD Pipeline [2m 34s]
โโโ โ ๐ฆ Setup [15s]
โ โโโ โ Checkout code [2s]
โ โโโ โ Install dependencies [12s]
โ โโโ โ Setup environment [1s]
โโโ โ ๐ Quality Checks [8s]
โ โโโ โ Lint [6s]
โ โโโ โ Type check [8s]
โ โโโ โ Security audit [3s]
โโโ โ ๐งช Tests [45s]
โ โโโ โ Unit tests [12s]
โ โโโ โ Integration tests [18s]
โ โโโ โ E2E tests [15s]
โโโ โ ๐จ Build [20s]
โ โโโ โ Build frontend [18s]
โ โโโ โ Build backend [20s]
โโโ โ ๐ Deploy [1m 6s]
โโโ โ Deploy to staging [25s]
โโโ โ Run smoke tests [11s]
โโโ โ Deploy to production [30s]
โ
Pipeline completed successfully!๐ State Machine
โโโโโโโโโโโโโโโโโโโ
โ โ
โ PENDING โ โ Task created
โ โ
โโโโโโโโโโฌโโโโโโโโโ
โ
โ add() / complete() / autoExecute
โผ
โโโโโโโโโโโโโโโโโโโ
โโโโโโบโ โโโโโโโ
โ โ PROCESSING โ โ new subtasks added
โ โ โ โ while processing
โ โโโโโโโโโโฌโโโโโโโโโโโโโโโ
โ โ
โ โ all done
โ โผ
โ โโโโโโโโโโโโโโโโโโโ
โ โ โ
โโโโโโโ (idle period) โ autoComplete timer
โ โ
โโโโโโโโโโฌโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโ
โ โ
โผ โผ
โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ
โ โ โ โ
โ COMPLETED โ โ FAILED โ
โ โ โ โ
โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ
success error / forceShutdown๐งช Testing
Use renderer: 'silent' to disable terminal output during tests.
import { createTask } from '@shoru/listrx';
import { describe, it, expect, beforeEach } from 'vitest';
describe('Task Processing', () => {
it('should execute subtasks in order', async () => {
const results = [];
const task = createTask({
title: 'Test Task',
rendererOptions: { renderer: 'silent' }
});
task.add({
title: 'Step 1',
task: async () => { results.push(1); }
});
task.add({
title: 'Step 2',
task: async () => { results.push(2); }
});
task.add({
title: 'Step 3',
task: async () => { results.push(3); }
});
await task.complete();
expect(results).toEqual([1, 2, 3]);
expect(task.state).toBe('completed');
expect(task.subtaskCount).toBe(3);
});
it('should share context between tasks', async () => {
const task = createTask({
title: 'Context Test',
mode: 'before',
rendererOptions: { renderer: 'silent' },
task: async (ctx) => {
ctx.initialized = true;
}
});
task.add({
title: 'Check context',
task: async (ctx) => {
expect(ctx.initialized).toBe(true);
ctx.value = 42;
}
});
await task.complete();
expect(task.ctx.initialized).toBe(true);
expect(task.ctx.value).toBe(42);
});
it('should handle nested subtasks', async () => {
const results = [];
const task = createTask({
title: 'Nested Test',
rendererOptions: { renderer: 'silent' }
});
const parent = task.add({ title: 'Parent' });
parent.add({
title: 'Child 1',
task: async () => { results.push('child1'); }
});
parent.add({
title: 'Child 2',
task: async () => { results.push('child2'); }
});
await task.complete();
expect(results).toEqual(['child1', 'child2']);
});
it('should respect skip conditions', async () => {
const results = [];
const task = createTask({
title: 'Skip Test',
rendererOptions: { renderer: 'silent' }
});
task.ctx.shouldSkip = true;
task.add({
title: 'Skipped',
task: async () => { results.push('skipped'); },
skip: (ctx) => ctx.shouldSkip
});
task.add({
title: 'Executed',
task: async () => { results.push('executed'); }
});
await task.complete();
expect(results).toEqual(['executed']);
});
});๐ง Advanced Configuration
Custom Renderer
import { createTask } from '@shoru/listrx';
// Use simple renderer for CI environments
const task = createTask({
title: 'CI Build',
rendererOptions: {
renderer: process.env.CI ? 'simple' : 'default'
}
});Default Subtask Options
// All subtasks inherit these options
const task = createTask({
title: 'Parallel Processing',
defaultSubtaskOptions: {
concurrent: true,
exitOnError: false
}
});
// Subtasks run concurrently by default
task.add({ title: 'Task 1', task: task1 });
task.add({ title: 'Task 2', task: task2 });
task.add({ title: 'Task 3', task: task3 });Batch Debouncing
// Batch rapid additions together
const task = createTask({
title: 'Batch Demo',
batchDebounceMs: 100 // Wait 100ms before processing batch
});
// These will be batched together
for (let i = 0; i < 100; i++) {
task.add({ title: `Item ${i}`, task: processItem });
}๐ค Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
๐ License
This project is licensed under the MIT License โ see the LICENSE file for details.
Made with โค๏ธ for the Node.js CLI community