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,359 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+
5
+ class ConfigManager {
6
+ constructor() {
7
+ this.configDir = path.join(os.homedir(), '.llm-checker');
8
+ this.configFile = path.join(this.configDir, 'config.json');
9
+ this.defaultConfig = this.getDefaultConfig();
10
+ this.config = null;
11
+
12
+ this.ensureConfigDirectory();
13
+ }
14
+
15
+ getDefaultConfig() {
16
+ return {
17
+ version: "2.0",
18
+ ollama: {
19
+ baseURL: process.env.OLLAMA_BASE_URL || "http://localhost:11434",
20
+ timeout: 30000,
21
+ enabled: true,
22
+ autoDetect: true,
23
+ retryAttempts: 3
24
+ },
25
+ analysis: {
26
+ includeCloudModels: false,
27
+ defaultUseCase: "general",
28
+ performanceTesting: false,
29
+ detailedHardwareInfo: false,
30
+ cacheResults: true,
31
+ cacheExpiry: 300000
32
+ },
33
+ display: {
34
+ maxModelsPerTable: 10,
35
+ showEmojis: !process.env.NO_COLOR,
36
+ colorOutput: !process.env.NO_COLOR,
37
+ compactMode: false,
38
+ showScores: true,
39
+ showInstallCommands: true
40
+ },
41
+ quantization: {
42
+ preferredLevel: "auto",
43
+ availableLevels: ["Q2_K", "Q3_K_M", "Q4_0", "Q4_K_M", "Q5_0", "Q5_K_M", "Q6_K", "Q8_0"],
44
+ hardwareBased: {
45
+ ultra_low: ["Q2_K", "Q3_K_M"],
46
+ low: ["Q4_0", "Q4_K_M"],
47
+ medium: ["Q4_K_M", "Q5_0"],
48
+ high: ["Q5_K_M", "Q6_K"],
49
+ ultra_high: ["Q8_0", "Q6_K"]
50
+ }
51
+ },
52
+ filters: {
53
+ excludeModels: [],
54
+ includeOnlyFrameworks: [],
55
+ excludeCategories: [],
56
+ minCompatibilityScore: 60,
57
+ maxModelSize: null,
58
+ yearFilter: null
59
+ },
60
+ hardware: {
61
+ overrides: {
62
+ ram: process.env.LLM_CHECKER_RAM_GB ? (isNaN(parseInt(process.env.LLM_CHECKER_RAM_GB)) ? null : parseInt(process.env.LLM_CHECKER_RAM_GB)) : null,
63
+ vram: process.env.LLM_CHECKER_VRAM_GB ? (isNaN(parseInt(process.env.LLM_CHECKER_VRAM_GB)) ? null : parseInt(process.env.LLM_CHECKER_VRAM_GB)) : null,
64
+ cpuCores: process.env.LLM_CHECKER_CPU_CORES ? (isNaN(parseInt(process.env.LLM_CHECKER_CPU_CORES)) ? null : parseInt(process.env.LLM_CHECKER_CPU_CORES)) : null,
65
+ architecture: process.env.LLM_CHECKER_ARCHITECTURE || null
66
+ },
67
+ ignoreIntegratedGPU: process.env.LLM_CHECKER_NO_GPU === 'true',
68
+ preferDedicatedGPU: true,
69
+ cacheHardwareInfo: true
70
+ },
71
+ recommendations: {
72
+ maxRecommendations: 5,
73
+ includeUpgradeSuggestions: true,
74
+ showPerformanceEstimates: true,
75
+ groupByCategory: true,
76
+ prioritizeOllamaSupport: true
77
+ },
78
+ logging: {
79
+ level: process.env.LLM_CHECKER_LOG_LEVEL || "info",
80
+ file: null,
81
+ enableDebug: process.env.DEBUG === '1',
82
+ enableVerbose: false,
83
+ saveReports: false,
84
+ reportsDirectory: path.join(os.homedir(), '.llm-checker', 'reports')
85
+ },
86
+ updates: {
87
+ checkForUpdates: true,
88
+ autoUpdateDatabase: true,
89
+ updateChannel: "stable",
90
+ notifyNewModels: true
91
+ },
92
+ customModels: []
93
+ };
94
+ }
95
+
96
+ ensureConfigDirectory() {
97
+ if (!fs.existsSync(this.configDir)) {
98
+ try {
99
+ fs.mkdirSync(this.configDir, { recursive: true });
100
+ } catch (error) {
101
+ console.warn(`Warning: Could not create config directory: ${error.message}`);
102
+ }
103
+ }
104
+ }
105
+
106
+ loadConfig() {
107
+ if (this.config) {
108
+ return this.config;
109
+ }
110
+
111
+ let userConfig = {};
112
+
113
+ // Try to load user config file
114
+ if (fs.existsSync(this.configFile)) {
115
+ try {
116
+ const configContent = fs.readFileSync(this.configFile, 'utf8');
117
+ userConfig = JSON.parse(configContent);
118
+ } catch (error) {
119
+ console.warn(`Warning: Could not parse config file: ${error.message}`);
120
+ console.warn('Using default configuration');
121
+ }
122
+ }
123
+
124
+ // Merge with defaults
125
+ this.config = this.mergeConfigs(this.defaultConfig, userConfig);
126
+
127
+ // Apply environment variable overrides
128
+ this.applyEnvironmentOverrides();
129
+
130
+ return this.config;
131
+ }
132
+
133
+ saveConfig(config = null) {
134
+ const configToSave = config || this.config || this.defaultConfig;
135
+
136
+ try {
137
+ const configContent = JSON.stringify(configToSave, null, 2);
138
+ fs.writeFileSync(this.configFile, configContent, 'utf8');
139
+ this.config = configToSave;
140
+ return true;
141
+ } catch (error) {
142
+ console.error(`Error saving config: ${error.message}`);
143
+ return false;
144
+ }
145
+ }
146
+
147
+ mergeConfigs(defaultConfig, userConfig) {
148
+ const merged = { ...defaultConfig };
149
+
150
+ for (const [key, value] of Object.entries(userConfig)) {
151
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
152
+ merged[key] = this.mergeConfigs(defaultConfig[key] || {}, value);
153
+ } else {
154
+ merged[key] = value;
155
+ }
156
+ }
157
+
158
+ return merged;
159
+ }
160
+
161
+ applyEnvironmentOverrides() {
162
+ if (!this.config) return;
163
+
164
+ // Hardware overrides (validate parsed values are valid numbers)
165
+ if (process.env.LLM_CHECKER_RAM_GB) {
166
+ const parsed = parseInt(process.env.LLM_CHECKER_RAM_GB);
167
+ if (!isNaN(parsed) && parsed > 0) this.config.hardware.overrides.ram = parsed;
168
+ }
169
+ if (process.env.LLM_CHECKER_VRAM_GB) {
170
+ const parsed = parseInt(process.env.LLM_CHECKER_VRAM_GB);
171
+ if (!isNaN(parsed) && parsed > 0) this.config.hardware.overrides.vram = parsed;
172
+ }
173
+ if (process.env.LLM_CHECKER_CPU_CORES) {
174
+ const parsed = parseInt(process.env.LLM_CHECKER_CPU_CORES);
175
+ if (!isNaN(parsed) && parsed > 0) this.config.hardware.overrides.cpuCores = parsed;
176
+ }
177
+
178
+ // Ollama overrides
179
+ if (process.env.OLLAMA_BASE_URL) {
180
+ this.config.ollama.baseURL = process.env.OLLAMA_BASE_URL;
181
+ }
182
+
183
+ // Display overrides
184
+ if (process.env.NO_COLOR) {
185
+ this.config.display.colorOutput = false;
186
+ this.config.display.showEmojis = false;
187
+ }
188
+
189
+ // Debug overrides
190
+ if (process.env.DEBUG === '1') {
191
+ this.config.logging.enableDebug = true;
192
+ this.config.logging.level = 'debug';
193
+ }
194
+
195
+ // GPU overrides
196
+ if (process.env.LLM_CHECKER_NO_GPU === 'true') {
197
+ this.config.hardware.ignoreIntegratedGPU = true;
198
+ }
199
+ }
200
+
201
+ get(keyPath, defaultValue = null) {
202
+ const config = this.loadConfig();
203
+ const keys = keyPath.split('.');
204
+ let value = config;
205
+
206
+ for (const key of keys) {
207
+ if (value && typeof value === 'object' && key in value) {
208
+ value = value[key];
209
+ } else {
210
+ return defaultValue;
211
+ }
212
+ }
213
+
214
+ return value;
215
+ }
216
+
217
+ set(keyPath, value) {
218
+ const config = this.loadConfig();
219
+ const keys = keyPath.split('.');
220
+ const lastKey = keys.pop();
221
+
222
+ let current = config;
223
+ for (const key of keys) {
224
+ if (!current[key] || typeof current[key] !== 'object') {
225
+ current[key] = {};
226
+ }
227
+ current = current[key];
228
+ }
229
+
230
+ current[lastKey] = value;
231
+ this.saveConfig(config);
232
+
233
+ return true;
234
+ }
235
+
236
+ reset() {
237
+ this.config = null;
238
+ if (fs.existsSync(this.configFile)) {
239
+ try {
240
+ fs.unlinkSync(this.configFile);
241
+ } catch (error) {
242
+ console.error(`Error removing config file: ${error.message}`);
243
+ return false;
244
+ }
245
+ }
246
+ return true;
247
+ }
248
+
249
+ exportConfig() {
250
+ const config = this.loadConfig();
251
+ return JSON.stringify(config, null, 2);
252
+ }
253
+
254
+ importConfig(configString) {
255
+ try {
256
+ const config = JSON.parse(configString);
257
+ return this.saveConfig(config);
258
+ } catch (error) {
259
+ console.error(`Error importing config: ${error.message}`);
260
+ return false;
261
+ }
262
+ }
263
+
264
+ validateConfig(config = null) {
265
+ const configToValidate = config || this.loadConfig();
266
+ const errors = [];
267
+
268
+ // Validate required sections
269
+ const requiredSections = ['ollama', 'analysis', 'display', 'hardware'];
270
+ for (const section of requiredSections) {
271
+ if (!configToValidate[section]) {
272
+ errors.push(`Missing required section: ${section}`);
273
+ }
274
+ }
275
+
276
+ // Validate Ollama URL
277
+ if (configToValidate.ollama?.baseURL) {
278
+ try {
279
+ new URL(configToValidate.ollama.baseURL);
280
+ } catch (error) {
281
+ errors.push(`Invalid Ollama URL: ${configToValidate.ollama.baseURL}`);
282
+ }
283
+ }
284
+
285
+ // Validate numeric values
286
+ if (configToValidate.ollama?.timeout && configToValidate.ollama.timeout < 1000) {
287
+ errors.push('Ollama timeout should be at least 1000ms');
288
+ }
289
+
290
+ if (configToValidate.display?.maxModelsPerTable && configToValidate.display.maxModelsPerTable < 1) {
291
+ errors.push('maxModelsPerTable should be at least 1');
292
+ }
293
+
294
+ return {
295
+ valid: errors.length === 0,
296
+ errors
297
+ };
298
+ }
299
+
300
+ getConfigPath() {
301
+ return this.configFile;
302
+ }
303
+
304
+ getConfigDirectory() {
305
+ return this.configDir;
306
+ }
307
+
308
+ createBackup() {
309
+ if (!fs.existsSync(this.configFile)) {
310
+ return null;
311
+ }
312
+
313
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
314
+ const backupFile = path.join(this.configDir, `config.backup.${timestamp}.json`);
315
+
316
+ try {
317
+ fs.copyFileSync(this.configFile, backupFile);
318
+ return backupFile;
319
+ } catch (error) {
320
+ console.error(`Error creating backup: ${error.message}`);
321
+ return null;
322
+ }
323
+ }
324
+
325
+ listBackups() {
326
+ try {
327
+ const files = fs.readdirSync(this.configDir);
328
+ return files
329
+ .filter(file => file.startsWith('config.backup.') && file.endsWith('.json'))
330
+ .map(file => ({
331
+ name: file,
332
+ path: path.join(this.configDir, file),
333
+ created: fs.statSync(path.join(this.configDir, file)).mtime
334
+ }))
335
+ .sort((a, b) => b.created - a.created);
336
+ } catch (error) {
337
+ console.error(`Error listing backups: ${error.message}`);
338
+ return [];
339
+ }
340
+ }
341
+
342
+ restoreBackup(backupName) {
343
+ const backupPath = path.join(this.configDir, backupName);
344
+
345
+ if (!fs.existsSync(backupPath)) {
346
+ throw new Error(`Backup file not found: ${backupName}`);
347
+ }
348
+
349
+ try {
350
+ fs.copyFileSync(backupPath, this.configFile);
351
+ this.config = null; // Force reload
352
+ return true;
353
+ } catch (error) {
354
+ throw new Error(`Error restoring backup: ${error.message}`);
355
+ }
356
+ }
357
+ }
358
+
359
+ module.exports = ConfigManager;
@@ -0,0 +1,315 @@
1
+ const chalk = require('chalk');
2
+
3
+ class OutputFormatter {
4
+ constructor(options = {}) {
5
+ this.useColors = options.colors !== false && !process.env.NO_COLOR;
6
+ this.useEmojis = options.emojis !== false && !process.env.NO_EMOJI;
7
+ this.compact = options.compact || false;
8
+ }
9
+
10
+ formatSystemInfo(hardware) {
11
+ const lines = [];
12
+
13
+ if (!this.compact) {
14
+ lines.push(this.header('System Information'));
15
+ }
16
+
17
+ lines.push(this.info('CPU', `${hardware.cpu.brand} (${hardware.cpu.cores} cores, ${hardware.cpu.speed || 'Unknown'}GHz)`));
18
+ lines.push(this.info('Architecture', hardware.cpu.architecture));
19
+ lines.push(this.info('RAM', `${hardware.memory.total}GB total (${hardware.memory.free}GB free, ${hardware.memory.usagePercent}% used)`));
20
+ lines.push(this.info('GPU', hardware.gpu.model || 'Not detected'));
21
+ const vramDisplay = hardware.gpu.vram === 0 && hardware.gpu.model && hardware.gpu.model.toLowerCase().includes('apple')
22
+ ? 'Unified Memory'
23
+ : `${hardware.gpu.vram || 'N/A'}GB`;
24
+ lines.push(this.info('VRAM', `${vramDisplay}${hardware.gpu.dedicated ? ' (Dedicated)' : ' (Integrated)'}`));
25
+ lines.push(this.info('OS', `${hardware.os.distro} ${hardware.os.release} (${hardware.os.arch})`));
26
+
27
+ return lines.join('\n');
28
+ }
29
+
30
+ formatCompatibilityResults(results) {
31
+ const lines = [];
32
+
33
+ if (results.compatible.length > 0) {
34
+ lines.push(this.success('Compatible Models (Score ≥ 75)'));
35
+ lines.push(this.formatModelsTable(results.compatible, 'compatible'));
36
+ }
37
+
38
+ if (results.marginal.length > 0) {
39
+ lines.push(this.warning('Marginal Performance (Score 60-74)'));
40
+ lines.push(this.formatModelsTable(results.marginal, 'marginal'));
41
+ }
42
+
43
+ if (results.incompatible.length > 0 && !this.compact) {
44
+ lines.push(this.error('Incompatible Models (showing top 5)'));
45
+ lines.push(this.formatModelsTable(results.incompatible.slice(0, 5), 'incompatible'));
46
+ }
47
+
48
+ return lines.join('\n\n');
49
+ }
50
+
51
+ formatModelsTable(models, type) {
52
+ if (models.length === 0) return '';
53
+
54
+ const headers = ['Model', 'Size', 'Score', 'RAM', 'VRAM', 'Speed'];
55
+ const rows = [headers];
56
+
57
+ models.slice(0, this.compact ? 5 : 10).forEach(model => {
58
+ const scoreText = `${model.score || 0}/100`;
59
+ const scoreColored = this.scoreColor(model.score || 0, scoreText);
60
+
61
+ rows.push([
62
+ this.truncate(model.name, 20),
63
+ model.size || 'Unknown',
64
+ scoreColored,
65
+ `${model.requirements?.ram || '?'}GB`,
66
+ `${model.requirements?.vram || 0}GB`,
67
+ this.formatSpeed(model.performance?.speed)
68
+ ]);
69
+ });
70
+
71
+ return this.createTable(rows);
72
+ }
73
+
74
+ formatRecommendations(recommendations) {
75
+ if (!recommendations || recommendations.length === 0) {
76
+ return '';
77
+ }
78
+
79
+ const lines = [this.header('Recommendations')];
80
+
81
+ recommendations.forEach((rec, index) => {
82
+ lines.push(`${index + 1}. ${rec}`);
83
+ });
84
+
85
+ return lines.join('\n');
86
+ }
87
+
88
+ formatOllamaStatus(ollamaInfo) {
89
+ if (!ollamaInfo) return '';
90
+
91
+ const status = ollamaInfo.available ?
92
+ this.success(`Running (v${ollamaInfo.version || 'unknown'})`) :
93
+ this.error(`${ollamaInfo.error || 'Not available'}`);
94
+
95
+ let result = this.info('Ollama Status', status);
96
+
97
+ if (ollamaInfo.available) {
98
+ result += '\n' + this.info('Local Models', `${ollamaInfo.localModels || 0} installed`);
99
+ if (ollamaInfo.runningModels > 0) {
100
+ result += '\n' + this.info('Running Models', ollamaInfo.runningModels);
101
+ }
102
+ }
103
+
104
+ return result;
105
+ }
106
+
107
+ formatHardwareTier(tier, score) {
108
+ const tierFormatted = tier.replace('_', ' ').toUpperCase();
109
+ const tierColored = this.tierColor(tier, tierFormatted);
110
+ return this.info('Hardware Tier', `${tierColored} (Overall Score: ${score}/100)`);
111
+ }
112
+
113
+ formatPerformanceBenchmark(benchmark) {
114
+ if (!benchmark) return '';
115
+
116
+ const lines = [this.header('Performance Benchmark')];
117
+ lines.push(this.info('CPU Score', `${benchmark.cpu}/100`));
118
+ lines.push(this.info('Memory Score', `${benchmark.memory}/100`));
119
+ lines.push(this.info('Overall Score', `${benchmark.overall}/100`));
120
+
121
+ return lines.join('\n');
122
+ }
123
+
124
+ formatInstallCommands(commands) {
125
+ if (!commands || commands.length === 0) return '';
126
+
127
+ const lines = [this.header('Quick Install Commands')];
128
+
129
+ commands.forEach(cmd => {
130
+ const status = cmd.isInstalled ?
131
+ this.success('Installed') :
132
+ this.dim('Not installed');
133
+ lines.push(`${status} ${this.highlight(cmd.command)}`);
134
+ });
135
+
136
+ return lines.join('\n');
137
+ }
138
+
139
+ formatUpgradeSuggestions(suggestions) {
140
+ if (!suggestions || suggestions.length === 0) return '';
141
+
142
+ const lines = [this.header('Hardware Upgrade Suggestions')];
143
+
144
+ suggestions.forEach((suggestion, index) => {
145
+ lines.push(`${index + 1}. ${suggestion}`);
146
+ });
147
+
148
+ return lines.join('\n');
149
+ }
150
+
151
+ formatNextSteps(steps) {
152
+ if (!steps || steps.length === 0) return '';
153
+
154
+ const lines = [this.header('Next Steps')];
155
+
156
+ steps.forEach((step, index) => {
157
+ lines.push(`${index + 1}. ${step}`);
158
+ });
159
+
160
+ return lines.join('\n');
161
+ }
162
+
163
+ // Helper methods for styling
164
+ header(text) {
165
+ return this.useColors ? chalk.blue.bold(text) : text;
166
+ }
167
+
168
+ success(text) {
169
+ return this.useColors ? chalk.green(text) : text;
170
+ }
171
+
172
+ warning(text) {
173
+ return this.useColors ? chalk.yellow(text) : text;
174
+ }
175
+
176
+ error(text) {
177
+ return this.useColors ? chalk.red(text) : text;
178
+ }
179
+
180
+ info(label, value) {
181
+ const labelFormatted = this.useColors ? chalk.cyan(label + ':') : label + ':';
182
+ return `${labelFormatted} ${value}`;
183
+ }
184
+
185
+ highlight(text) {
186
+ return this.useColors ? chalk.cyan(text) : text;
187
+ }
188
+
189
+ dim(text) {
190
+ return this.useColors ? chalk.gray(text) : text;
191
+ }
192
+
193
+ scoreColor(score, text) {
194
+ if (!this.useColors) return text;
195
+
196
+ if (score >= 90) return chalk.green(text);
197
+ if (score >= 75) return chalk.yellow(text);
198
+ if (score >= 60) return chalk.orange(text);
199
+ return chalk.red(text);
200
+ }
201
+
202
+ tierColor(tier, text) {
203
+ if (!this.useColors) return text;
204
+
205
+ switch (tier) {
206
+ case 'ultra_high': return chalk.magenta(text);
207
+ case 'high': return chalk.green(text);
208
+ case 'medium': return chalk.yellow(text);
209
+ case 'low': return chalk.orange(text);
210
+ case 'ultra_low': return chalk.red(text);
211
+ default: return text;
212
+ }
213
+ }
214
+
215
+ formatSpeed(speed) {
216
+ const speedMap = {
217
+ 'very_fast': 'Very Fast',
218
+ 'fast': 'Fast',
219
+ 'medium': 'Medium',
220
+ 'slow': 'Slow',
221
+ 'very_slow': 'Very Slow'
222
+ };
223
+
224
+ return speedMap[speed] || speed || 'Unknown';
225
+ }
226
+
227
+ truncate(text, maxLength) {
228
+ if (!text) return '';
229
+ return text.length > maxLength ? text.substring(0, maxLength - 3) + '...' : text;
230
+ }
231
+
232
+ createTable(rows) {
233
+ if (rows.length === 0) return '';
234
+
235
+ // Calculate column widths
236
+ const widths = rows[0].map((_, colIndex) =>
237
+ Math.max(...rows.map(row => this.stripAnsi(row[colIndex] || '').length))
238
+ );
239
+
240
+ const lines = [];
241
+
242
+ rows.forEach((row, rowIndex) => {
243
+ const paddedRow = row.map((cell, colIndex) => {
244
+ const cellText = cell || '';
245
+ const padding = widths[colIndex] - this.stripAnsi(cellText).length;
246
+ return cellText + ' '.repeat(Math.max(0, padding));
247
+ });
248
+
249
+ lines.push('│ ' + paddedRow.join(' │ ') + ' │');
250
+
251
+ // Add separator after header
252
+ if (rowIndex === 0) {
253
+ const separator = '├' + widths.map(w => '─'.repeat(w + 2)).join('┼') + '┤';
254
+ lines.push(separator);
255
+ }
256
+ });
257
+
258
+ // Add top and bottom borders
259
+ const topBorder = '┌' + widths.map(w => '─'.repeat(w + 2)).join('┬') + '┐';
260
+ const bottomBorder = '└' + widths.map(w => '─'.repeat(w + 2)).join('┴') + '┘';
261
+
262
+ return [topBorder, ...lines, bottomBorder].join('\n');
263
+ }
264
+
265
+ stripAnsi(text) {
266
+ // Simple ANSI escape code removal
267
+ return text.replace(/\x1b\[[0-9;]*m/g, '');
268
+ }
269
+
270
+ formatJSON(data, pretty = true) {
271
+ return JSON.stringify(data, null, pretty ? 2 : 0);
272
+ }
273
+
274
+ formatCSV(data, headers) {
275
+ const lines = [];
276
+
277
+ if (headers) {
278
+ lines.push(headers.join(','));
279
+ }
280
+
281
+ data.forEach(row => {
282
+ const csvRow = row.map(cell => {
283
+ const cellStr = String(cell || '');
284
+ // Escape quotes and wrap in quotes if necessary
285
+ if (cellStr.includes(',') || cellStr.includes('"') || cellStr.includes('\n')) {
286
+ return '"' + cellStr.replace(/"/g, '""') + '"';
287
+ }
288
+ return cellStr;
289
+ });
290
+ lines.push(csvRow.join(','));
291
+ });
292
+
293
+ return lines.join('\n');
294
+ }
295
+
296
+ formatMarkdown(data) {
297
+ // Simple markdown table formatting
298
+ if (!data || data.length === 0) return '';
299
+
300
+ const headers = data[0];
301
+ const rows = data.slice(1);
302
+
303
+ const lines = [];
304
+ lines.push('| ' + headers.join(' | ') + ' |');
305
+ lines.push('| ' + headers.map(() => '---').join(' | ') + ' |');
306
+
307
+ rows.forEach(row => {
308
+ lines.push('| ' + row.join(' | ') + ' |');
309
+ });
310
+
311
+ return lines.join('\n');
312
+ }
313
+ }
314
+
315
+ module.exports = OutputFormatter;