bktide
Version:
Command-line interface for Buildkite CI/CD workflows with rich shell completions (Fish, Bash, Zsh) and Alfred workflow integration for macOS power users
187 lines • 6.74 kB
JavaScript
/**
* CLI Error Handler - Provides error display for CLI applications
*/
import { logger } from '../services/logger.js';
import { FormatterFactory, FormatterType } from '../formatters/FormatterFactory.js';
// Default format to use for error output
let globalErrorFormat = 'plain';
/**
* Set the global error output format
* @param format The format to use ('plain', 'json', 'alfred')
*/
export function setErrorFormat(format) {
const normalizedFormat = format.toLowerCase().trim();
if (['plain', 'json', 'alfred'].includes(normalizedFormat)) {
globalErrorFormat = normalizedFormat;
logger.debug(`Error output format set to ${normalizedFormat}`);
}
else {
logger.warn(`Unknown format '${format}', error output format remains as ${globalErrorFormat}`);
}
}
/**
* Display a formatted error message
*
* @param error The error to display
* @param debug Whether to include debug information
* @param format Output format (plain, json, alfred), defaults to global setting
*/
export function displayCLIError(error, debug = false, format) {
// Use provided format or fall back to global format
const outputFormat = format || globalErrorFormat;
// Get the appropriate formatter based on format
const formatter = FormatterFactory.getFormatter(FormatterType.ERROR, outputFormat);
// Format the error using the selected formatter
const formattedError = formatter.formatError(error, { debug });
// Print the formatted output
try {
if (outputFormat === 'plain') {
// Route plain errors to stderr for CLI best practices
process.stderr.write(formattedError + '\n');
}
else {
// Machine-readable formats to stdout
process.stdout.write(formattedError + '\n');
}
}
catch {
// Fallback to logger if direct write fails
if (outputFormat === 'plain') {
logger.error(formattedError);
}
else {
logger.console(formattedError);
}
}
process.exitCode = 1;
}
/**
* Get the current global error output format
* @returns The current format
*/
export function getErrorFormat() {
return globalErrorFormat;
}
/**
* Format an error object for display in the CLI
* Extracts as much useful information as possible, including stack traces
*
* @param error The error object to format
* @param debug Whether to include debug information
* @returns Formatted error message
*/
export function formatErrorForCLI(error, debug = false) {
let output = '';
// Add a separator and heading (plain text, no inline ANSI)
output += '\nERROR\n\n';
if (error instanceof Error) {
// Handle standard Error objects
output += `${error.name}: ${error.message}\n\n`;
if (error.stack) {
const stackLines = error.stack.split('\n');
// First line usually contains the error message, which we've already displayed
const stackTrace = stackLines.slice(1).join('\n');
output += `Stack Trace:\n${stackTrace}\n\n`;
}
// Handle additional properties that might be present in API errors
const apiError = error;
if (apiError.response?.errors) {
output += 'API Errors:\n';
apiError.response.errors.forEach((e, i) => {
output += ` Error ${i + 1}: ${e.message || 'Unknown error'}\n`;
if (e.path)
output += ` Path: ${e.path.join('.')}\n`;
if (e.locations)
output += ` Locations: ${JSON.stringify(e.locations)}\n`;
output += '\n';
});
}
if (debug && apiError.request) {
output += 'Request Details:\n';
output += ` URL: ${apiError.request.url || 'N/A'}\n`;
output += ` Method: ${apiError.request.method || 'N/A'}\n\n`;
}
// If error has a cause, include it
if (apiError.cause) {
output += 'Caused by:\n';
output += formatErrorForCLI(apiError.cause, debug);
}
}
else if (error && typeof error === 'object') {
// Handle non-Error objects
try {
output += 'Error Object:\n';
output += JSON.stringify(error, null, 2) + '\n\n';
// Try to extract more detailed information
const errorObj = error;
if (errorObj.message) {
output += `Message: ${errorObj.message}\n`;
}
if (debug) {
output += 'Properties:\n';
for (const key in errorObj) {
try {
if (key !== 'stack' && key !== 'message') {
const value = errorObj[key];
output += ` ${key}: ${typeof value === 'object' ? JSON.stringify(value) : value}\n`;
}
}
catch {
output += ` ${key}: [Cannot stringify]\n`;
}
}
output += '\n';
}
}
catch {
output += 'Unable to stringify error object\n\n';
}
}
else {
// Handle primitive values
output += `${String(error)}\n\n`;
}
if (debug) {
// Add debug information
output += 'Debug Information:\n';
output += ` Timestamp: ${new Date().toISOString()}\n`;
output += ` Node Version: ${process.version}\n`;
output += ` Platform: ${process.platform} (${process.arch})\n`;
try {
// Get the current call stack
const stack = new Error().stack;
if (stack) {
const stackLines = stack.split('\n').slice(2); // Skip the Error creation line and this function
output += '\nCurrent Stack:\n';
output += stackLines.join('\n') + '\n';
}
}
catch {
// Ignore stack trace errors
}
}
// Add a closing separator
output += '\n';
return output;
}
/**
* Wraps a function with CLI error handling
*
* @param fn The function to wrap
* @param options Error handling options
* @returns A wrapped function with error handling
*/
export function withCLIErrorHandling(fn, options = {}) {
return async function (...args) {
try {
return await fn(...args);
}
catch (error) {
displayCLIError(error, options.debugMode || false, options.format);
process.exitCode = 1;
// Return void to satisfy TypeScript
return;
}
};
}
//# sourceMappingURL=cli-error-handler.js.map