162 lines
4.2 KiB
JavaScript
Executable File
162 lines
4.2 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
/**
|
|
* error-formatter.cjs - Rich, actionable error messages for scout-block
|
|
*
|
|
* Follows CLI UX best practices: Problem + Reason + Solution
|
|
* Supports ANSI colors with NO_COLOR env var respect.
|
|
*/
|
|
|
|
const path = require('path');
|
|
|
|
// ANSI color codes
|
|
const COLORS = {
|
|
red: '\x1b[31m',
|
|
yellow: '\x1b[33m',
|
|
blue: '\x1b[34m',
|
|
cyan: '\x1b[36m',
|
|
bold: '\x1b[1m',
|
|
dim: '\x1b[2m',
|
|
reset: '\x1b[0m'
|
|
};
|
|
|
|
/**
|
|
* Check if terminal supports colors
|
|
* Respects NO_COLOR standard and FORCE_COLOR
|
|
*
|
|
* @returns {boolean}
|
|
*/
|
|
function supportsColor() {
|
|
// Respect NO_COLOR standard (https://no-color.org/)
|
|
if (process.env.NO_COLOR !== undefined) return false;
|
|
|
|
// Respect FORCE_COLOR
|
|
if (process.env.FORCE_COLOR !== undefined) return true;
|
|
|
|
// Check if stderr is TTY (we output errors to stderr)
|
|
return process.stderr.isTTY || false;
|
|
}
|
|
|
|
/**
|
|
* Apply color to text if supported
|
|
*
|
|
* @param {string} text - Text to colorize
|
|
* @param {string} color - Color name from COLORS
|
|
* @returns {string}
|
|
*/
|
|
function colorize(text, color) {
|
|
if (!supportsColor()) return text;
|
|
const colorCode = COLORS[color] || '';
|
|
return `${colorCode}${text}${COLORS.reset}`;
|
|
}
|
|
|
|
/**
|
|
* Get .ckignore config path
|
|
*
|
|
* @param {string} claudeDir - Path to .claude directory
|
|
* @param {string} [configPath] - Explicit config path to prefer
|
|
* @returns {string}
|
|
*/
|
|
function formatConfigPath(claudeDir, configPath) {
|
|
if (configPath) {
|
|
return configPath;
|
|
}
|
|
if (claudeDir) {
|
|
return path.join(claudeDir, '.ckignore');
|
|
}
|
|
return '.claude/.ckignore';
|
|
}
|
|
|
|
/**
|
|
* Format a blocked path error with actionable guidance
|
|
*
|
|
* Pattern: What went wrong → Why → How to fix → Where to configure
|
|
*
|
|
* @param {Object} details - Error details
|
|
* @param {string} details.path - The blocked path
|
|
* @param {string} details.pattern - The pattern that matched
|
|
* @param {string} details.tool - The tool that was blocked
|
|
* @param {string} details.claudeDir - Path to .claude directory
|
|
* @param {string} [details.configPath] - Explicit config path to edit
|
|
* @returns {string}
|
|
*/
|
|
function formatBlockedError(details) {
|
|
const { path: blockedPath, pattern, tool, claudeDir, configPath } = details;
|
|
const resolvedConfigPath = formatConfigPath(claudeDir, configPath);
|
|
|
|
// Truncate path if too long
|
|
const displayPath = blockedPath.length > 60
|
|
? '...' + blockedPath.slice(-57)
|
|
: blockedPath;
|
|
|
|
const lines = [
|
|
'',
|
|
colorize('NOTE:', 'cyan') + ' This is not an error - this block is intentional to optimize context.',
|
|
'',
|
|
colorize('BLOCKED', 'red') + `: Access to '${displayPath}' denied`,
|
|
'',
|
|
` ${colorize('Pattern:', 'yellow')} ${pattern}`,
|
|
` ${colorize('Tool:', 'yellow')} ${tool || 'unknown'}`,
|
|
'',
|
|
` ${colorize('To allow, add to', 'blue')} ${resolvedConfigPath}:`,
|
|
` !${pattern}`,
|
|
'',
|
|
` ${colorize('Config:', 'dim')} ${resolvedConfigPath}`,
|
|
''
|
|
];
|
|
|
|
return lines.join('\n');
|
|
}
|
|
|
|
/**
|
|
* Format a simple error message (one line, for piped output)
|
|
*
|
|
* @param {string} pattern - The pattern that matched
|
|
* @param {string} blockedPath - The path that was blocked
|
|
* @returns {string}
|
|
*/
|
|
function formatSimpleError(pattern, blockedPath) {
|
|
return `ERROR: Blocked pattern '${pattern}' matched path: ${blockedPath}`;
|
|
}
|
|
|
|
/**
|
|
* Format error for machine-readable output (exit code 2)
|
|
* Used when stderr is not a TTY
|
|
*
|
|
* @param {Object} details - Error details
|
|
* @returns {string}
|
|
*/
|
|
function formatMachineError(details) {
|
|
const { path: blockedPath, pattern, tool, claudeDir, configPath } = details;
|
|
const resolvedConfigPath = formatConfigPath(claudeDir, configPath);
|
|
|
|
return JSON.stringify({
|
|
error: 'BLOCKED',
|
|
path: blockedPath,
|
|
pattern: pattern,
|
|
tool: tool,
|
|
config: resolvedConfigPath,
|
|
fix: `Add '!${pattern}' to ${resolvedConfigPath} to allow this path`
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Format a warning message (non-blocking)
|
|
*
|
|
* @param {string} message - Warning message
|
|
* @returns {string}
|
|
*/
|
|
function formatWarning(message) {
|
|
return colorize('WARN:', 'yellow') + ' ' + message;
|
|
}
|
|
|
|
module.exports = {
|
|
formatBlockedError,
|
|
formatSimpleError,
|
|
formatMachineError,
|
|
formatWarning,
|
|
formatConfigPath,
|
|
supportsColor,
|
|
colorize,
|
|
COLORS
|
|
};
|