ops-toolkit 1.2.0 → 1.2.2

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.
@@ -0,0 +1,234 @@
1
+ import chalk from 'chalk';
2
+ import { Logger } from './logger';
3
+
4
+ /**
5
+ * 错误严重程度
6
+ */
7
+ export enum ErrorSeverity {
8
+ LOW = 'low',
9
+ MEDIUM = 'medium',
10
+ HIGH = 'high',
11
+ CRITICAL = 'critical',
12
+ }
13
+
14
+ /**
15
+ * 错误上下文信息
16
+ */
17
+ export interface ErrorContext {
18
+ command?: string;
19
+ action?: string;
20
+ userId?: string;
21
+ sessionId?: string;
22
+ timestamp?: string;
23
+ additionalInfo?: Record<string, unknown>;
24
+ }
25
+
26
+ /**
27
+ * 标准化错误报告
28
+ */
29
+ export interface ErrorReport {
30
+ id: string;
31
+ code: string;
32
+ message: string;
33
+ severity: ErrorSeverity;
34
+ context?: ErrorContext;
35
+ originalError?: Error;
36
+ stack?: string;
37
+ timestamp: string;
38
+ resolved: boolean;
39
+ }
40
+
41
+ /**
42
+ * 错误报告器
43
+ */
44
+ export class ErrorReporter {
45
+ private static reports: Map<string, ErrorReport> = new Map();
46
+
47
+ /**
48
+ * 创建错误报告
49
+ */
50
+ static createReport(
51
+ code: string,
52
+ message: string,
53
+ severity: ErrorSeverity = ErrorSeverity.MEDIUM,
54
+ context?: ErrorContext,
55
+ originalError?: Error
56
+ ): ErrorReport {
57
+ const report: ErrorReport = {
58
+ id: this.generateId(),
59
+ code,
60
+ message,
61
+ severity,
62
+ context: {
63
+ timestamp: new Date().toISOString(),
64
+ ...context,
65
+ },
66
+ originalError,
67
+ stack: originalError?.stack,
68
+ timestamp: new Date().toISOString(),
69
+ resolved: false,
70
+ };
71
+
72
+ this.reports.set(report.id, report);
73
+ return report;
74
+ }
75
+
76
+ /**
77
+ * 报告错误
78
+ */
79
+ static report(report: ErrorReport): void {
80
+ const severityColor = this.getSeverityColor(report.severity);
81
+ const icon = this.getSeverityIcon(report.severity);
82
+
83
+ console.error(chalk.red(`${icon} 错误报告 [${report.id}]`));
84
+ console.error(chalk.red(` 代码: ${report.code}`));
85
+ console.error(chalk.red(` 消息: ${report.message}`));
86
+ console.error(severityColor(` 严重程度: ${report.severity.toUpperCase()}`));
87
+ console.error(chalk.gray(` 时间: ${report.timestamp}`));
88
+
89
+ if (report.context) {
90
+ console.error(chalk.cyan(' 上下文:'));
91
+ Object.entries(report.context).forEach(([key, value]) => {
92
+ if (key !== 'timestamp') {
93
+ console.error(chalk.cyan(` ${key}: ${value}`));
94
+ }
95
+ });
96
+ }
97
+
98
+ if (report.originalError) {
99
+ console.error(chalk.yellow(' 原始错误:'));
100
+ console.error(chalk.yellow(` ${report.originalError.message}`));
101
+ }
102
+
103
+ if (process.env.DEBUG && report.stack) {
104
+ console.error(chalk.gray(' 堆栈跟踪:'));
105
+ console.error(chalk.gray(report.stack));
106
+ }
107
+
108
+ // 记录到日志
109
+ Logger.error(`错误报告 [${report.id}]: ${report.code} - ${report.message}`, report);
110
+ }
111
+
112
+ /**
113
+ * 获取错误严重程度的颜色
114
+ */
115
+ private static getSeverityColor(severity: ErrorSeverity): (text: string) => string {
116
+ switch (severity) {
117
+ case ErrorSeverity.LOW:
118
+ return chalk.blue;
119
+ case ErrorSeverity.MEDIUM:
120
+ return chalk.yellow;
121
+ case ErrorSeverity.HIGH:
122
+ return chalk.red;
123
+ case ErrorSeverity.CRITICAL:
124
+ return chalk.magenta;
125
+ default:
126
+ return chalk.white;
127
+ }
128
+ }
129
+
130
+ /**
131
+ * 获取错误严重程度的图标
132
+ */
133
+ private static getSeverityIcon(severity: ErrorSeverity): string {
134
+ switch (severity) {
135
+ case ErrorSeverity.LOW:
136
+ return '💡';
137
+ case ErrorSeverity.MEDIUM:
138
+ return '⚠️';
139
+ case ErrorSeverity.HIGH:
140
+ return '❌';
141
+ case ErrorSeverity.CRITICAL:
142
+ return '🔥';
143
+ default:
144
+ return '❓';
145
+ }
146
+ }
147
+
148
+ /**
149
+ * 生成唯一ID
150
+ */
151
+ private static generateId(): string {
152
+ return `ERR_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
153
+ }
154
+
155
+ /**
156
+ * 获取所有错误报告
157
+ */
158
+ static getAllReports(): ErrorReport[] {
159
+ return Array.from(this.reports.values());
160
+ }
161
+
162
+ /**
163
+ * 根据ID获取错误报告
164
+ */
165
+ static getReport(id: string): ErrorReport | undefined {
166
+ return this.reports.get(id);
167
+ }
168
+
169
+ /**
170
+ * 标记错误为已解决
171
+ */
172
+ static resolveReport(id: string): boolean {
173
+ const report = this.reports.get(id);
174
+ if (report) {
175
+ report.resolved = true;
176
+ Logger.info(`错误报告已解决: ${id}`);
177
+ return true;
178
+ }
179
+ return false;
180
+ }
181
+
182
+ /**
183
+ * 清理已解决的错误报告
184
+ */
185
+ static clearResolvedReports(): void {
186
+ const unresolved = Array.from(this.reports.entries()).filter(([, report]) => !report.resolved);
187
+
188
+ this.reports = new Map(unresolved);
189
+ Logger.info('已清理解决的错误报告');
190
+ }
191
+ }
192
+
193
+ /**
194
+ * 错误处理中间件
195
+ */
196
+ export function errorHandler(
197
+ error: Error,
198
+ context: ErrorContext = {},
199
+ severity: ErrorSeverity = ErrorSeverity.MEDIUM
200
+ ): void {
201
+ const report = ErrorReporter.createReport(
202
+ 'UNKNOWN_ERROR',
203
+ error.message,
204
+ severity,
205
+ context,
206
+ error
207
+ );
208
+
209
+ ErrorReporter.report(report);
210
+
211
+ // 根据严重程度决定是否退出程序
212
+ if (severity === ErrorSeverity.CRITICAL) {
213
+ Logger.error('严重错误,程序即将退出');
214
+ process.exit(1);
215
+ }
216
+ }
217
+
218
+ /**
219
+ * 异步错误处理包装器
220
+ */
221
+ export function withAsyncErrorHandling<T extends unknown[]>(
222
+ fn: (...args: T) => Promise<void>,
223
+ context: ErrorContext = {},
224
+ severity: ErrorSeverity = ErrorSeverity.MEDIUM
225
+ ) {
226
+ return async (...args: T): Promise<void> => {
227
+ try {
228
+ await fn(...args);
229
+ } catch (error) {
230
+ const err = error instanceof Error ? error : new Error(String(error));
231
+ errorHandler(err, context, severity);
232
+ }
233
+ };
234
+ }
@@ -1,3 +1,5 @@
1
1
  export * from './system';
2
2
  export * from './logger';
3
3
  export * from './config';
4
+ export * from './error-handlers';
5
+ export * from './error-reporter';
@@ -1,47 +1,386 @@
1
1
  import chalk from 'chalk';
2
2
  import figlet from 'figlet';
3
3
  import boxen from 'boxen';
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+ import os from 'os';
7
+ import type { ErrorReport } from './error-reporter';
4
8
 
5
- // 日志工具类
9
+ /**
10
+ * 日志级别
11
+ */
12
+ export enum LogLevel {
13
+ DEBUG = 0,
14
+ INFO = 1,
15
+ WARN = 2,
16
+ ERROR = 3,
17
+ CRITICAL = 4,
18
+ }
19
+
20
+ /**
21
+ * 日志条目
22
+ */
23
+ export interface LogEntry {
24
+ timestamp: string;
25
+ level: LogLevel;
26
+ message: string;
27
+ context?: Record<string, unknown>;
28
+ error?: Error;
29
+ module?: string;
30
+ }
31
+
32
+ /**
33
+ * 日志配置
34
+ */
35
+ export interface LogConfig {
36
+ level: LogLevel;
37
+ enableFileLogging: boolean;
38
+ logDirectory: string;
39
+ maxFileSize: number;
40
+ maxFiles: number;
41
+ enableConsoleColors: boolean;
42
+ enableStructuredOutput: boolean;
43
+ }
44
+
45
+ /**
46
+ * 增强的日志工具类
47
+ */
6
48
  export class Logger {
7
- private static formatMessage(level: string, message: string, color: any): string {
8
- const timestamp = new Date().toISOString();
9
- return `${color(`[${timestamp}] [${level}]`)} ${message}`;
49
+ private static config: LogConfig = {
50
+ level: LogLevel.INFO,
51
+ enableFileLogging: false,
52
+ logDirectory: path.join(os.homedir(), '.ops-toolkit', 'logs'),
53
+ maxFileSize: 10 * 1024 * 1024, // 10MB
54
+ maxFiles: 5,
55
+ enableConsoleColors: true,
56
+ enableStructuredOutput: false,
57
+ };
58
+
59
+ private static logBuffer: LogEntry[] = [];
60
+ private static maxBufferSize = 1000;
61
+
62
+ /**
63
+ * 配置日志系统
64
+ */
65
+ static configure(config: Partial<LogConfig>): void {
66
+ this.config = { ...this.config, ...config };
67
+
68
+ if (this.config.enableFileLogging) {
69
+ this.ensureLogDirectory();
70
+ }
10
71
  }
11
72
 
12
- static info(message: string): void {
13
- console.log(this.formatMessage('INFO', message, chalk.blue));
73
+ /**
74
+ * 确保日志目录存在
75
+ */
76
+ private static ensureLogDirectory(): void {
77
+ if (!fs.existsSync(this.config.logDirectory)) {
78
+ fs.mkdirSync(this.config.logDirectory, { recursive: true });
79
+ }
14
80
  }
15
81
 
16
- static success(message: string): void {
17
- console.log(this.formatMessage('SUCCESS', message, chalk.green));
82
+ /**
83
+ * 获取日志级别名称
84
+ */
85
+ private static getLevelName(level: LogLevel): string {
86
+ switch (level) {
87
+ case LogLevel.DEBUG:
88
+ return 'DEBUG';
89
+ case LogLevel.INFO:
90
+ return 'INFO';
91
+ case LogLevel.WARN:
92
+ return 'WARN';
93
+ case LogLevel.ERROR:
94
+ return 'ERROR';
95
+ case LogLevel.CRITICAL:
96
+ return 'CRITICAL';
97
+ default:
98
+ return 'UNKNOWN';
99
+ }
18
100
  }
19
101
 
20
- static warning(message: string): void {
21
- console.log(this.formatMessage('WARNING', message, chalk.yellow));
102
+ /**
103
+ * 获取日志级别颜色
104
+ */
105
+ private static getLevelColor(level: LogLevel): (text: string) => string {
106
+ if (!this.config.enableConsoleColors) {
107
+ return (text: string) => text;
108
+ }
109
+
110
+ switch (level) {
111
+ case LogLevel.DEBUG:
112
+ return chalk.gray;
113
+ case LogLevel.INFO:
114
+ return chalk.blue;
115
+ case LogLevel.WARN:
116
+ return chalk.yellow;
117
+ case LogLevel.ERROR:
118
+ return chalk.red;
119
+ case LogLevel.CRITICAL:
120
+ return chalk.magenta;
121
+ default:
122
+ return chalk.white;
123
+ }
22
124
  }
23
125
 
24
- static error(message: string): void {
25
- console.error(this.formatMessage('ERROR', message, chalk.red));
126
+ /**
127
+ * 格式化控制台消息
128
+ */
129
+ private static formatConsoleMessage(entry: LogEntry): string {
130
+ const levelName = this.getLevelName(entry.level);
131
+ const color = this.getLevelColor(entry.level);
132
+ const timestamp = entry.timestamp;
133
+
134
+ let message = `${color(`[${timestamp}] [${levelName}]`)} ${entry.message}`;
135
+
136
+ if (entry.module) {
137
+ message += chalk.gray(` (${entry.module})`);
138
+ }
139
+
140
+ if (entry.context && Object.keys(entry.context).length > 0) {
141
+ const contextStr = Object.entries(entry.context)
142
+ .map(([key, value]) => `${key}=${value}`)
143
+ .join(', ');
144
+ message += chalk.gray(` [${contextStr}]`);
145
+ }
146
+
147
+ return message;
148
+ }
149
+
150
+ /**
151
+ * 格式化文件日志消息(JSON格式)
152
+ */
153
+ private static formatFileMessage(entry: LogEntry): string {
154
+ return JSON.stringify(entry, null, 0);
26
155
  }
27
156
 
28
- static debug(message: string): void {
29
- if (process.env.NODE_ENV === 'development' || process.env.DEBUG) {
30
- console.log(this.formatMessage('DEBUG', message, chalk.gray));
157
+ /**
158
+ * 记录日志条目
159
+ */
160
+ private static logEntry(entry: LogEntry): void {
161
+ if (entry.level < this.config.level) {
162
+ return;
163
+ }
164
+
165
+ // 控制台输出
166
+ console.log(this.formatConsoleMessage(entry));
167
+
168
+ // 文件输出
169
+ if (this.config.enableFileLogging) {
170
+ this.writeToFile(entry);
171
+ }
172
+
173
+ // 添加到缓冲区
174
+ this.logBuffer.push(entry);
175
+ if (this.logBuffer.length > this.maxBufferSize) {
176
+ this.logBuffer.shift();
177
+ }
178
+
179
+ // 如果是严重错误,额外处理
180
+ if (entry.level >= LogLevel.ERROR) {
181
+ if (entry.error) {
182
+ this.handleSeriousError(entry);
183
+ }
31
184
  }
32
185
  }
33
186
 
34
- // 显示标题
187
+ /**
188
+ * 写入日志文件
189
+ */
190
+ private static writeToFile(entry: LogEntry): void {
191
+ try {
192
+ const logFile = path.join(
193
+ this.config.logDirectory,
194
+ `ops-toolkit-${this.getDateString()}.log`
195
+ );
196
+ const message = this.formatFileMessage(entry) + '\n';
197
+
198
+ // 检查文件大小
199
+ if (fs.existsSync(logFile)) {
200
+ const stats = fs.statSync(logFile);
201
+ if (stats.size > this.config.maxFileSize) {
202
+ this.rotateLogFile(logFile);
203
+ }
204
+ }
205
+
206
+ fs.appendFileSync(logFile, message, 'utf8');
207
+ } catch (error) {
208
+ console.error('写入日志文件失败:', error);
209
+ }
210
+ }
211
+
212
+ /**
213
+ * 轮转日志文件
214
+ */
215
+ private static rotateLogFile(logFile: string): void {
216
+ const baseName = logFile.replace(/\.log$/, '');
217
+
218
+ // 删除最老的日志文件
219
+ const oldestFile = `${baseName}-${this.config.maxFiles}.log`;
220
+ if (fs.existsSync(oldestFile)) {
221
+ fs.unlinkSync(oldestFile);
222
+ }
223
+
224
+ // 轮转现有文件
225
+ for (let i = this.config.maxFiles - 1; i >= 1; i--) {
226
+ const currentFile = `${baseName}-${i}.log`;
227
+ const nextFile = `${baseName}-${i + 1}.log`;
228
+ if (fs.existsSync(currentFile)) {
229
+ fs.renameSync(currentFile, nextFile);
230
+ }
231
+ }
232
+
233
+ // 移动当前文件
234
+ const firstRotatedFile = `${baseName}-1.log`;
235
+ if (fs.existsSync(logFile)) {
236
+ fs.renameSync(logFile, firstRotatedFile);
237
+ }
238
+ }
239
+
240
+ /**
241
+ * 获取日期字符串
242
+ */
243
+ private static getDateString(): string {
244
+ const dateStr = new Date().toISOString().split('T')[0];
245
+ return dateStr || new Date().toISOString().slice(0, 10);
246
+ }
247
+
248
+ /**
249
+ * 处理严重错误
250
+ */
251
+ private static handleSeriousError(entry: LogEntry): void {
252
+ if (entry.error) {
253
+ const errorReport: Partial<ErrorReport> = {
254
+ code: 'LOG_ERROR',
255
+ message: entry.message,
256
+ originalError: entry.error,
257
+ context: entry.context,
258
+ };
259
+
260
+ // 这里可以集成错误报告系统
261
+ console.error('严重错误已记录:', errorReport);
262
+ }
263
+ }
264
+
265
+ /**
266
+ * 调试日志
267
+ */
268
+ static debug(message: string, context?: Record<string, unknown>, module?: string): void {
269
+ this.logEntry({
270
+ timestamp: new Date().toISOString(),
271
+ level: LogLevel.DEBUG,
272
+ message,
273
+ context,
274
+ module,
275
+ });
276
+ }
277
+
278
+ /**
279
+ * 信息日志
280
+ */
281
+ static info(message: string, context?: Record<string, unknown>, module?: string): void {
282
+ this.logEntry({
283
+ timestamp: new Date().toISOString(),
284
+ level: LogLevel.INFO,
285
+ message,
286
+ context,
287
+ module,
288
+ });
289
+ }
290
+
291
+ /**
292
+ * 警告日志
293
+ */
294
+ static warn(message: string, context?: Record<string, unknown>, module?: string): void {
295
+ this.logEntry({
296
+ timestamp: new Date().toISOString(),
297
+ level: LogLevel.WARN,
298
+ message,
299
+ context,
300
+ module,
301
+ });
302
+ }
303
+
304
+ /**
305
+ * 警告日志(别名)
306
+ */
307
+ static warning(message: string, context?: Record<string, unknown>, module?: string): void {
308
+ this.warn(message, context, module);
309
+ }
310
+
311
+ /**
312
+ * 错误日志
313
+ */
314
+ static error(
315
+ message: string,
316
+ error?: Error | unknown,
317
+ context?: Record<string, unknown>,
318
+ module?: string
319
+ ): void {
320
+ const err = error instanceof Error ? error : undefined;
321
+ this.logEntry({
322
+ timestamp: new Date().toISOString(),
323
+ level: LogLevel.ERROR,
324
+ message,
325
+ error: err,
326
+ context: error && !err ? { error: String(error) } : context,
327
+ module,
328
+ });
329
+ }
330
+
331
+ /**
332
+ * 严重错误日志
333
+ */
334
+ static critical(
335
+ message: string,
336
+ error?: Error | unknown,
337
+ context?: Record<string, unknown>,
338
+ module?: string
339
+ ): void {
340
+ const err = error instanceof Error ? error : undefined;
341
+ this.logEntry({
342
+ timestamp: new Date().toISOString(),
343
+ level: LogLevel.CRITICAL,
344
+ message,
345
+ error: err,
346
+ context: error && !err ? { error: String(error) } : context,
347
+ module,
348
+ });
349
+ }
350
+
351
+ /**
352
+ * 成功日志(info的别名,绿色显示)
353
+ */
354
+ static success(message: string, context?: Record<string, unknown>, module?: string): void {
355
+ if (this.config.enableConsoleColors) {
356
+ const successMessage = chalk.green(message);
357
+ this.logEntry({
358
+ timestamp: new Date().toISOString(),
359
+ level: LogLevel.INFO,
360
+ message: `✅ ${successMessage}`,
361
+ context,
362
+ module,
363
+ });
364
+ } else {
365
+ this.info(`✅ ${message}`, context, module);
366
+ }
367
+ }
368
+
369
+ /**
370
+ * 显示标题
371
+ */
35
372
  static showTitle(text: string, font: string = 'Standard'): void {
36
373
  console.log(chalk.cyan(figlet.textSync(text, { font })));
37
374
  }
38
375
 
39
- // 显示框
40
- static showBox(content: string, options?: any): void {
376
+ /**
377
+ * 显示框
378
+ */
379
+ static showBox(content: string, options?: Record<string, unknown>): void {
41
380
  const boxOptions = {
42
381
  padding: 1,
43
382
  margin: 1,
44
- borderStyle: 'round',
383
+ borderStyle: 'round' as const,
45
384
  borderColor: 'cyan',
46
385
  ...options,
47
386
  };
@@ -49,14 +388,71 @@ export class Logger {
49
388
  console.log(boxen(content, boxOptions));
50
389
  }
51
390
 
52
- // 显示分隔线
391
+ /**
392
+ * 显示分隔线
393
+ */
53
394
  static separator(char: string = '-', length: number = 50): void {
54
395
  console.log(chalk.gray(char.repeat(length)));
55
396
  }
56
397
 
57
- // 显示加载动画
58
- static spinner(message: string): any {
398
+ /**
399
+ * 显示加载动画
400
+ */
401
+ static spinner(message: string): {
402
+ start: () => void;
403
+ stop: () => void;
404
+ succeed: (text?: string) => void;
405
+ fail: (text?: string) => void;
406
+ warn: (text?: string) => void;
407
+ info: (text?: string) => void;
408
+ } {
59
409
  const ora = require('ora');
60
410
  return ora(message).start();
61
411
  }
412
+
413
+ /**
414
+ * 获取日志缓冲区
415
+ */
416
+ static getLogBuffer(): LogEntry[] {
417
+ return [...this.logBuffer];
418
+ }
419
+
420
+ /**
421
+ * 清空日志缓冲区
422
+ */
423
+ static clearLogBuffer(): void {
424
+ this.logBuffer = [];
425
+ }
426
+
427
+ /**
428
+ * 设置日志级别
429
+ */
430
+ static setLevel(level: LogLevel): void {
431
+ this.config.level = level;
432
+ }
433
+
434
+ /**
435
+ * 启用文件日志
436
+ */
437
+ static enableFileLogging(directory?: string): void {
438
+ this.config.enableFileLogging = true;
439
+ if (directory) {
440
+ this.config.logDirectory = directory;
441
+ }
442
+ this.ensureLogDirectory();
443
+ }
444
+
445
+ /**
446
+ * 禁用文件日志
447
+ */
448
+ static disableFileLogging(): void {
449
+ this.config.enableFileLogging = false;
450
+ }
451
+
452
+ /**
453
+ * 获取当前配置
454
+ */
455
+ static getConfig(): LogConfig {
456
+ return { ...this.config };
457
+ }
62
458
  }