npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@shoru/listrx

v2.1.4

Published

Beautiful CLI task management with dynamic subtask injection

Readme

🚀 ListrX

Beautiful CLI task management with dynamic subtask injection

Node.js Downloads npm Types

Installation · Quick Start · API · Examples


✨ Features

| | Feature | Description | |---|---|---| | 🎯 | Dynamic Subtasks | Add and nest subtasks at runtime | | 🔄 | Lifecycle Hooks | setuptaskafterEachfinally | | ✨ | 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/listrx

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

  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
  5. Open 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