@entro314labs/ai-changelog-generator
Version:
AI-powered changelog generator with MCP server support - works with most providers, online and local models
305 lines (304 loc) • 11.8 kB
JavaScript
/**
* Specialized Error Classes for AI Changelog Generator
* Provides domain-specific error handling with rich context and debugging information
* Based on the original lib-old/utils/error-handler.js
*/
import JsonUtils from './json-utils.js';
/**
* Base error class for domain-specific errors
*/
export class AIChangelogError extends Error {
constructor(message, type, context = {}, originalError = null) {
super(message);
this.name = this.constructor.name;
this.type = type;
this.context = context;
this.originalError = originalError;
this.timestamp = new Date().toISOString();
// Capture stack trace
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
toJSON() {
return {
name: this.name,
message: this.message,
type: this.type,
context: this.context,
timestamp: this.timestamp,
stack: this.stack,
originalError: this.originalError
? {
name: this.originalError.name,
message: this.originalError.message,
stack: this.originalError.stack,
}
: null,
};
}
toString() {
let str = `${this.name}: ${this.message}`;
if (Object.keys(this.context).length > 0) {
str += `\nContext: ${JsonUtils.stringifyCompact(this.context)}`;
}
if (this.originalError) {
str += `\nCaused by: ${this.originalError.name}: ${this.originalError.message}`;
}
return str;
}
}
/**
* Git operation related errors
*/
export class GitError extends AIChangelogError {
constructor(message, command, originalError = null, context = {}) {
super(message, 'git', { command, ...context }, originalError);
this.command = command;
}
static fromCommandFailure(command, exitCode, stdout, stderr, originalError = null) {
const context = {
command,
exitCode,
stdout: stdout?.trim(),
stderr: stderr?.trim(),
};
let message = `Git command failed: ${command}`;
if (exitCode) {
message += ` (exit code: ${exitCode})`;
}
if (stderr) {
message += `\nError: ${stderr}`;
}
return new GitError(message, command, originalError, context);
}
static repositoryNotFound(path) {
return new GitError(`Git repository not found at path: ${path}`, 'init', null, {
path,
reason: 'repository_not_found',
});
}
static invalidReference(ref, reason = 'unknown') {
return new GitError(`Invalid git reference: ${ref}`, 'show', null, { ref, reason });
}
static workingDirectoryDirty(files = []) {
return new GitError('Working directory has uncommitted changes', 'status', null, {
dirtyFiles: files,
reason: 'working_directory_dirty',
});
}
}
/**
* Configuration related errors
*/
export class ConfigError extends AIChangelogError {
constructor(message, configKey, originalError = null, context = {}) {
super(message, 'config', { configKey, ...context }, originalError);
this.configKey = configKey;
}
static missingRequired(configKey, source = 'configuration') {
return new ConfigError(`Required configuration missing: ${configKey}`, configKey, null, {
source,
reason: 'missing_required',
});
}
static invalidValue(configKey, value, expectedType, source = 'configuration') {
return new ConfigError(`Invalid configuration value for '${configKey}': expected ${expectedType}, got ${typeof value}`, configKey, null, { value, expectedType, actualType: typeof value, source, reason: 'invalid_type' });
}
static fileNotFound(configPath) {
return new ConfigError(`Configuration file not found: ${configPath}`, 'file', null, {
configPath,
reason: 'file_not_found',
});
}
static parseError(configPath, originalError) {
return new ConfigError(`Failed to parse configuration file: ${configPath}`, 'file', originalError, { configPath, reason: 'parse_error' });
}
static providerNotConfigured(providerName) {
return new ConfigError(`AI provider '${providerName}' is not properly configured`, `providers.${providerName}`, null, { providerName, reason: 'provider_not_configured' });
}
}
/**
* Validation related errors
*/
export class ValidationError extends AIChangelogError {
constructor(message, field, value, originalError = null, context = {}) {
super(message, 'validation', { field, value, ...context }, originalError);
this.field = field;
this.value = value;
}
static required(field) {
return new ValidationError(`Field '${field}' is required`, field, undefined, null, {
reason: 'required_field',
});
}
static invalidType(field, value, expectedType) {
return new ValidationError(`Field '${field}' must be of type ${expectedType}`, field, value, null, { expectedType, actualType: typeof value, reason: 'invalid_type' });
}
static outOfRange(field, value, min, max) {
return new ValidationError(`Field '${field}' must be between ${min} and ${max}`, field, value, null, { min, max, reason: 'out_of_range' });
}
static invalidFormat(field, value, format, example = null) {
let message = `Field '${field}' has invalid format: expected ${format}`;
if (example) {
message += ` (example: ${example})`;
}
return new ValidationError(message, field, value, null, {
format,
example,
reason: 'invalid_format',
});
}
static custom(field, value, message, context = {}) {
return new ValidationError(message, field, value, null, {
...context,
reason: 'custom_validation',
});
}
}
/**
* AI Provider related errors
*/
export class ProviderError extends AIChangelogError {
constructor(message, providerName, methodName, originalError = null, context = {}) {
super(message, 'provider', { providerName, methodName, ...context }, originalError);
this.providerName = providerName;
this.methodName = methodName;
}
static notAvailable(providerName, reason = 'unknown') {
return new ProviderError(`AI provider '${providerName}' is not available`, providerName, 'availability', null, { reason });
}
static authenticationFailed(providerName, details = {}) {
return new ProviderError(`Authentication failed for provider '${providerName}'`, providerName, 'authenticate', null, { ...details, reason: 'authentication_failed' });
}
static rateLimitExceeded(providerName, retryAfter = null) {
return new ProviderError(`Rate limit exceeded for provider '${providerName}'`, providerName, 'request', null, { retryAfter, reason: 'rate_limit_exceeded' });
}
static apiError(providerName, methodName, statusCode, response, originalError = null) {
return new ProviderError(`API error from '${providerName}': ${statusCode}`, providerName, methodName, originalError, { statusCode, response, reason: 'api_error' });
}
static invalidResponse(providerName, methodName, response, expectedFormat) {
return new ProviderError(`Invalid response format from '${providerName}': expected ${expectedFormat}`, providerName, methodName, null, { response, expectedFormat, reason: 'invalid_response' });
}
}
/**
* Abstract method errors (for incomplete implementations)
*/
export class AbstractMethodError extends AIChangelogError {
constructor(message, className, methodName, context = {}) {
super(message, 'abstract', { className, methodName, ...context });
this.className = className;
this.methodName = methodName;
}
static notImplemented(className, methodName) {
return new AbstractMethodError(`Abstract method '${methodName}' not implemented in class '${className}'`, className, methodName, { reason: 'not_implemented' });
}
}
/**
* File system related errors
*/
export class FileSystemError extends AIChangelogError {
constructor(message, operation, path, originalError = null, context = {}) {
super(message, 'filesystem', { operation, path, ...context }, originalError);
this.operation = operation;
this.path = path;
}
static fileNotFound(path) {
return new FileSystemError(`File not found: ${path}`, 'read', path, null, {
reason: 'file_not_found',
});
}
static permissionDenied(operation, path) {
return new FileSystemError(`Permission denied: cannot ${operation} ${path}`, operation, path, null, { reason: 'permission_denied' });
}
static directoryNotFound(path) {
return new FileSystemError(`Directory not found: ${path}`, 'access', path, null, {
reason: 'directory_not_found',
});
}
}
/**
* Network related errors
*/
export class NetworkError extends AIChangelogError {
constructor(message, url, operation, originalError = null, context = {}) {
super(message, 'network', { url, operation, ...context }, originalError);
this.url = url;
this.operation = operation;
}
static connectionFailed(url, originalError = null) {
return new NetworkError(`Failed to connect to ${url}`, url, 'connect', originalError, {
reason: 'connection_failed',
});
}
static timeout(url, timeoutMs) {
return new NetworkError(`Request timeout after ${timeoutMs}ms: ${url}`, url, 'request', null, {
timeoutMs,
reason: 'timeout',
});
}
static httpError(url, statusCode, statusText) {
return new NetworkError(`HTTP ${statusCode} ${statusText}: ${url}`, url, 'request', null, {
statusCode,
statusText,
reason: 'http_error',
});
}
}
/**
* Parse error for malformed data/responses
*/
export class ParseError extends Error {
constructor(message, data = null, originalError = null) {
super(message);
this.name = 'ParseError';
this.data = data;
this.originalError = originalError;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, ParseError);
}
}
}
/**
* Concurrency limit error for resource management
*/
export class ConcurrencyLimitError extends Error {
constructor(message, limit, active, originalError = null) {
super(message);
this.name = 'ConcurrencyLimitError';
this.limit = limit;
this.active = active;
this.originalError = originalError;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, ConcurrencyLimitError);
}
}
}
/**
* Error context builder utility
*/
export class ErrorContext {
constructor() {
this.data = {};
}
add(key, value) {
this.data[key] = value;
return this;
}
addGitInfo(command, exitCode, stdout, stderr) {
return this.add('git', { command, exitCode, stdout, stderr });
}
addProviderInfo(name, model, endpoint) {
return this.add('provider', { name, model, endpoint });
}
addFileInfo(path, size, mtime) {
return this.add('file', { path, size, mtime });
}
addUserInfo(action, timestamp = new Date().toISOString()) {
return this.add('user', { action, timestamp });
}
build() {
return { ...this.data };
}
}
// All error classes are exported individually with their class declarations above