UNPKG

@neurolint/cli

Version:

NeuroLint CLI for React/Next.js modernization with advanced 6-layer orchestration and intelligent AST transformations

332 lines (283 loc) 8.07 kB
import fs from "fs-extra"; import path from "path"; import chalk from "chalk"; import ora, { Ora } from "ora"; export interface ProgressState { id: string; operation: string; total: number; completed: number; failed: number; startTime: number; lastUpdate: number; files: { completed: string[]; failed: string[]; remaining: string[]; }; } export class ProgressTracker { private state: ProgressState; private stateFile: string; private spinner?: Ora; private autoSave: boolean; constructor(operation: string, files: string[], autoSave: boolean = true) { this.autoSave = autoSave; this.state = { id: `${operation}-${Date.now()}`, operation, total: files.length, completed: 0, failed: 0, startTime: Date.now(), lastUpdate: Date.now(), files: { completed: [], failed: [], remaining: [...files], }, }; this.stateFile = path.join(process.cwd(), ".neurolint-progress.json"); } async start(): Promise<void> { if (this.autoSave) { await this.saveState(); } this.spinner = ora({ text: this.getSpinnerText(), color: "blue", }).start(); } async markCompleted(file: string): Promise<void> { const index = this.state.files.remaining.indexOf(file); if (index !== -1) { this.state.files.remaining.splice(index, 1); this.state.files.completed.push(file); this.state.completed++; this.state.lastUpdate = Date.now(); if (this.spinner) { this.spinner.text = this.getSpinnerText(); } if (this.autoSave) { await this.saveState(); } } } async markFailed(file: string, error?: string): Promise<void> { const index = this.state.files.remaining.indexOf(file); if (index !== -1) { this.state.files.remaining.splice(index, 1); this.state.files.failed.push(file); this.state.failed++; this.state.lastUpdate = Date.now(); if (this.spinner) { this.spinner.text = this.getSpinnerText(); } if (this.autoSave) { await this.saveState(); } } } async complete(success: boolean = true): Promise<void> { if (this.spinner) { if (success) { this.spinner.succeed(this.getCompletionMessage()); } else { this.spinner.fail(this.getCompletionMessage()); } } if (this.autoSave) { await this.cleanup(); } } async saveState(): Promise<void> { try { await fs.writeJson(this.stateFile, this.state, { spaces: 2 }); } catch (error) { // Ignore save errors to not interrupt the main operation } } async loadState(): Promise<ProgressState | null> { try { if (await fs.pathExists(this.stateFile)) { return await fs.readJson(this.stateFile); } } catch (error) { // Ignore load errors } return null; } async cleanup(): Promise<void> { try { if (await fs.pathExists(this.stateFile)) { await fs.remove(this.stateFile); } } catch (error) { // Ignore cleanup errors } } getProgress(): { percentage: number; eta: string } { const processed = this.state.completed + this.state.failed; const percentage = this.state.total > 0 ? (processed / this.state.total) * 100 : 0; const elapsed = Date.now() - this.state.startTime; const rate = processed / (elapsed / 1000); // files per second const remaining = this.state.total - processed; const eta = rate > 0 ? (remaining / rate) * 1000 : 0; // milliseconds return { percentage: Math.round(percentage * 100) / 100, eta: formatDuration(eta), }; } private getSpinnerText(): string { const { percentage, eta } = this.getProgress(); const processed = this.state.completed + this.state.failed; let text = `${this.state.operation} (${processed}/${this.state.total}) ${percentage.toFixed(1)}%`; if (eta !== "0s" && processed > 0) { text += ` - ETA: ${eta}`; } if (this.state.failed > 0) { text += chalk.red(` - ${this.state.failed} failed`); } return text; } private getCompletionMessage(): string { const duration = formatDuration(Date.now() - this.state.startTime); let message = `${this.state.operation} completed in ${duration}`; if (this.state.failed > 0) { message += chalk.red(` (${this.state.failed} failed)`); } return message; } // Getters for accessing state get totalFiles(): number { return this.state.total; } get completedFiles(): number { return this.state.completed; } get failedFiles(): number { return this.state.failed; } get remainingFiles(): string[] { return [...this.state.files.remaining]; } get completedFilesList(): string[] { return [...this.state.files.completed]; } get failedFilesList(): string[] { return [...this.state.files.failed]; } } export async function resumeOperation( operationName: string, ): Promise<ProgressState | null> { const stateFile = path.join(process.cwd(), ".neurolint-progress.json"); try { if (await fs.pathExists(stateFile)) { const state = await fs.readJson(stateFile); if ( state.operation === operationName && state.files.remaining.length > 0 ) { console.log( chalk.white(`\nFound incomplete ${operationName} operation`), ); console.log( chalk.gray( ` Started: ${new Date(state.startTime).toLocaleString()}`, ), ); console.log( chalk.gray( ` Progress: ${state.completed}/${state.total} completed, ${state.failed} failed`, ), ); console.log( chalk.gray(` Remaining: ${state.files.remaining.length} files\n`), ); return state; } } } catch (error) { // Ignore errors and start fresh } return null; } function formatDuration(ms: number): string { if (ms < 1000) return "0s"; const seconds = Math.floor(ms / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); if (hours > 0) { return `${hours}h ${minutes % 60}m`; } else if (minutes > 0) { return `${minutes}m ${seconds % 60}s`; } else { return `${seconds}s`; } } export function createBatchProcessor<T>( items: T[], processor: (item: T) => Promise<void>, options: { batchSize?: number; concurrency?: number; onProgress?: (completed: number, total: number) => void; } = {}, ) { const { batchSize = 10, concurrency = 3, onProgress } = options; return { async process(): Promise<{ completed: number; failed: number }> { let completed = 0; let failed = 0; // Process items in batches with limited concurrency for (let i = 0; i < items.length; i += batchSize) { const batch = items.slice(i, i + batchSize); // Limit concurrency within batch const semaphore = new Semaphore(concurrency); await Promise.all( batch.map(async (item) => { await semaphore.acquire(); try { await processor(item); completed++; } catch (error) { failed++; } finally { semaphore.release(); if (onProgress) { onProgress(completed + failed, items.length); } } }), ); } return { completed, failed }; }, }; } class Semaphore { private permits: number; private waiting: (() => void)[] = []; constructor(permits: number) { this.permits = permits; } async acquire(): Promise<void> { if (this.permits > 0) { this.permits--; return; } return new Promise((resolve) => { this.waiting.push(resolve); }); } release(): void { if (this.waiting.length > 0) { const resolve = this.waiting.shift()!; resolve(); } else { this.permits++; } } }