llm-checker 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +418 -0
  3. package/analyzer/compatibility.js +584 -0
  4. package/analyzer/performance.js +505 -0
  5. package/bin/CLAUDE.md +12 -0
  6. package/bin/enhanced_cli.js +3118 -0
  7. package/bin/test-deterministic.js +41 -0
  8. package/package.json +96 -0
  9. package/src/CLAUDE.md +12 -0
  10. package/src/ai/intelligent-selector.js +615 -0
  11. package/src/ai/model-selector.js +312 -0
  12. package/src/ai/multi-objective-selector.js +820 -0
  13. package/src/commands/check.js +58 -0
  14. package/src/data/CLAUDE.md +11 -0
  15. package/src/data/model-database.js +637 -0
  16. package/src/data/sync-manager.js +279 -0
  17. package/src/hardware/CLAUDE.md +12 -0
  18. package/src/hardware/backends/CLAUDE.md +11 -0
  19. package/src/hardware/backends/apple-silicon.js +318 -0
  20. package/src/hardware/backends/cpu-detector.js +490 -0
  21. package/src/hardware/backends/cuda-detector.js +417 -0
  22. package/src/hardware/backends/intel-detector.js +436 -0
  23. package/src/hardware/backends/rocm-detector.js +440 -0
  24. package/src/hardware/detector.js +573 -0
  25. package/src/hardware/pc-optimizer.js +635 -0
  26. package/src/hardware/specs.js +286 -0
  27. package/src/hardware/unified-detector.js +442 -0
  28. package/src/index.js +2289 -0
  29. package/src/models/CLAUDE.md +17 -0
  30. package/src/models/ai-check-selector.js +806 -0
  31. package/src/models/catalog.json +426 -0
  32. package/src/models/deterministic-selector.js +1145 -0
  33. package/src/models/expanded_database.js +1142 -0
  34. package/src/models/intelligent-selector.js +532 -0
  35. package/src/models/requirements.js +310 -0
  36. package/src/models/scoring-config.js +57 -0
  37. package/src/models/scoring-engine.js +715 -0
  38. package/src/ollama/.cache/README.md +33 -0
  39. package/src/ollama/CLAUDE.md +24 -0
  40. package/src/ollama/client.js +438 -0
  41. package/src/ollama/enhanced-client.js +113 -0
  42. package/src/ollama/enhanced-scraper.js +634 -0
  43. package/src/ollama/manager.js +357 -0
  44. package/src/ollama/native-scraper.js +776 -0
  45. package/src/plugins/CLAUDE.md +11 -0
  46. package/src/plugins/examples/custom_model_plugin.js +87 -0
  47. package/src/plugins/index.js +295 -0
  48. package/src/utils/CLAUDE.md +11 -0
  49. package/src/utils/config.js +359 -0
  50. package/src/utils/formatter.js +315 -0
  51. package/src/utils/logger.js +272 -0
  52. package/src/utils/model-classifier.js +167 -0
  53. package/src/utils/verbose-progress.js +266 -0
@@ -0,0 +1,272 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+
5
+ class Logger {
6
+ constructor(options = {}) {
7
+ this.level = options.level || process.env.LLM_CHECKER_LOG_LEVEL || 'info';
8
+ this.enableColors = options.colors !== false && !process.env.NO_COLOR;
9
+ this.logFile = options.logFile || null;
10
+ this.enableConsole = options.console !== false;
11
+ this.enableDebug = options.debug || process.env.DEBUG === '1';
12
+
13
+ this.levels = {
14
+ error: 0,
15
+ warn: 1,
16
+ info: 2,
17
+ debug: 3,
18
+ trace: 4
19
+ };
20
+
21
+ this.colors = {
22
+ error: '\x1b[31m', // Red
23
+ warn: '\x1b[33m', // Yellow
24
+ info: '\x1b[36m', // Cyan
25
+ debug: '\x1b[35m', // Magenta
26
+ trace: '\x1b[37m', // White
27
+ reset: '\x1b[0m'
28
+ };
29
+
30
+ this.setupLogFile();
31
+ }
32
+
33
+ setupLogFile() {
34
+ if (this.logFile) {
35
+ const logDir = path.dirname(this.logFile);
36
+ if (!fs.existsSync(logDir)) {
37
+ try {
38
+ fs.mkdirSync(logDir, { recursive: true });
39
+ } catch (error) {
40
+ console.warn(`Warning: Could not create log directory: ${error.message}`);
41
+ this.logFile = null;
42
+ }
43
+ }
44
+ }
45
+ }
46
+
47
+ shouldLog(level) {
48
+ return this.levels[level] <= this.levels[this.level];
49
+ }
50
+
51
+ formatMessage(level, message, meta = {}) {
52
+ const timestamp = new Date().toISOString();
53
+ const levelUpper = level.toUpperCase();
54
+
55
+ let formatted = `${timestamp} [${levelUpper}]`;
56
+
57
+ if (meta.component) {
58
+ formatted += ` [${meta.component}]`;
59
+ }
60
+
61
+ formatted += ` ${message}`;
62
+
63
+ if (meta.data && typeof meta.data === 'object') {
64
+ formatted += '\n' + JSON.stringify(meta.data, null, 2);
65
+ }
66
+
67
+ if (meta.error && meta.error instanceof Error) {
68
+ formatted += '\n' + meta.error.stack;
69
+ }
70
+
71
+ return formatted;
72
+ }
73
+
74
+ colorize(level, message) {
75
+ if (!this.enableColors) return message;
76
+ return this.colors[level] + message + this.colors.reset;
77
+ }
78
+
79
+ writeToFile(message) {
80
+ if (!this.logFile) return;
81
+
82
+ try {
83
+ fs.appendFileSync(this.logFile, message + '\n', 'utf8');
84
+ } catch (error) {
85
+ // Fallback to console if file write fails
86
+ if (this.enableConsole) {
87
+ console.error('Failed to write to log file:', error.message);
88
+ }
89
+ }
90
+ }
91
+
92
+ log(level, message, meta = {}) {
93
+ if (!this.shouldLog(level)) return;
94
+
95
+ const formatted = this.formatMessage(level, message, meta);
96
+
97
+ // Write to console
98
+ if (this.enableConsole) {
99
+ const colored = this.colorize(level, formatted);
100
+
101
+ if (level === 'error') {
102
+ console.error(colored);
103
+ } else if (level === 'warn') {
104
+ console.warn(colored);
105
+ } else {
106
+ console.log(colored);
107
+ }
108
+ }
109
+
110
+ // Write to file
111
+ this.writeToFile(formatted);
112
+ }
113
+
114
+ error(message, meta = {}) {
115
+ this.log('error', message, meta);
116
+ }
117
+
118
+ warn(message, meta = {}) {
119
+ this.log('warn', message, meta);
120
+ }
121
+
122
+ info(message, meta = {}) {
123
+ this.log('info', message, meta);
124
+ }
125
+
126
+ debug(message, meta = {}) {
127
+ this.log('debug', message, meta);
128
+ }
129
+
130
+ trace(message, meta = {}) {
131
+ this.log('trace', message, meta);
132
+ }
133
+
134
+ // Specialized logging methods
135
+ logHardwareDetection(hardware) {
136
+ this.debug('Hardware detection completed', {
137
+ component: 'HardwareDetector',
138
+ data: {
139
+ cpu: hardware.cpu.brand,
140
+ ram: `${hardware.memory.total}GB`,
141
+ gpu: hardware.gpu.model,
142
+ architecture: hardware.cpu.architecture
143
+ }
144
+ });
145
+ }
146
+
147
+ logModelAnalysis(model, score) {
148
+ this.debug('Model compatibility analyzed', {
149
+ component: 'CompatibilityAnalyzer',
150
+ data: {
151
+ model: model.name,
152
+ score: score,
153
+ category: model.category
154
+ }
155
+ });
156
+ }
157
+
158
+ logOllamaOperation(operation, model, result) {
159
+ this.info(`Ollama ${operation} ${result ? 'succeeded' : 'failed'}`, {
160
+ component: 'OllamaClient',
161
+ data: {
162
+ operation,
163
+ model,
164
+ success: result
165
+ }
166
+ });
167
+ }
168
+
169
+ logPerformanceTest(model, results) {
170
+ this.info('Performance test completed', {
171
+ component: 'PerformanceAnalyzer',
172
+ data: {
173
+ model,
174
+ tokensPerSecond: results.tokensPerSecond,
175
+ responseTime: results.responseTime
176
+ }
177
+ });
178
+ }
179
+
180
+ logError(error, context = {}) {
181
+ this.error(error.message, {
182
+ component: context.component || 'Unknown',
183
+ error: error,
184
+ data: context.data
185
+ });
186
+ }
187
+
188
+ // Utility methods
189
+ time(label) {
190
+ const start = process.hrtime.bigint();
191
+ return {
192
+ end: () => {
193
+ const end = process.hrtime.bigint();
194
+ const duration = Number(end - start) / 1000000; // Convert to milliseconds
195
+ this.debug(`Timer: ${label} took ${duration.toFixed(2)}ms`);
196
+ return duration;
197
+ }
198
+ };
199
+ }
200
+
201
+ createChild(component) {
202
+ return {
203
+ error: (message, meta = {}) => this.error(message, { ...meta, component }),
204
+ warn: (message, meta = {}) => this.warn(message, { ...meta, component }),
205
+ info: (message, meta = {}) => this.info(message, { ...meta, component }),
206
+ debug: (message, meta = {}) => this.debug(message, { ...meta, component }),
207
+ trace: (message, meta = {}) => this.trace(message, { ...meta, component })
208
+ };
209
+ }
210
+
211
+ getLogFile() {
212
+ return this.logFile;
213
+ }
214
+
215
+ setLogFile(filePath) {
216
+ this.logFile = filePath;
217
+ this.setupLogFile();
218
+ }
219
+
220
+ setLevel(level) {
221
+ if (this.levels.hasOwnProperty(level)) {
222
+ this.level = level;
223
+ } else {
224
+ this.warn(`Invalid log level: ${level}. Valid levels: ${Object.keys(this.levels).join(', ')}`);
225
+ }
226
+ }
227
+
228
+ clearLogFile() {
229
+ if (this.logFile && fs.existsSync(this.logFile)) {
230
+ try {
231
+ fs.writeFileSync(this.logFile, '', 'utf8');
232
+ this.info('Log file cleared');
233
+ } catch (error) {
234
+ this.error('Failed to clear log file', { error });
235
+ }
236
+ }
237
+ }
238
+
239
+ rotateLogFile() {
240
+ if (!this.logFile || !fs.existsSync(this.logFile)) return;
241
+
242
+ try {
243
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
244
+ const rotatedFile = this.logFile + '.' + timestamp;
245
+
246
+ fs.renameSync(this.logFile, rotatedFile);
247
+ this.info(`Log file rotated to: ${rotatedFile}`);
248
+ } catch (error) {
249
+ this.error('Failed to rotate log file', { error });
250
+ }
251
+ }
252
+ }
253
+
254
+ // Global logger instance
255
+ let globalLogger = null;
256
+
257
+ function getLogger(options = {}) {
258
+ if (!globalLogger) {
259
+ globalLogger = new Logger(options);
260
+ }
261
+ return globalLogger;
262
+ }
263
+
264
+ function setLogger(logger) {
265
+ globalLogger = logger;
266
+ }
267
+
268
+ module.exports = {
269
+ Logger,
270
+ getLogger,
271
+ setLogger
272
+ };
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env node
2
+ // model-classifier.js
3
+ // Classifier that adds `categories` (multi) and `primary_category` (single) to each model
4
+ //
5
+ // IMPORTANT: STATIC DATABASE WITH PRE-CLASSIFIED CATEGORIES
6
+ // This system uses a static model database (177 models) with pre-classified categories.
7
+ // If you need to update the model database in the future:
8
+ // 1. Run the database update process
9
+ // 2. RE-ADD CATEGORIES: Run this classification system again on all new models
10
+ // 3. Update the cache file with the new categories
11
+ // 4. Test all use-case filters (coding, creative, reasoning, multimodal, embeddings, etc.)
12
+ //
13
+ // The classification rules below must be applied to any new models added to the database.
14
+
15
+ const fs = require('fs');
16
+
17
+ const CANON = ['coding','reasoning','creative','chat','multimodal','embeddings','safety','general'];
18
+
19
+ const CATEGORY_SYNONYMS = {
20
+ // normalize incoming "category" values
21
+ talking: 'chat',
22
+ multimodal: 'multimodal',
23
+ embeddings: 'embeddings',
24
+ reasoning: 'reasoning',
25
+ coding: 'coding',
26
+ safety: 'safety',
27
+ general: 'general',
28
+ };
29
+
30
+ const RULES = [
31
+ // embeddings - very specific patterns
32
+ {cat:'embeddings', re: /(embedding|embed|e5|bge|gte|minilm|paraphrase|text-embedding|nomic[-\s]?embed|voyage|snowflake-embed|arctic-embed|mxbai-embed)/i},
33
+
34
+ // multimodal / vision - specific vision models
35
+ {cat:'multimodal', re: /(llava|vision|vl\b|moondream|paligemma|pixtral|idefics|granite.*vision|llama3\.2[-_ ]?vision|qwen.*vl)/i},
36
+
37
+ // safety/moderation
38
+ {cat:'safety', re: /(guard|shield|moderation|safety)/i},
39
+
40
+ // coding - be more specific, exclude general models
41
+ {cat:'coding', re: /^(code|coder|codellama|starcoder|magicoder|phind[-_ ]?codellama|codeqwen|granite[-_ ]?code|yi[-_ ]?coder|opencoder|stable[-_ ]?code|deepseek[-_ ]?coder|codegemma)/i},
42
+
43
+ // reasoning / math
44
+ {cat:'reasoning', re: /(deepseek[-_ ]?r1|reason|r1\b|math|mathstral|wizard[-_ ]?math|gsm8k|mmlu|logic|phi4[-_ ]?reasoning|o1\b)/i},
45
+
46
+ // creative / RP / storytelling - look for specific creative models
47
+ {cat:'creative', re: /(dolphin(?![-_ ]?coder)|wizard(?![-_ ]?math)|mytho|synthia|airoboros|uncensored|roleplay|storyteller|creative|fiction)/i},
48
+
49
+ // chat / assistants - general conversational models
50
+ {cat:'chat', re: /(llama(?!.*code)|mistral(?!.*code)|qwen(?!.*code|.*vl)|gemma(?!.*code)|chat|assistant|hermes|openhermes|command\b|mistrallite|reflection)/i},
51
+ ];
52
+
53
+ const PRIMARY_ORDER = [
54
+ 'embeddings','safety','coding','reasoning','creative','multimodal','chat','general'
55
+ ];
56
+
57
+ function toText(model) {
58
+ return [
59
+ model.model_identifier, model.model_name,
60
+ model.description, model.detailed_description,
61
+ model.category, model.use_cases, model.input_types,
62
+ model.tags, model.labels, model.url
63
+ ]
64
+ .flat().filter(Boolean).map(String).join(' ').toLowerCase();
65
+ }
66
+
67
+ function normalizeFromCategoryField(cat) {
68
+ if (!cat) return [];
69
+ const c = CATEGORY_SYNONYMS[String(cat).toLowerCase()];
70
+ return c ? [c] : [];
71
+ }
72
+
73
+ function fromUseCases(use_cases=[]) {
74
+ const set = new Set();
75
+ for (const u of use_cases.map(x => String(x).toLowerCase())) {
76
+ if (/(coding|programming|development)/.test(u)) set.add('coding');
77
+ if (/(reasoning|mathematics|logic)/.test(u)) set.add('reasoning');
78
+ if (/(chat|conversation|assistant)/.test(u)) set.add('chat');
79
+ if (/(vision|image|multimodal)/.test(u)) set.add('multimodal');
80
+ if (/(embedding|embeddings|search|similarity)/.test(u)) set.add('embeddings');
81
+ if (/(safety|moderation)/.test(u)) set.add('safety');
82
+ if (/(creative|story|roleplay)/.test(u)) set.add('creative');
83
+ }
84
+ return [...set];
85
+ }
86
+
87
+ function fromInputTypes(input_types=[]) {
88
+ const set = new Set();
89
+ const low = input_types.map(x => String(x).toLowerCase());
90
+ // Only use input_types as hints, not definitive classification
91
+ // Most models have ['text', 'image', 'code'] automatically, so ignore generic ones
92
+ if (low.includes('image') && low.length === 1) set.add('multimodal'); // only if pure image model
93
+ if (low.includes('audio') && low.length === 1) set.add('multimodal'); // only if pure audio model
94
+ // Don't auto-classify as coding based on input_types since all models have 'code'
95
+ return [...set];
96
+ }
97
+
98
+ function regexRules(haystack) {
99
+ const set = new Set();
100
+ for (const {cat, re} of RULES) if (re.test(haystack)) set.add(cat);
101
+ return [...set];
102
+ }
103
+
104
+ function choosePrimary(cats) {
105
+ for (const c of PRIMARY_ORDER) if (cats.has(c)) return c;
106
+ return 'general';
107
+ }
108
+
109
+ function classifyModel(m) {
110
+ const hay = toText(m);
111
+ const cats = new Set([
112
+ ...normalizeFromCategoryField(m.category),
113
+ ...fromUseCases(m.use_cases),
114
+ ...fromInputTypes(m.input_types),
115
+ ...regexRules(hay),
116
+ ]);
117
+
118
+ if (cats.size === 0) cats.add('general');
119
+
120
+ // Keep only canonical labels
121
+ for (const c of [...cats]) if (!CANON.includes(c)) cats.delete(c);
122
+
123
+ const primary = choosePrimary(cats);
124
+ const categories = [...cats].sort();
125
+
126
+ // Also annotate variants (handy for tag-level filtering)
127
+ const variants = (m.variants || []).map(v => {
128
+ const vHay = [m.model_name, v.tag, v.size, v.quantization].filter(Boolean).join(' ').toLowerCase();
129
+ const vCats = new Set(categories);
130
+ regexRules(vHay).forEach(c => vCats.add(c));
131
+ return { ...v, categories: [...vCats].sort() };
132
+ });
133
+
134
+ return { ...m, categories, primary_category: primary, variants };
135
+ }
136
+
137
+ function classifyAllModels(inputData) {
138
+ const models = inputData.models || [];
139
+ const classifiedModels = models.map(classifyModel);
140
+ return { ...inputData, models: classifiedModels };
141
+ }
142
+
143
+ function run(path) {
144
+ try {
145
+ const input = JSON.parse(fs.readFileSync(path, 'utf8'));
146
+ const output = classifyAllModels(input);
147
+ console.log(JSON.stringify(output, null, 2));
148
+ } catch (error) {
149
+ if (error instanceof SyntaxError) {
150
+ console.error(`Invalid JSON in file: ${path}`, error.message);
151
+ } else {
152
+ console.error(`Error reading file: ${path}`, error.message);
153
+ }
154
+ process.exit(1);
155
+ }
156
+ }
157
+
158
+ if (require.main === module) {
159
+ const path = process.argv[2];
160
+ if (!path) {
161
+ console.error('Usage: node model-classifier.js models.json > models_with_categories.json');
162
+ process.exit(1);
163
+ }
164
+ run(path);
165
+ }
166
+
167
+ module.exports = { classifyAllModels, classifyModel };
@@ -0,0 +1,266 @@
1
+ const chalk = require('chalk');
2
+ const ora = require('ora');
3
+
4
+ /**
5
+ * Verbose Progress Reporter - Enhanced Visual Style
6
+ * Muestra operaciones paso a paso con barras de progreso y spinners
7
+ */
8
+ class VerboseProgress {
9
+ constructor(enabled = true) {
10
+ this.enabled = enabled;
11
+ this.currentStep = 0;
12
+ this.totalSteps = 0;
13
+ this.operationTitle = '';
14
+ this.startTime = null;
15
+ this.stepTimes = [];
16
+ this.currentSpinner = null;
17
+ this.stepStartTime = null;
18
+ }
19
+
20
+ /**
21
+ * Inicia una nueva operación con múltiples pasos
22
+ */
23
+ startOperation(title, totalSteps) {
24
+ if (!this.enabled) return;
25
+
26
+ this.operationTitle = title;
27
+ this.totalSteps = totalSteps;
28
+ this.currentStep = 0;
29
+ this.startTime = Date.now();
30
+ this.stepTimes = [];
31
+
32
+ console.log(''); // Espacio inicial
33
+ }
34
+
35
+ /**
36
+ * Avanza al siguiente paso de la operación
37
+ */
38
+ step(description, details = null) {
39
+ if (!this.enabled) return;
40
+
41
+ // Finalizar spinner anterior si existe (disabled)
42
+ if (this.currentSpinner) {
43
+ // this.currentSpinner.stop(); // Disabled to prevent UI issues
44
+ }
45
+
46
+ this.currentStep++;
47
+ this.stepStartTime = Date.now();
48
+
49
+ // Crear indicador de progreso visual
50
+ const progress = this.createProgressBar();
51
+ const stepIndicator = chalk.cyan(`[${this.currentStep}/${this.totalSteps}]`);
52
+
53
+ // Mostrar el paso actual
54
+ console.log(`\n${progress} ${stepIndicator} ${chalk.white.bold(description)}`);
55
+
56
+ if (details) {
57
+ console.log(` ${chalk.gray('└─ ' + details)}`);
58
+ }
59
+
60
+ // Crear spinner para este paso (disabled to fix UI issues)
61
+ this.currentSpinner = null;
62
+
63
+ this.stepTimes.push(this.stepStartTime);
64
+
65
+ return this;
66
+ }
67
+
68
+ /**
69
+ * Muestra progreso dentro de un paso (sub-operación)
70
+ */
71
+ substep(description, isLast = false) {
72
+ if (!this.enabled) return;
73
+
74
+ const connector = isLast ? '└─' : '├─';
75
+ const elapsed = this.getStepElapsedTime();
76
+ console.log(` ${chalk.gray(connector)} ${description} ${chalk.dim(`(${elapsed})`)}`);
77
+
78
+ return this;
79
+ }
80
+
81
+ /**
82
+ * Marca el paso actual como completado exitosamente
83
+ */
84
+ stepComplete(result = null, timing = null) {
85
+ if (!this.enabled) return;
86
+
87
+ if (result) {
88
+ console.log(` ${chalk.green(result)}`);
89
+ }
90
+
91
+ // Mostrar timing
92
+ const elapsed = this.getStepElapsedTime();
93
+ console.log(` ${chalk.dim(`└─ ${elapsed}`)}`);
94
+
95
+ return this;
96
+ }
97
+
98
+ /**
99
+ * Marca el paso actual como fallido
100
+ */
101
+ stepFail(error = null) {
102
+ if (!this.enabled) return;
103
+
104
+ if (error) {
105
+ console.log(` ${chalk.red(error)}`);
106
+ }
107
+
108
+ return this;
109
+ }
110
+
111
+ /**
112
+ * Muestra información adicional durante un paso
113
+ */
114
+ info(message, indent = true) {
115
+ if (!this.enabled) return;
116
+
117
+ const prefix = indent ? ' ' : '';
118
+ console.log(`${prefix}${chalk.gray(message)}`);
119
+
120
+ return this;
121
+ }
122
+
123
+ /**
124
+ * Muestra una advertencia
125
+ */
126
+ warn(message, indent = true) {
127
+ if (!this.enabled) return;
128
+
129
+ const prefix = indent ? ' ' : '';
130
+ console.log(`${prefix}${chalk.yellow(message)}`);
131
+
132
+ return this;
133
+ }
134
+
135
+ /**
136
+ * Muestra resultados o datos encontrados
137
+ */
138
+ found(message, count = null, indent = true) {
139
+ if (!this.enabled) return;
140
+
141
+ const prefix = indent ? ' ' : '';
142
+ const countStr = count !== null ? chalk.cyan.bold(` (${count})`) : '';
143
+ console.log(`${prefix}${chalk.white(message)}${countStr}`);
144
+
145
+ return this;
146
+ }
147
+
148
+ /**
149
+ * Finaliza la operación completa
150
+ */
151
+ complete(summary = null) {
152
+ if (!this.enabled) return;
153
+
154
+ // Finalizar spinner si existe (disabled)
155
+ if (this.currentSpinner) {
156
+ // this.currentSpinner.stop(); // Disabled to prevent UI issues
157
+ this.currentSpinner = null;
158
+ }
159
+
160
+ const totalTime = this.getTotalElapsedTime();
161
+
162
+ console.log(chalk.gray('─'.repeat(60)));
163
+
164
+ if (summary) {
165
+ console.log(chalk.green.bold(`${this.operationTitle} complete!`));
166
+ console.log(chalk.gray(` ${summary}`));
167
+ } else {
168
+ console.log(chalk.green.bold(`Operation complete!`));
169
+ }
170
+
171
+ console.log(chalk.dim(` Total time: ${totalTime}`));
172
+ console.log('');
173
+
174
+ return this;
175
+ }
176
+
177
+ /**
178
+ * Finaliza la operación con error
179
+ */
180
+ fail(error = null) {
181
+ if (!this.enabled) return;
182
+
183
+ // Finalizar spinner si existe (disabled)
184
+ if (this.currentSpinner) {
185
+ // this.currentSpinner.fail(); // Disabled to prevent UI issues
186
+ this.currentSpinner = null;
187
+ }
188
+
189
+ console.log(chalk.gray('─'.repeat(60)));
190
+ console.log(chalk.red.bold(`Operation failed!`));
191
+
192
+ if (error) {
193
+ console.log(chalk.red(` ${error}`));
194
+ }
195
+
196
+ console.log('');
197
+
198
+ return this;
199
+ }
200
+
201
+ /**
202
+ * Crea una barra de progreso visual
203
+ */
204
+ createProgressBar() {
205
+ if (this.totalSteps === 0) return '';
206
+
207
+ const percentage = (this.currentStep / this.totalSteps);
208
+ const filledBars = Math.floor(percentage * 20);
209
+ const emptyBars = 20 - filledBars;
210
+
211
+ const filled = chalk.cyan('█'.repeat(filledBars));
212
+ const empty = chalk.gray('░'.repeat(emptyBars));
213
+
214
+ return `${filled}${empty} ${Math.round(percentage * 100)}%`;
215
+ }
216
+
217
+ /**
218
+ * Obtiene el tiempo transcurrido desde el inicio de la operación
219
+ */
220
+ getTotalElapsedTime() {
221
+ if (!this.startTime) return '0ms';
222
+
223
+ const elapsed = Date.now() - this.startTime;
224
+
225
+ if (elapsed < 1000) return `${elapsed}ms`;
226
+ if (elapsed < 60000) return `${(elapsed / 1000).toFixed(1)}s`;
227
+
228
+ const minutes = Math.floor(elapsed / 60000);
229
+ const seconds = Math.floor((elapsed % 60000) / 1000);
230
+ return `${minutes}m ${seconds}s`;
231
+ }
232
+
233
+ /**
234
+ * Obtiene el tiempo transcurrido desde el último paso
235
+ */
236
+ getElapsedTime() {
237
+ if (this.stepTimes.length === 0) return '0ms';
238
+
239
+ const lastStepTime = this.stepTimes[this.stepTimes.length - 1];
240
+ const elapsed = Date.now() - lastStepTime;
241
+
242
+ if (elapsed < 1000) return `${elapsed}ms`;
243
+ return `${(elapsed / 1000).toFixed(1)}s`;
244
+ }
245
+
246
+ /**
247
+ * Obtiene el tiempo transcurrido desde el inicio del paso actual
248
+ */
249
+ getStepElapsedTime() {
250
+ if (!this.stepStartTime) return '0ms';
251
+
252
+ const elapsed = Date.now() - this.stepStartTime;
253
+
254
+ if (elapsed < 1000) return `${elapsed}ms`;
255
+ return `${(elapsed / 1000).toFixed(1)}s`;
256
+ }
257
+
258
+ /**
259
+ * Método estático para crear una instancia rápida
260
+ */
261
+ static create(enabled = true) {
262
+ return new VerboseProgress(enabled);
263
+ }
264
+ }
265
+
266
+ module.exports = VerboseProgress;