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 Downloads npm Types

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/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