Skip to content

Mangaka-bot/ListrX

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

34 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸš€ 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

About

Beautiful CLI task management with runtime task injection

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published