Part of Tuulbelt — A collection of zero-dependency tools.
Concurrent-safe progress reporting for CLI tools with customizable templates and fluent API.
🚀 Major Feature Release:
- 🌊 Streaming API — Native async generator support with
ProgressStreamandProgressTransformfor Node.js streams - 🎯 Nested Command Structure — More intuitive CLI:
prog <id> <action>instead ofprog <action> --id <id> - 📋 Formal Specification — Complete SPEC.md documenting all behavior, algorithms, and invariants
- 📚 Advanced Examples — 4 comprehensive examples: concurrent downloads, build pipeline, streaming data, multi-service deployment
- ⚡ Performance Benchmarks — Statistical benchmarking with tatami-ng (criterion-equivalent rigor)
- 🛡️ Buffer Overflow Protection — List command limits output to prevent ENOBUFS errors
- 🧹 Simplified Codebase — Removed 195 lines of unnecessary backward compatibility code
Breaking Change: CLI syntax has changed to a more intuitive nested structure. Update your scripts:
# Old (no longer supported)
prog init --total 100 --id myproject
# New
prog myproject init 100Test Coverage: Expanded from 239 to 264 tests (10.5% increase) with zero flaky tests.
🎉 Major enhancements:
- ✨ Fluent Builder API — Clean, chainable method syntax with
createProgress() - 🎨 Template System — 7 built-in templates + custom template support
- 🔄 MultiProgress — Track multiple progress states simultaneously
- ⏱️ ETA Calculation — Automatic time-remaining estimation
- 🎭 Spinner Animations — 5 built-in spinner sets (dots, line, arrows, box, clock)
- 📊 Progress Bars — Customizable Unicode progress bars
- 📦 Object-Oriented API — Modern class-based ProgressTracker
- 🔙 Backward Compatible — Original functional API still works
Test Coverage: Expanded from 111 to 239 tests (116% increase) with zero flaky tests.
When building CLI tools that process multiple items, you need a way to:
- Track progress across different processes or scripts
- Report progress updates safely from concurrent tasks
- Persist progress state that survives process crashes
- Query progress from separate monitoring scripts
Existing solutions require complex state management or don't handle concurrency well.
- Zero runtime dependencies — Uses only Node.js built-in modules
- Concurrent-safe — Atomic file writes prevent corruption
- Persistent — Progress survives process crashes and restarts
- Multi-tracker — Track multiple independent progress states simultaneously
- Fluent API — Modern builder pattern with method chaining
- Customizable templates — 7 built-in formats + custom template support
- Multiple APIs — Functional, object-oriented, and CLI interfaces
- TypeScript — Full type safety with strict mode
Clone the repository:
git clone https://github.com/tuulbelt/cli-progress-reporting.git
cd cli-progress-reporting
npm install # Install dev dependencies onlyCLI names - both short and long forms work:
- Short (recommended):
prog - Long:
cli-progress-reporting
Recommended setup - install globally for easy access:
npm link # Enable the 'prog' command globally
prog --helpNo runtime dependencies — this tool uses only Node.js standard library.
The easiest way to use the library is with the fluent Builder API:
import{createProgress}from'./src/index.js';// Create and configure a progress trackerconstprogress=createProgress().total(100).message('Processing files').build();// Update progressfor(leti=0;i<100;i++){progress.increment(1,`Processing file ${i+1}`);}// Mark as finishedprogress.finish('All files processed!');For more control, use the ProgressTracker class directly:
import{ProgressTracker}from'./src/index.js';consttracker=newProgressTracker({id: 'my-task'});// InitializeconstinitResult=tracker.init(100,'Processing files');if(!initResult.ok){console.error(initResult.error);process.exit(1);}// Increment progressfor(leti=0;i<100;i++){tracker.increment(1,`Processing file ${i+1}`);}// Get current stateconststate=tracker.get();if(state.ok){console.log(`Progress: ${state.value.percentage}%`);}// Mark as finishedtracker.finish('All files processed!');Track multiple progress states simultaneously:
import{MultiProgress}from'./src/index.js';constmulti=newMultiProgress();// Create multiple trackersconstdownloads=multi.create('downloads',50,'Downloading files');constuploads=multi.create('uploads',30,'Uploading results');// Update them independentlydownloads.increment(5);uploads.increment(3);// Get all statesconstallStates=multi.getAll();if(allStates.ok){for(const[id,state]ofObject.entries(allStates.value)){console.log(`${id}: ${state.percentage}%`);}}// Finish specific trackersdownloads.finish('Downloads complete!');uploads.finish('Uploads complete!');// Clear allmulti.clearAll();Track progress while processing async iterables or Node.js streams:
ProgressStream (Async Generator):
import{ProgressStream}from'./src/index.js';// Create a progress-tracked async generatorconststream=newProgressStream({total: 100,message: 'Processing items',id: 'stream-task',incrementAmount: 1,});// Iterate with automatic progress trackingforawait(constitemofstream){// Process each item (0-99)awaitprocessItem(item);// Progress auto-increments after each iteration}// Stream automatically marks complete when doneProgressTransform (Node.js Streams):
import{ProgressTransform}from'./src/index.js';import{createReadStream,createWriteStream}from'node:fs';import{pipeline}from'node:stream/promises';// Create a transform stream with progress trackingconstprogressTransform=newProgressTransform({total: fileSize,message: 'Copying file',id: 'file-copy',updateInterval: 100,// Update every 100 bytes});// Use in pipelineawaitpipeline(createReadStream('input.dat'),progressTransform,createWriteStream('output.dat'));console.log('File copied:',progressTransform.getProgress());Streaming with attachProgress helper:
import{attachProgress}from'./src/index.js';import{createReadStream}from'node:fs';// Attach progress tracking to any readable streamconstfileStream=createReadStream('large-file.bin');constprogressStream=attachProgress(fileStream,{total: fileSize,message: 'Reading file',id: 'file-read',});// Monitor progress while streamingprogressStream.on('data',(chunk)=>{// Process data});progressStream.on('end',()=>{constprogress=progressStream.getProgress();console.log(`Read complete: ${progress.percentage}%`);});Customize output format with templates:
import{ProgressTracker,TemplateEngine,templates,spinners}from'./src/index.js';consttracker=newProgressTracker({id: 'my-task'});constengine=newTemplateEngine({spinnerFrames: spinners.dots,barWidth: 30,});tracker.init(100,'Processing');// Use built-in templatesconststate=tracker.get();if(state.ok){console.log(engine.render(templates.bar,state.value));// [███████░░░░░░░░░░░░░░░░░░░░░░░] 25%console.log(engine.render(templates.spinner,state.value));// ⠋ Processingconsole.log(engine.render(templates.full,state.value));// [███████░░░░░░░░░░░░░░░░░░░░░░░] 25% - Processing (5s elapsed)}// Or create custom templatesconstcustomTemplate='{{spinner}} [{{percentage}}%] {{current}}/{{total}} - {{message}}';console.log(engine.render(customTemplate,state.value));// ⠙ [25%] 25/100 - Processing// Function-based templates for full controlconstadvancedTemplate=(vars)=>{consteta=vars.eta>0 ? ` (ETA: ${vars.eta}s)` : '';return`${vars.bar}${vars.percentage}% - ${vars.message}${eta}`;};console.log(engine.render(advancedTemplate,state.value));The original functional API is still supported for backward compatibility:
import{init,increment,get,finish,formatProgress}from'./src/index.js';constconfig={id: 'my-task'};// Initialize progressconstinitResult=init(100,'Processing files',config);if(initResult.ok){console.log(formatProgress(initResult.value));// [0%] 0/100 - Processing files (0s)}// Increment progressfor(leti=0;i<100;i++){increment(1,`Processing file ${i+1}`,config);}// Get current stateconststate=get(config);if(state.ok){console.log(state.value.percentage);// 100}// Mark as finishedfinish('All files processed!',config);The CLI uses a nested command structure where the tracker ID comes first:
Single Progress Tracker:
# Initialize progress (tracker-id first, then action)
prog myproject init 100 --message "Processing files"# Increment progress
prog myproject inc 5 --message "Processing item 5"# Set absolute progress
prog myproject set 75 --message "Almost done"# Get current state (returns JSON)
prog myproject get
# Mark as finished
prog myproject done"Complete!"# Clear progress file
prog myproject clearMulti-Progress Tracking:
# Initialize multi-progress container
prog multi builds init
# Add individual trackers
prog multi builds add frontend 50 --message "Building frontend"
prog multi builds add backend 30 --message "Building backend"# Check status of all trackers
prog multi builds status
# Mark all as done
prog multi builds done# Clear all
prog multi builds clearGlobal Commands:
# List all active trackers
prog list
# Show version
prog version
# Show help
prog help
prog help init # Help for specific command#!/bin/bash
TASK_ID="my-batch-job"
TOTAL_FILES=$(ls data/*.csv | wc -l)# Initialize (new syntax: ID first, then action)
prog "$TASK_ID" init "$TOTAL_FILES" --message "Processing CSV files"# Process filesforfilein data/*.csv;do
process_file "$file"
prog "$TASK_ID" inc 1 --message "Processed $(basename $file)"done# Finish
prog "$TASK_ID"done"All files processed"# Clear when done
prog "$TASK_ID" clearMulti-progress example:
#!/bin/bash# Initialize multi-progress for parallel tasks
prog multi deployment init
# Start multiple sub-tasks
prog multi deployment add database 5 --message "Migrating database"
prog multi deployment add assets 20 --message "Compiling assets"
prog multi deployment add services 10 --message "Deploying services"# Update individual trackers as tasks progressforiin {1..5};do
migrate_database "$i"
prog multi deployment add database "$i"done# Check overall status
prog multi deployment status
# Clean up
prog multi deployment clearCreate a new progress tracker using the fluent Builder API.
Returns: ProgressBuilder instance for method chaining
Example:
constprogress=createProgress().id('my-task').total(100).message('Processing').build();Fluent API for configuring progress trackers.
Methods:
id(id: string): ProgressBuilder— Set tracker IDtotal(total: number): ProgressBuilder— Set total unitsmessage(message: string): ProgressBuilder— Set initial messagefilePath(path: string): ProgressBuilder— Set custom file pathbuild(): ProgressTracker— Build and return configured tracker
Example:
constprogress=createProgress().id('downloads').total(50).message('Downloading files').build();progress.increment(5);Object-oriented API for managing a single progress tracker.
Constructor:
newProgressTracker(config?: ProgressConfig)Methods:
init(total: number, message: string): Result<ProgressState>— Initialize progressincrement(amount?: number, message?: string): Result<ProgressState>— Increment by amount (default 1)set(current: number, message?: string): Result<ProgressState>— Set absolute progressfinish(message?: string): Result<ProgressState>— Mark as completeget(): Result<ProgressState>— Get current stateclear(): Result<void>— Remove progress file
Example:
consttracker=newProgressTracker({id: 'uploads'});tracker.init(100,'Uploading files');tracker.increment(10,'Uploaded batch 1');tracker.finish('All files uploaded');Manage multiple progress trackers simultaneously.
Constructor:
newMultiProgress()Methods:
create(id: string, total: number, message: string): ProgressTracker— Create new trackerget(id: string): ProgressTracker | undefined— Get existing trackergetAll(): Result<Record<string, ProgressState>>— Get all tracker statesclearAll(): Result<void>— Clear all trackershas(id: string): boolean— Check if tracker exists
Example:
constmulti=newMultiProgress();constdownloads=multi.create('downloads',50,'Downloading');constuploads=multi.create('uploads',30,'Uploading');downloads.increment(5);uploads.increment(3);constallStates=multi.getAll();Render progress state with customizable templates.
Constructor:
newTemplateEngine(options?: {spinnerFrames?: readonlystring[];barWidth?: number;})Methods:
render(template: Template, state: ProgressState): string— Render template with stateresetSpinner(): void— Reset spinner to first framesetSpinnerFrames(frames: readonly string[]): void— Change spinner framessetBarWidth(width: number): void— Change progress bar width
Template Variables:
{{percentage}}— Percentage complete (0-100){{current}}— Current value{{total}}— Total value{{message}}— User message{{elapsed}}— Elapsed seconds{{spinner}}— Animated spinner character{{bar}}— Progress bar string{{eta}}— Estimated time remaining (seconds)
Built-in Templates:
templates.bar—"{{bar}} {{percentage}}%"templates.spinner—"{{spinner}} {{message}}"templates.percentage—"{{percentage}}%"templates.detailed—"[{{percentage}}%] {{current}}/{{total}} - {{message}} ({{elapsed}}s)"templates.minimal—"{{message}} {{percentage}}%"templates.full—"{{bar}} {{percentage}}% - {{message}} ({{elapsed}}s elapsed{{eta}})"templates.spinnerProgress—"{{spinner}} [{{percentage}}%] {{message}}"
Built-in Spinners:
spinners.dots—['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']spinners.line—['|', '/', '-', '\\']spinners.arrows—['←', '↖', '↑', '↗', '→', '↘', '↓', '↙']spinners.box—['◰', '◳', '◲', '◱']spinners.clock—['🕐', '🕑', '🕒', '🕓', '🕔', '🕕', '🕖', '🕗', '🕘', '🕙', '🕚', '🕛']
Example:
import{TemplateEngine,templates,spinners}from'./src/index.js';constengine=newTemplateEngine({spinnerFrames: spinners.dots,barWidth: 20,});conststate=tracker.get();if(state.ok){console.log(engine.render(templates.full,state.value));// [████████░░░░░░░░░░░░] 40% - Processing (5s elapsed)}// Custom templateconstcustom='{{spinner}} {{percentage}}% complete';console.log(engine.render(custom,state.value));// ⠋ 40% completeInitialize progress tracking.
Parameters:
total— Total units of work (must be > 0)message— Initial progress messageconfig— Optional configurationid— Unique tracker ID (default: 'default')filePath— Custom file path (default: temp directory)
Returns: Result with initialized state or error
Example:
constresult=init(100,'Processing items',{id: 'my-task'});Increment progress by a specified amount.
Parameters:
amount— Amount to increment (default: 1, must be ≥ 0)message— Optional new messageconfig— Configuration object
Returns: Result with updated state or error
Example:
constresult=increment(5,'Processed 5 items');Set progress to an absolute value.
Parameters:
current— Current progress value (must be ≥ 0)message— Optional new messageconfig— Configuration object
Returns: Result with updated state or error
Example:
constresult=set(75,'Almost done');Mark progress as complete.
Parameters:
message— Optional completion messageconfig— Configuration object
Returns: Result with final state or error
Example:
constresult=finish('All tasks complete!');Get current progress state.
Parameters:
config— Configuration object
Returns: Result with current state or error
Example:
constresult=get({id: 'my-task'});if(result.ok){console.log(`Progress: ${result.value.percentage}%`);}Remove progress file.
Parameters:
config— Configuration object
Returns: Result indicating success or error
Example:
constresult=clear({id: 'my-task'});Format progress state as a human-readable string.
Parameters:
state— Progress state to format
Returns: Formatted string like [50%] 50/100 - Processing (5s)
Example:
conststate=get();if(state.ok){console.log(formatProgress(state.value));}Progress state is stored as JSON:
interfaceProgressState{total: number;// Total units of workcurrent: number;// Current units completedmessage: string;// User-friendly messagepercentage: number;// Percentage complete (0-100)startTime: number;// Timestamp when started (ms)updatedTime: number;// Timestamp of last update (ms)complete: boolean;// Whether progress is complete}The tool uses file-based atomic writes for concurrent safety:
- Unique filenames — Each tracker ID gets a separate file
- Atomic rename — Write to temp file, then rename atomically
- Random temp names — Prevents temp file collisions
- File locking — OS-level atomicity guarantees
Multiple processes can safely update the same progress tracker.
- ID validation: Only alphanumeric characters, hyphens, and underscores allowed (prevents path traversal)
- Null byte protection: IDs and file paths reject null bytes
- Max ID length: 255 characters maximum
- High-frequency updates: Designed for concurrent writes from multiple processes
- Shared progress files: Progress data is meant to be shared—do not include sensitive data in messages
- File permissions: Progress files are created with mode 0o644 (world-readable)
See the examples/ directory for runnable examples:
# Quick start with Builder API (recommended)
npx tsx examples/builder-api.ts
# Multi-progress tracking
npx tsx examples/multi-progress.ts
# Custom templates and spinners
npx tsx examples/templates.ts
# Advanced multi-stage pipeline
npx tsx examples/advanced.ts
# Legacy examples
npx tsx examples/basic.ts # Original functional API
npx tsx examples/concurrent.ts # Original concurrent tracking
bash examples/cli-usage.sh # Shell script usagenpm test# Run all tests (239 tests)
npm run build # TypeScript compilation
npx tsc --noEmit # Type check onlyTest Coverage: 239 tests
- Functional API tests (35 tests)
- CLI integration tests (28 tests)
- Filesystem edge cases (21 tests)
- Fuzzy tests (32 tests)
- ProgressTracker tests (28 tests)
- ProgressBuilder tests (17 tests)
- createProgress tests (7 tests)
- MultiProgress tests (23 tests)
- Template system tests (48 tests)
Test Quality:
- 100% pass rate
- Zero flaky tests (validated with Test Flakiness Detector)
- Fully deterministic
- Comprehensive edge case coverage
This tool demonstrates BIDIRECTIONAL VALIDATION - we both USE and are VALIDATED BY other Tuulbelt tools:
1. Used By Test Flakiness Detector (Library Integration)
The Test Flakiness Detector integrates cli-progress-reporting to show real-time progress during detection (when running ≥5 iterations):
cd /path/to/test-flakiness-detector
prog --test "npm test" --runs 20 --verbose
# [INFO] Progress tracking enabled (dogfooding cli-progress-reporting)# [INFO] Run 1/20# [INFO] Run 2/20 passed (2 passed, 0 failed)# ...This provides:
- Live run counts and pass/fail status
- Better UX for long detection runs (50-100 iterations)
- Real-world validation of the progress reporting tool
- Graceful fallback when cloned standalone
2. High-Value Composition Scripts
Test Flakiness Detector - Prove concurrent safety (bidirectional validation):
./scripts/dogfood-flaky.sh 20
# ✅ NO FLAKINESS DETECTED# 239 tests × 20 runs = 4,780 executions# Validates concurrent progress tracking and template renderingOutput Diffing Utility - Prove deterministic outputs:
./scripts/dogfood-diff.sh
# Compares test outputs between runs# Should be IDENTICAL (no random data)This creates a bidirectional validation network where:
↔️ Test Flakiness Detector USES CLI Progress (library integration)↔️ Test Flakiness Detector VALIDATES CLI Progress (composition scripts)
See DOGFOODING_STRATEGY.md for implementation details.
All operations return a Result<T> type:
typeResult<T>=|{ok: true;value: T}|{ok: false;error: string};Errors are never thrown, making it safe to use in scripts.
Common errors:
Total must be greater than 0— Invalid initializationIncrement amount must be non-negative— Negative incrementProgress file does not exist— Tracker not initializedFailed to write progress: ...— File system error
0— Success1— Error (invalid arguments, file operation failed, etc.)
- File I/O: ~1-2ms per operation (read + write)
- Atomic writes: No performance penalty vs. direct writes
- Scalability: Tested with 1,000,000 total units
- File-based: Not suitable for in-memory progress bars
- Polling required: No push notifications when progress changes
- Temp directory: Progress files stored in OS temp directory by default
Uses the write-then-rename pattern for atomic updates:
- Write new state to temporary file (
progress-{id}.json.tmp.{random}) - Atomically rename temp file to target file (
progress-{id}.json) - Read operations always see complete, valid JSON
This ensures concurrent processes never read partial writes.
Potential improvements for future versions:
- ✅ Built-in progress bar rendering with customizable formats — Completed in v0.2.0 (TemplateEngine)
- ✅ Progress aggregation across multiple trackers — Completed in v0.2.0 (MultiProgress)
- ✅ Time estimation based on historical progress rates — Completed in v0.2.0 (ETA calculation)
- Real-time progress streaming via WebSocket or Server-Sent Events (planned for v0.3.0)
- CLI nested command structure for better UX (planned for v0.3.0)
- Integration with popular build tools (npm scripts, Make, Gradle)
- Optional compression for progress state files
▶ View interactive recording on asciinema.org
MIT — see LICENSE
See CONTRIBUTING.md for contribution guidelines.
Part of the Tuulbelt collection:
- Test Flakiness Detector — Detect unreliable tests
- More tools at https://tuulbelt.github.io/tuulbelt/
