init
This commit is contained in:
161
.opencode/plugin/scout-block/error-formatter.cjs
Executable file
161
.opencode/plugin/scout-block/error-formatter.cjs
Executable file
@@ -0,0 +1,161 @@
|
||||
#!/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
|
||||
};
|
||||
Reference in New Issue
Block a user