Compare commits

...

6 Commits

Author SHA1 Message Date
f80ec7ddfe 3.1.2
Some checks failed
Default (tags) / security (push) Successful in 44s
Default (tags) / test (push) Failing after 1m5s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-05-16 15:01:57 +00:00
f2823c2645 fix(tests): Update test imports and devDependencies to use @git.zone/tstest/tapbundle 2025-05-16 15:01:56 +00:00
75783b0e87 3.1.1
Some checks failed
Default (tags) / security (push) Successful in 45s
Default (tags) / test (push) Failing after 1m6s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-05-15 20:00:38 +00:00
13e1582732 fix(source-interactive): Fix import path in receiver tests and rename progress bar property for clarity; update SmartlogSourceOra getter for improved backward compatibility. 2025-05-15 20:00:38 +00:00
7e2f076b35 3.1.0
Some checks failed
Default (tags) / security (push) Successful in 38s
Default (tags) / test (push) Failing after 1m6s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-05-15 19:53:29 +00:00
7e8a404fcf feat(interactive): Add interactive console features and refactor spinner module by renaming source-ora to source-interactive and removing ora dependency 2025-05-15 19:53:29 +00:00
23 changed files with 1561 additions and 1459 deletions

View File

@ -1,5 +1,27 @@
# Changelog
## 2025-05-16 - 3.1.2 - fix(tests)
Update test imports and devDependencies to use @git.zone/tstest/tapbundle
- Changed import statements in test files from '@push.rocks/tapbundle' to '@git.zone/tstest/tapbundle'
- Updated devDependency '@git.zone/tstest' to version ^1.7.0 and removed dependency on '@push.rocks/tapbundle'
## 2025-05-15 - 3.1.1 - fix(source-interactive)
Fix import path in receiver tests and rename progress bar property for clarity; update SmartlogSourceOra getter for improved backward compatibility.
- Changed test file import from '../ts/index.js' to '../dist_ts/index.js' in test.receiver.node.ts to resolve module path issues
- Renamed property 'complete' to 'completeChar' in SmartlogProgressBar and updated its usage accordingly
- Modified SmartlogSourceOra getter to use public methods for starting and stopping the spinner, ensuring backward compatibility
## 2025-05-15 - 3.1.0 - feat(interactive)
Add interactive console features and refactor spinner module by renaming source-ora to source-interactive and removing ora dependency
- Renamed source-ora module to source-interactive and updated package.json exports
- Removed ora dependency in favor of a custom spinner implementation
- Added new progress bar functionality with configurable options including ETA, percentage, and color
- Updated tests and documentation (README and plan) to reflect the new interactive features
- Bumped dependency versions in package.json and improved test script configuration
## 2025-05-12 - 3.0.9 - fix(test/destination-devtools.browser)
Simplify DevTools browser tests by removing redundant styled log assertions.

View File

@ -1,6 +1,6 @@
{
"name": "@push.rocks/smartlog",
"version": "3.0.9",
"version": "3.1.2",
"private": false,
"description": "A minimalistic, distributed, and extensible logging tool supporting centralized log management.",
"keywords": [
@ -25,7 +25,7 @@
},
"./context": "./dist_ts_context/index.js",
"./interfaces": "./dist_ts_interfaces/index.js",
"./source-ora": "./dist_ts_source_ora/index.js",
"./source-interactive": "./dist_ts_source_interactive/index.js",
"./destination-clickhouse": "./dist_ts_destination_clickhouse/index.js",
"./destination-devtools": "./dist_ts_destination_devtools/index.js",
"./destination-file": "./dist_ts_destination_file/index.js",
@ -36,18 +36,17 @@
"author": "Lossless GmbH",
"license": "MIT",
"scripts": {
"test": "(tstest test/)",
"test": "(tstest test/**/*.ts --verbose)",
"build": "(tsbuild tsfolders --allowimplicitany && tsbundle npm)",
"format": "(gitzone format)",
"buildDocs": "tsdoc"
},
"devDependencies": {
"@git.zone/tsbuild": "^2.3.2",
"@git.zone/tsbuild": "^2.5.1",
"@git.zone/tsbundle": "^2.2.5",
"@git.zone/tsrun": "^1.3.3",
"@git.zone/tstest": "^1.0.96",
"@push.rocks/tapbundle": "^6.0.3",
"@types/node": "^22.15.17"
"@git.zone/tstest": "^1.7.0",
"@types/node": "^22.15.18"
},
"dependencies": {
"@api.global/typedrequest-interfaces": "^3.0.19",
@ -59,8 +58,7 @@
"@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smarttime": "^4.1.1",
"@push.rocks/webrequest": "^3.0.37",
"@tsclass/tsclass": "^9.2.0",
"ora": "^8.2.0"
"@tsclass/tsclass": "^9.2.0"
},
"files": [
"ts/**/*",

1978
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -60,6 +60,78 @@ defaultLogger.log('warn', 'This is a warning message using the default logger');
This is particularly helpful for simple applications or for initial project setup.
### Interactive Console Features
Smartlog provides interactive console features through the `@push.rocks/smartlog/source-interactive` module:
#### Spinners
Use spinners to show progress for operations:
```typescript
import { SmartlogSourceInteractive } from '@push.rocks/smartlog/source-interactive';
const spinner = new SmartlogSourceInteractive();
spinner.text('Loading data...');
// Later, when the operation completes:
spinner.finishSuccess('Data loaded successfully!');
// Or if it fails:
spinner.finishFail('Failed to load data');
// You can chain operations:
spinner.text('Connecting to server');
spinner.successAndNext('Fetching records');
spinner.successAndNext('Processing data');
spinner.finishSuccess('All done!');
// Customize appearance:
spinner.setSpinnerStyle('line'); // 'dots', 'line', 'star', or 'simple'
spinner.setColor('green'); // 'red', 'green', 'yellow', 'blue', etc.
spinner.setSpeed(100); // Animation speed in milliseconds
```
#### Progress Bars
Create progress bars for tracking operation progress:
```typescript
import { SmartlogProgressBar } from '@push.rocks/smartlog/source-interactive';
const progressBar = new SmartlogProgressBar({
total: 100, // Total number of items
width: 40, // Width of the progress bar
complete: '█', // Character for completed section
incomplete: '░', // Character for incomplete section
showEta: true, // Show estimated time remaining
showPercent: true, // Show percentage
showCount: true // Show count (e.g., "50/100")
});
// Update progress
progressBar.update(50); // Set to 50% progress
// Or increment
progressBar.increment(10); // Increase by 10 units
// Change color
progressBar.setColor('blue');
// Complete the progress bar
progressBar.update(100); // or progressBar.complete();
```
#### Non-Interactive Environments
Both spinners and progress bars automatically detect non-interactive environments (CI/CD, piped output, non-TTY) and provide fallback text-based output:
```
[Loading] Loading data...
Progress: 50% (50/100)
Progress: 100% (100/100)
[Success] Data loaded successfully!
```
### Extending With Log Destinations
One of the core strengths of `@push.rocks/smartlog` is its ability to work with multiple log destinations, enabling you to log messages not just to the console but also to external logging services or custom destinations.

View File

@ -0,0 +1,101 @@
# Smartlog Interactive Console Features Plan
## Overview
This document outlines the plan for enhancing the console output capabilities of `@push.rocks/smartlog` by creating a comprehensive interactive console module. This involves renaming the current `ts_source_ora` module to `ts_source_interactive`, implementing our own spinner functionality (removing the ora dependency), and adding new features like progress bars and other interactive elements.
## Implementation Steps
### 1. Rename and Restructure
- Rename directory from `ts_source_ora` to `ts_source_interactive`
- Update all imports, exports, and references
- Update package.json exports to reflect new module name
- Maintain backward compatibility through proper export paths
### 2. Custom Spinner Implementation
- Create a native spinner implementation to replace ora dependency
- Implement spinner frames and animation timing
- Maintain API compatibility with current spinner methods:
- `text(textArg)` - Set text and start spinner
- `stop()` - Stop the spinner
- `finishSuccess(textArg?)` - Mark as succeeded
- `finishFail(textArg?)` - Mark as failed
- `successAndNext(textArg)` - Success and start new spinner
- `failAndNext(textArg)` - Fail and start new spinner
- Add spinner customization options (speed, frames, colors)
### 3. Progress Bar Implementation
- Create a progress bar component with the following features:
- Configurable width and style
- Percentage display
- ETA calculation
- Current/total value display
- Custom formatting
- Theming support
- Methods for incrementing and updating
### 4. Additional Interactive Features
- Add indeterminate progress indicator
- Add multi-line status display
- Add table formatting for structured data
- Add interactive prompts/confirmations
### 5. Testing
- Update existing tests to work with new implementation
- Add tests for new progress bar functionality
- Add tests for additional interactive features
- Ensure consistent behavior across platforms
### 6. Documentation
- Update README with examples of new features
- Add API documentation for all new methods
- Include usage examples
## Implementation Details
### Progress Bar API
```typescript
// Creating a progress bar
const progressBar = new SmartlogProgressBar({
total: 100,
width: 40,
complete: '=',
incomplete: ' ',
renderThrottle: 100, // ms
clearOnComplete: false,
showEta: true
});
// Updating a progress bar
progressBar.update(50); // Update to 50%
progressBar.increment(10); // Increment by 10
progressBar.complete(); // Mark as complete
```
### Spinner API (maintains compatibility)
```typescript
// Current API (to be maintained)
const spinner = new SmartlogSourceInteractive();
spinner.text('Loading data');
spinner.finishSuccess('Data loaded successfully');
// New additions
spinner.setSpinnerStyle('dots'); // Change spinner style
spinner.setColor('green'); // Change color
```
## Benefits
- Remove external dependency (ora) for better control and smaller bundle size
- Provide more interactive console features for improved user experience
- Maintain consistent API styling across all smartlog modules
- Improve testability with custom implementation
- Enable more advanced terminal interactions

View File

@ -1,4 +1,4 @@
import { expect, tap } from '@push.rocks/tapbundle';
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as smartlog from '../ts/index.js';
let testConsoleLog: smartlog.ConsoleLog;

View File

@ -1,4 +1,4 @@
import { expect, tap } from '@push.rocks/tapbundle';
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as smartlogContext from '../ts_context/index.js';
tap.test('should correctly export strings from context module', async () => {

View File

@ -1,4 +1,4 @@
import { expect, tap } from '@push.rocks/tapbundle';
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { SmartlogDestinationClickhouse } from '../ts_destination_clickhouse/index.js';
import * as smartclickhouse from '@push.rocks/smartclickhouse';

View File

@ -1,4 +1,4 @@
import { expect, tap } from '@push.rocks/tapbundle';
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { SmartlogDestinationDevtools } from '../ts_destination_devtools/index.js';
export const run = async function() {

View File

@ -1,4 +1,4 @@
import { expect, tap } from '@push.rocks/tapbundle';
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { SmartlogDestinationDevtools } from '../ts_destination_devtools/index.js';
// Test we can create a destination instance

View File

@ -1,4 +1,4 @@
import { expect, tap } from '@push.rocks/tapbundle';
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { SmartlogDestinationFile } from '../ts_destination_file/index.js';
import * as fs from 'fs';
import * as path from 'path';

View File

@ -1,4 +1,4 @@
import { expect, tap } from '@push.rocks/tapbundle';
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DestinationLocal } from '../ts_destination_local/index.js';
import * as smartlogInterfaces from '../ts_interfaces/index.js';

View File

@ -1,4 +1,4 @@
import { expect, tap } from '@push.rocks/tapbundle';
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { SmartlogDestinationReceiver } from '../ts_destination_receiver/index.js';
import { Smartlog } from '../ts/index.js';
import * as smartlogInterfaces from '../ts_interfaces/index.js';

View File

@ -0,0 +1,64 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { SmartlogSourceInteractive, SmartlogProgressBar } from '../ts_source_interactive/index.js';
// Test instances
let testSpinner: SmartlogSourceInteractive;
let testProgressBar: SmartlogProgressBar;
// Original state for restoration
const originalState = {
stdoutTTY: process.stdout.isTTY,
consoleLog: console.log
};
// Log tracking
const logs: string[] = [];
tap.test('should handle non-interactive mode correctly', async (toolsArg) => {
// Setup non-interactive mode
process.stdout.isTTY = false;
console.log = (...args: any[]) => {
logs.push(args.join(' '));
};
// Test spinner creation
testSpinner = new SmartlogSourceInteractive();
expect(testSpinner).toBeTruthy();
// Test spinner text
logs.length = 0;
testSpinner.text('Loading data');
expect(logs.length).toBeGreaterThan(0);
expect(logs[0]).toContain('[Loading]');
expect(logs[0]).toContain('Loading data');
// Test spinner success
logs.length = 0;
testSpinner.finishSuccess('Task completed');
expect(logs.length).toBeGreaterThan(0);
expect(logs[0]).toContain('[Success]');
expect(logs[0]).toContain('Task completed');
// Test progress bar
testProgressBar = new SmartlogProgressBar({ total: 100 });
expect(testProgressBar).toBeTruthy();
// Test progress updates
logs.length = 0;
testProgressBar.update(10);
testProgressBar.update(50);
testProgressBar.update(100);
expect(logs.length).toBeGreaterThan(0);
const progressLogs = logs.join(' ');
expect(progressLogs).toContain('10%');
expect(progressLogs).toContain('50%');
expect(progressLogs).toContain('100%');
// Cleanup
testSpinner.stop();
console.log = originalState.consoleLog;
process.stdout.isTTY = originalState.stdoutTTY;
});
export default tap.start();

View File

@ -1,6 +1,6 @@
import { expect, tap } from '@push.rocks/tapbundle';
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { SmartlogReceiver } from '../ts_receiver/index.js';
import { Smartlog } from '../ts/index.js';
import { Smartlog } from '../dist_ts/index.js';
import * as smartlogInterfaces from '../ts_interfaces/index.js';
import * as smarthash from '@push.rocks/smarthash';

View File

@ -0,0 +1,190 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { SmartlogSourceInteractive, SmartlogProgressBar, SmartlogSourceOra } from '../ts_source_interactive/index.js';
// Test spinner functionality
let testSpinner: SmartlogSourceInteractive;
// Helper function to clean up spinners after each test
const cleanupSpinner = (spinner: SmartlogSourceInteractive) => {
if (spinner.isStarted()) {
spinner.stop();
}
};
tap.test('should create a SmartlogSourceInteractive instance', async () => {
testSpinner = new SmartlogSourceInteractive();
testSpinner.setSpeed(10); // Set fast animation speed for tests
expect(testSpinner).toBeTruthy();
expect(testSpinner.isStarted()).toBeFalse();
});
tap.test('should set text and start spinner', async () => {
const testText = 'Testing spinner';
testSpinner.text(testText);
expect(testSpinner.isStarted()).toBeTrue();
cleanupSpinner(testSpinner);
});
tap.test('should update text', async () => {
const newText = 'Updated text';
testSpinner.text(newText);
expect(testSpinner.isStarted()).toBeTrue();
cleanupSpinner(testSpinner);
});
tap.test('should stop spinner', async () => {
testSpinner.stop();
// We can't easily test the visual state, but we can verify it doesn't throw errors
});
tap.test('should finish with success', async () => {
testSpinner = new SmartlogSourceInteractive();
testSpinner.text('Starting again');
const successText = 'Operation successful';
testSpinner.finishSuccess(successText);
expect(testSpinner.isStarted()).toBeFalse();
});
tap.test('should finish with failure', async () => {
testSpinner = new SmartlogSourceInteractive();
testSpinner.text('Starting again');
const failText = 'Operation failed';
testSpinner.finishFail(failText);
expect(testSpinner.isStarted()).toBeFalse();
});
tap.test('should handle success and next', async () => {
testSpinner = new SmartlogSourceInteractive();
testSpinner.setSpeed(10); // Fast animation
testSpinner.text('Starting again');
const nextText = 'Next operation';
testSpinner.successAndNext(nextText);
expect(testSpinner.isStarted()).toBeTrue();
cleanupSpinner(testSpinner);
});
tap.test('should handle fail and next', async () => {
testSpinner = new SmartlogSourceInteractive();
testSpinner.setSpeed(10); // Fast animation
testSpinner.text('Starting again');
const nextText = 'Next operation after failure';
testSpinner.failAndNext(nextText);
expect(testSpinner.isStarted()).toBeTrue();
cleanupSpinner(testSpinner);
});
tap.test('should set spinner style', async () => {
testSpinner = new SmartlogSourceInteractive();
testSpinner.setSpeed(10); // Fast animation
testSpinner.setSpinnerStyle('line');
testSpinner.text('Custom style spinner');
// Visual effect can't be easily tested, but we can verify it doesn't throw errors
expect(testSpinner.isStarted()).toBeTrue();
cleanupSpinner(testSpinner);
});
tap.test('should set spinner color', async () => {
testSpinner = new SmartlogSourceInteractive();
testSpinner.setSpeed(10); // Fast animation
testSpinner.setColor('green');
testSpinner.text('Green spinner');
// Visual effect can't be easily tested, but we can verify it doesn't throw errors
expect(testSpinner.isStarted()).toBeTrue();
cleanupSpinner(testSpinner);
});
tap.test('should set animation speed', async () => {
testSpinner = new SmartlogSourceInteractive();
testSpinner.setSpeed(10); // Actually set fast for testing
testSpinner.text('Slow spinner');
// Visual effect can't be easily tested, but we can verify it doesn't throw errors
expect(testSpinner.isStarted()).toBeTrue();
cleanupSpinner(testSpinner);
});
// Test progress bar functionality
let testProgressBar: SmartlogProgressBar;
tap.test('should create a progress bar instance', async () => {
testProgressBar = new SmartlogProgressBar({
total: 100
});
expect(testProgressBar).toBeTruthy();
});
tap.test('should update progress bar value', async () => {
testProgressBar.update(50);
// Visual effect can't be easily tested, but we can verify it doesn't throw errors
});
tap.test('should increment progress bar', async () => {
const initialValue = 50;
const increment = 10;
testProgressBar = new SmartlogProgressBar({ total: 100 });
testProgressBar.update(initialValue);
testProgressBar.increment(increment);
// Visual effect can't be easily tested, but we can verify it doesn't throw errors
});
tap.test('should complete progress bar', async () => {
testProgressBar = new SmartlogProgressBar({ total: 100 });
testProgressBar.update(50);
testProgressBar.update(100); // Update to 100% to simulate completion
// Visual effect can't be easily tested, but we can verify it doesn't throw errors
});
tap.test('should set progress bar color', async () => {
testProgressBar = new SmartlogProgressBar({ total: 100 });
testProgressBar.setColor('blue');
testProgressBar.update(50);
// Visual effect can't be easily tested, but we can verify it doesn't throw errors
});
tap.test('should handle custom progress bar options', async () => {
testProgressBar = new SmartlogProgressBar({
total: 100,
width: 40,
complete: '=',
incomplete: '-',
showEta: false,
showPercent: true,
showCount: true
});
testProgressBar.update(30);
// Visual effect can't be easily tested, but we can verify it doesn't throw errors
});
// Test backward compatibility with SmartlogSourceOra
let testSourceOra: SmartlogSourceOra;
tap.test('should create a SmartlogSourceOra instance for backward compatibility', async () => {
testSourceOra = new SmartlogSourceOra();
expect(testSourceOra).toBeTruthy();
expect(testSourceOra.isStarted()).toBeFalse();
});
tap.test('should maintain compatibility with old API', async () => {
testSourceOra.setSpeed(10); // Fast animation
testSourceOra.text('Testing backward compatibility');
expect(testSourceOra.isStarted()).toBeTrue();
testSourceOra.finishSuccess('Success');
expect(testSourceOra.isStarted()).toBeFalse();
});
export default tap.start();

View File

@ -1,75 +0,0 @@
import { expect, tap } from '@push.rocks/tapbundle';
import { SmartlogSourceOra } from '../ts_source_ora/index.js';
let testSourceOra: SmartlogSourceOra;
tap.test('should create a SmartlogSourceOra instance', async () => {
testSourceOra = new SmartlogSourceOra();
expect(testSourceOra).toBeTruthy();
expect(testSourceOra.started).toBeFalse();
});
tap.test('should set text and start spinner', async () => {
const testText = 'Testing ora spinner';
testSourceOra.text(testText);
expect(testSourceOra.started).toBeTrue();
expect(testSourceOra.oraInstance.text).toEqual(testText);
});
tap.test('should update text', async () => {
const newText = 'Updated text';
testSourceOra.text(newText);
expect(testSourceOra.oraInstance.text).toEqual(newText);
expect(testSourceOra.started).toBeTrue();
});
tap.test('should stop spinner', async () => {
testSourceOra.stop();
// We can't easily test the visual state, but we can verify it doesn't throw errors
});
tap.test('should finish with success', async () => {
testSourceOra = new SmartlogSourceOra();
testSourceOra.text('Starting again');
const successText = 'Operation successful';
testSourceOra.finishSuccess(successText);
expect(testSourceOra.started).toBeFalse();
});
tap.test('should finish with failure', async () => {
testSourceOra = new SmartlogSourceOra();
testSourceOra.text('Starting again');
const failText = 'Operation failed';
testSourceOra.finishFail(failText);
expect(testSourceOra.started).toBeFalse();
});
tap.test('should handle success and next', async () => {
testSourceOra = new SmartlogSourceOra();
testSourceOra.text('Starting again');
const nextText = 'Next operation';
testSourceOra.successAndNext(nextText);
expect(testSourceOra.started).toBeTrue();
expect(testSourceOra.oraInstance.text).toEqual(nextText);
});
tap.test('should handle fail and next', async () => {
testSourceOra = new SmartlogSourceOra();
testSourceOra.text('Starting again');
const nextText = 'Next operation after failure';
testSourceOra.failAndNext(nextText);
expect(testSourceOra.started).toBeTrue();
expect(testSourceOra.oraInstance.text).toEqual(nextText);
});
export default tap.start();

View File

@ -1,4 +1,4 @@
import { expect, tap } from '@push.rocks/tapbundle';
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as smartlog from '../ts/index.js';
let testConsoleLog: smartlog.ConsoleLog;

View File

@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartlog',
version: '3.0.9',
version: '3.1.2',
description: 'A minimalistic, distributed, and extensible logging tool supporting centralized log management.'
}

View File

@ -0,0 +1,417 @@
import * as plugins from './smartlog-source-interactive.plugins.js';
/**
* Utility to detect if the environment is interactive
* Checks for TTY capability and common CI environment variables
*/
const isInteractive = () => {
try {
return Boolean(
// Check TTY capability
process.stdout && process.stdout.isTTY &&
// Additional checks for non-interactive environments
!('CI' in process.env) &&
!process.env.GITHUB_ACTIONS &&
!process.env.JENKINS_URL &&
!process.env.GITLAB_CI &&
!process.env.TRAVIS &&
!process.env.CIRCLECI &&
process.env.TERM !== 'dumb'
);
} catch (e) {
// If any error occurs (e.g., in browser environments without process),
// assume a non-interactive environment to be safe
return false;
}
};
// Helper to log messages in non-interactive mode
const logMessage = (message: string, prefix = '') => {
if (prefix) {
console.log(`${prefix} ${message}`);
} else {
console.log(message);
}
};
// Spinner frames and styles
const spinnerFrames = {
dots: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
line: ['|', '/', '-', '\\'],
star: ['✶', '✸', '✹', '✺', '✹', '✷'],
simple: ['-', '\\', '|', '/']
};
// Color names mapping to ANSI color codes
const colors = {
black: '\u001b[30m',
red: '\u001b[31m',
green: '\u001b[32m',
yellow: '\u001b[33m',
blue: '\u001b[34m',
magenta: '\u001b[35m',
cyan: '\u001b[36m',
white: '\u001b[37m',
gray: '\u001b[90m',
reset: '\u001b[0m'
};
/**
* A class for creating interactive spinners
* Automatically handles non-interactive environments
*/
export class SmartlogSourceInteractive {
private textContent: string = 'loading';
private currentFrame: number = 0;
private interval: NodeJS.Timeout | null = null;
private started: boolean = false;
private spinnerStyle: keyof typeof spinnerFrames = 'dots';
private color: keyof typeof colors = 'cyan';
private frames: string[];
private frameInterval: number = 80;
private interactive: boolean;
constructor() {
this.frames = spinnerFrames[this.spinnerStyle];
this.interactive = isInteractive();
}
/**
* Sets the text for the spinner and starts it if not already started
*/
public text(textArg: string) {
this.textContent = textArg;
if (!this.interactive) {
// In non-interactive mode, just log the message with a loading indicator
logMessage(textArg, '[Loading]');
this.started = true;
return;
}
if (!this.started) {
this.started = true;
this.start();
} else {
this.renderFrame();
}
}
/**
* Starts the spinner animation
*/
private start() {
if (!this.interactive) {
return; // No animation in non-interactive mode
}
if (this.interval) {
clearInterval(this.interval);
}
this.renderFrame();
this.interval = setInterval(() => {
this.currentFrame = (this.currentFrame + 1) % this.frames.length;
this.renderFrame();
}, this.frameInterval);
}
/**
* Renders the current frame of the spinner
*/
private renderFrame() {
if (!this.started || !this.interactive) return;
const frame = this.frames[this.currentFrame];
const colorCode = colors[this.color];
const resetCode = colors.reset;
// Only use ANSI escape codes in interactive mode
process.stdout.write('\r\x1b[2K'); // Clear the current line
process.stdout.write(`${colorCode}${frame}${resetCode} ${this.textContent}`);
}
/**
* Stops the spinner
*/
public stop() {
// Always clear the interval even in non-interactive mode
// This prevents memory leaks in tests and long-running applications
if (this.interval) {
clearInterval(this.interval);
this.interval = null;
}
if (!this.interactive) {
return; // No need to clear the line in non-interactive mode
}
process.stdout.write('\r\x1b[2K'); // Clear the current line
}
/**
* Marks the spinner as successful and optionally displays a success message
*/
public finishSuccess(textArg?: string) {
const message = textArg || this.textContent;
// Always stop the spinner first to clean up intervals
this.stop();
if (!this.interactive) {
logMessage(message, '[Success]');
} else {
const successSymbol = colors.green + '✓' + colors.reset;
process.stdout.write(`${successSymbol} ${message}\n`);
}
this.started = false;
}
/**
* Marks the spinner as failed and optionally displays a failure message
*/
public finishFail(textArg?: string) {
const message = textArg || this.textContent;
// Always stop the spinner first to clean up intervals
this.stop();
if (!this.interactive) {
logMessage(message, '[Failed]');
} else {
const failSymbol = colors.red + '✗' + colors.reset;
process.stdout.write(`${failSymbol} ${message}\n`);
}
this.started = false;
}
/**
* Marks the current spinner as successful and starts a new one
*/
public successAndNext(textArg: string) {
this.finishSuccess();
this.text(textArg);
}
/**
* Marks the current spinner as failed and starts a new one
*/
public failAndNext(textArg: string) {
this.finishFail();
this.text(textArg);
}
/**
* Sets the spinner style
*/
public setSpinnerStyle(style: keyof typeof spinnerFrames) {
this.spinnerStyle = style;
this.frames = spinnerFrames[style];
return this;
}
/**
* Sets the spinner color
*/
public setColor(colorName: keyof typeof colors) {
if (colorName in colors) {
this.color = colorName;
}
return this;
}
/**
* Sets the animation speed in milliseconds
*/
public setSpeed(ms: number) {
this.frameInterval = ms;
if (this.started) {
this.stop();
this.start();
}
return this;
}
/**
* Gets the current started state
*/
public isStarted() {
return this.started;
}
}
export interface IProgressBarOptions {
total: number;
width?: number;
complete?: string;
incomplete?: string;
renderThrottle?: number;
clear?: boolean;
showEta?: boolean;
showPercent?: boolean;
showCount?: boolean;
}
export class SmartlogProgressBar {
private total: number;
private current: number = 0;
private width: number;
private completeChar: string;
private incomplete: string;
private renderThrottle: number;
private clear: boolean;
private showEta: boolean;
private showPercent: boolean;
private showCount: boolean;
private color: keyof typeof colors = 'green';
private startTime: number | null = null;
private lastRenderTime: number = 0;
private interactive: boolean;
private lastLoggedPercent: number = 0;
private logThreshold: number = 10; // Log every 10% in non-interactive mode
constructor(options: IProgressBarOptions) {
this.total = options.total;
this.width = options.width || 30;
this.completeChar = options.complete || '█';
this.incomplete = options.incomplete || '░';
this.renderThrottle = options.renderThrottle || 16;
this.clear = options.clear !== undefined ? options.clear : false;
this.showEta = options.showEta !== undefined ? options.showEta : true;
this.showPercent = options.showPercent !== undefined ? options.showPercent : true;
this.showCount = options.showCount !== undefined ? options.showCount : true;
this.interactive = isInteractive();
}
/**
* Update the progress bar to a specific value
*/
public update(value: number): this {
if (this.startTime === null) {
this.startTime = Date.now();
}
this.current = Math.min(value, this.total);
if (!this.interactive) {
// In non-interactive mode, log progress at certain thresholds
const percent = Math.floor((this.current / this.total) * 100);
const currentThreshold = Math.floor(percent / this.logThreshold) * this.logThreshold;
if (currentThreshold > this.lastLoggedPercent || percent === 100) {
this.lastLoggedPercent = currentThreshold;
logMessage(`Progress: ${percent}% (${this.current}/${this.total})`);
}
return this;
}
// Throttle rendering to avoid excessive updates in interactive mode
const now = Date.now();
if (now - this.lastRenderTime < this.renderThrottle) {
return this;
}
this.lastRenderTime = now;
this.render();
return this;
}
/**
* Increment the progress bar by a value
*/
public increment(value: number = 1): this {
return this.update(this.current + value);
}
/**
* Mark the progress bar as complete
*/
public complete(): this {
this.update(this.total);
if (!this.interactive) {
logMessage(`Completed: 100% (${this.total}/${this.total})`);
return this;
}
if (this.clear) {
process.stdout.write('\r\x1b[2K');
} else {
process.stdout.write('\n');
}
return this;
}
/**
* Set the color of the progress bar
*/
public setColor(colorName: keyof typeof colors): this {
if (colorName in colors) {
this.color = colorName;
}
return this;
}
/**
* Render the progress bar
*/
private render(): void {
if (!this.interactive) {
return; // Don't render in non-interactive mode
}
// Calculate percent complete
const percent = Math.floor((this.current / this.total) * 100);
const completeLength = Math.round((this.current / this.total) * this.width);
const incompleteLength = this.width - completeLength;
// Build the progress bar
const completePart = colors[this.color] + this.completeChar.repeat(completeLength) + colors.reset;
const incompletePart = this.incomplete.repeat(incompleteLength);
const progressBar = `[${completePart}${incompletePart}]`;
// Calculate ETA if needed
let etaStr = '';
if (this.showEta && this.startTime !== null && this.current > 0) {
const elapsed = (Date.now() - this.startTime) / 1000;
const rate = this.current / elapsed;
const remaining = Math.max(0, this.total - this.current);
const eta = Math.round(remaining / rate);
const mins = Math.floor(eta / 60);
const secs = eta % 60;
etaStr = ` eta: ${mins}m${secs}s`;
}
// Build additional information
const percentStr = this.showPercent ? ` ${percent}%` : '';
const countStr = this.showCount ? ` ${this.current}/${this.total}` : '';
// Clear the line and render
process.stdout.write('\r\x1b[2K');
process.stdout.write(`${progressBar}${percentStr}${countStr}${etaStr}`);
}
}
// For backward compatibility with 'source-ora' module
export class SmartlogSourceOra extends SmartlogSourceInteractive {
// Add a stub for the oraInstance property for backward compatibility
public get oraInstance() {
// Use public methods instead of accessing private properties
const instance = this;
return {
get text() { return ''; }, // We can't access private textContent directly
start: () => instance.text(''), // This starts the spinner
stop: () => instance.stop(),
succeed: (text?: string) => instance.finishSuccess(text),
fail: (text?: string) => instance.finishFail(text)
};
}
public set oraInstance(value: any) {
// No-op, just for compatibility
}
}

View File

@ -0,0 +1,10 @@
// pushrocks scope
import * as smartlogInterfaces from '../dist_ts_interfaces/index.js';
import * as consolecolor from '@push.rocks/consolecolor';
export { smartlogInterfaces, consolecolor };
// node.js internal
import { stdout, stderr } from 'process';
export { stdout, stderr };

View File

@ -1,40 +0,0 @@
import * as plugins from './smartlog-source-ora.plugins.js';
export class SmartlogSourceOra {
public oraInstance = plugins.ora('loading');
public started = false;
constructor() {}
public text(textArg: string) {
this.oraInstance.text = textArg;
if (!this.started) {
this.started = true;
this.oraInstance.start();
}
}
public stop() {
this.oraInstance.stop();
}
public finishSuccess(textArg?: string) {
this.oraInstance.succeed(textArg);
this.started = false;
}
public finishFail(textArg?: string) {
this.oraInstance.fail(textArg);
this.started = false;
}
public successAndNext(textArg: string) {
this.finishSuccess();
this.text(textArg);
}
public failAndNext(textArg: string) {
this.finishFail();
this.text(textArg);
}
}

View File

@ -1,9 +0,0 @@
// pushrocks scope
import * as smartlogInterfaces from '../dist_ts_interfaces/index.js';
export { smartlogInterfaces };
// third party scope
import ora from 'ora';
export { ora };