JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 11
  • Score
    100M100P100Q41758F
  • License MIT

Beautiful CLI task management with dynamic subtask injection

Package Exports

  • @shoru/listrx

Readme

๐Ÿš€ ListrX

Beautiful CLI task management with dynamic subtask injection

Node.js listr2 License

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

  • Dynamic Subtask Injection โ€” Add subtasks at runtime from anywhere
  • Nested Subtasks โ€” Unlimited nesting depth with chained API
  • Shared Context โ€” All tasks share a common ctx object
  • Two Execution Modes โ€” Run main task before or after subtasks
  • Auto Behaviors โ€” Auto-complete and auto-execute timers
  • Beautiful Output โ€” Powered by listr2's polished terminal UI

๐Ÿ› ๏ธ Developer Experience

  • Minimal API โ€” Just createTask() and task.add()
  • Event Listeners โ€” React to state changes and subtask additions
  • Error Handling โ€” Built-in retry, skip, and rollback support
  • Graceful Shutdown โ€” Clean completion and force shutdown
  • TypeScript Ready โ€” Full JSDoc type annotations
  • Zero Config โ€” Sensible defaults, everything optional

๐Ÿ“ฆ Installation

# npm
npm install @shoru/listrx

# yarn
yarn add @shoru/listrx

# pnpm
pnpm add @shoru/listrx

Requirements

  • 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 files

Nested 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:

  1. All current subtasks finish executing
  2. Timer starts (e.g., 500ms)
  3. If a new subtask is added during the timer:
    • Timer is cancelled
    • New subtask executes
    • When done, timer restarts from step 2
  4. 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] โ†’ โœ“ complete

autoExecute

For mode: 'after' โ€” automatically executes the main task after no new subtasks have been added for the specified duration.

How it works:

  1. Subtasks are being added
  2. No new subtask added for the duration โ†’ timer fires
  3. Any pending subtasks execute
  4. Main task executes
  5. autoComplete timer 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 1

Error 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 workers

Cross-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.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. 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

โฌ† Back to Top