diff --git a/README.md b/README.md index d488c25..6fac93e 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,8 @@ - šŸ”„ **Live Monitoring** – Catch errors in real-time using the `errlens run` command. - 🧠 **Fuzzy Logic Engine** – Matches messy stack traces and typos using `Fuse.js`. - šŸŽØ **Beautiful UI** – High-visibility terminal output powered by `boxen` and `chalk`. +- šŸŽ­ **Color Themes** – 5 built-in themes (`default`, `minimal`, `high-contrast`, `dracula`, `monokai`). +- šŸ” **Verbose Mode** – Extra diagnostic details with `--verbose` for debugging custom patterns. - šŸ¤– **CI/CD Ready** – Export raw data via `--json` for automated error reporting. --- @@ -84,6 +86,12 @@ errlens analyze "Cannot read properties of undefined" --lang hi # Run a script with output in Spanish errlens run app.js --lang es + +# Use a color theme +errlens analyze "is not a function" --theme dracula + +# Show verbose diagnostic info +errlens analyze "Cannot read properties of undefined" --verbose ``` --- @@ -93,12 +101,21 @@ errlens run app.js --lang es ### Available Commands ```bash -errlens run [--json] # Run a script and analyze any crashes -errlens analyze [--json] # Analyze a specific error message -errlens --version # Show version information -errlens --help # Show help +errlens run [options] # Run a script and analyze any crashes +errlens analyze [options] # Analyze a specific error message +errlens --version # Show version information +errlens --help # Show help ``` +### Options + +| Flag | Description | +|------|-------------| +| `--json` | Output raw JSON (no colors, no boxes) — perfect for CI/CD pipelines | +| `--theme ` | Pick a color theme (see the Color Themes section below) | +| `--verbose` | Show extra diagnostic info for each matched error | +| `--lang ` | Output language (e.g. `hi`, `es`, `fr`) — available on `run` and `analyze` | + ### 1ļøāƒ£ Automatic Monitoring (The "Pro" Way) Run your script through ErrLens. If it crashes, ErrLens intercepts the error and explains the fix before the process exits. @@ -176,7 +193,61 @@ This follows Unix conventions where `0` means success and non-zero means failure --- -### 4ļøāƒ£ Multilingual Support +### 4ļøāƒ£ Color Themes + +ErrLens ships with several built-in color themes. Use the `--theme` flag to switch between them: + +```bash +errlens analyze "TypeError: Cannot read properties of undefined" --theme dracula +errlens run app.js --theme high-contrast +errlens analyze "is not a function" --theme monokai +``` + +#### Available Themes + +| Theme | Description | +|-------|-------------| +| `default` | Cyan-bordered, colorful theme (the classic ErrLens look) | +| `minimal` | Grayscale — clean, distraction-free output | +| `high-contrast` | Bright colors on dark terminals for accessibility | +| `dracula` | Magenta/pink palette inspired by the Dracula color scheme | +| `monokai` | Yellow/green palette inspired by Monokai | + +> **Tip:** Combine `--theme` with `--verbose` for the most detailed output: +> ```bash +> errlens analyze "Cannot read properties of undefined" --theme dracula --verbose +> ``` + +When using `--json`, themes are ignored — you always get clean machine-readable output. + +#### Runtime Injection Themes + +When using `errlens/injector` or `errlens/auto` in your own code, set themes via environment variables: + +```bash +ERRLENS_THEME=monokai ERRLENS_VERBOSE=1 node app.js +``` + +--- + +### 5ļøāƒ£ Verbose Mode + +Add `--verbose` to see extra diagnostic details alongside each error match: + +```bash +errlens analyze "Cannot read properties of undefined" --verbose +``` + +Verbose output adds: +- **MATCH:** the exact substring that triggered the database lookup +- **LENGTH:** character count of the match phrase +- **KEY:** the internal database key used + +This is useful when debugging custom error patterns or contributing new entries to the database. + +--- + +### 6ļøāƒ£ Multilingual Support ErrLens supports error explanations in multiple languages using the `--lang` flag. @@ -230,7 +301,9 @@ errlens/ ā”œā”€ā”€ lib/ │ ā”œā”€ā”€ matcher.js # Fuzzy search & Logic engine │ ā”œā”€ā”€ formatter.js # UI & Terminal styling +│ ā”œā”€ā”€ themes.js # Color theme definitions & resolver │ ā”œā”€ā”€ auto.js # Automation & Error interception +│ ā”œā”€ā”€ injector.js # Runtime injection with theme support │ └── database.json # The "Knowledge Base" (Dictionary) ā”œā”€ā”€ package.json # Dependencies & Metadata └── README.md # Documentation diff --git a/bin/index.js b/bin/index.js index 5c2b6b4..c134573 100644 --- a/bin/index.js +++ b/bin/index.js @@ -1,10 +1,13 @@ #!/usr/bin/env node +"use strict"; + const { Command } = require("commander"); const { spawn } = require("child_process"); const path = require("path"); const { findError } = require("../lib/matcher"); const { formatError } = require("../lib/formatter"); +const { THEME_NAMES, isValidTheme, resolveTheme } = require("../lib/themes"); const { version } = require("../package.json"); const program = new Command(); @@ -13,19 +16,43 @@ program .name("errlens") .description("Professional JS Error Analytics") .version(version) - .option("--json", "Output JSON instead of pretty UI"); // āœ… GLOBAL OPTION + .option("--json", "Output JSON instead of pretty UI") + .option( + "--theme ", + `Color theme (${THEME_NAMES.join(", ")})`, + "default" + ) + .option("--verbose", "Show extra diagnostic info for each match"); + +// ----------------- HELPERS ----------------- +function validateTheme(name) { + if (name && !isValidTheme(name)) { + console.error( + `Unknown theme "${name}". Available: ${THEME_NAMES.join(", ")}` + ); + process.exit(1); + } +} + +function sanitizeMatches(matches) { + return matches.map(({ matchedText, ...rest }) => rest); +} // ----------------- RUN COMMAND ----------------- program .command("run ") .description("Run a Javascript file and analyze crashes") .option("--lang ", "output language (e.g. hi, es, fr)", "en") - - .action(async (file,options) => { + .action(async (file, options) => { const { default: ora } = await import("ora"); const { default: chalk } = await import("chalk"); - + const isJson = Boolean(program.opts().json); + const themeName = program.opts().theme; + const verbose = Boolean(program.opts().verbose); + + validateTheme(themeName); + const filePath = path.resolve(process.cwd(), file); const spinner = isJson ? null @@ -37,7 +64,6 @@ program let errorOutput = ""; - // Stream stdout only in pretty mode child.stdout.on("data", (data) => { if (!isJson) { spinner.stop(); @@ -46,10 +72,8 @@ program } }); - // Capture stderr (DO NOT print in JSON mode) child.stderr.on("data", (data) => { errorOutput += data.toString(); - if (!isJson) { process.stderr.write(data); } @@ -60,43 +84,51 @@ program spinner.stop(); } - const { count, matches } = findError(errorOutput,options.lang); + const { count, matches } = findError(errorOutput, options.lang); - // Process killed by signal if (code === null) { - const result = { code: 1, count, matches }; - if (isJson) { - console.log(JSON.stringify(result, null, 2)); + console.log( + JSON.stringify({ code: 1, count, matches: sanitizeMatches(matches) }, null, 2) + ); } else { + const theme = resolveTheme(themeName); console.log( - chalk.red.bold(`\nāš ļø Process killed by signal: ${signal}`) + chalk[theme.signalText].bold(`\nāš ļø Process killed by signal: ${signal}`) ); } process.exit(1); } - // JSON MODE if (isJson) { - console.log(JSON.stringify({ code, count, matches }, null, 2)); + console.log( + JSON.stringify({ code, count, matches: sanitizeMatches(matches) }, null, 2) + ); process.exit(code ?? 1); } - // PRETTY MODE + const theme = resolveTheme(themeName); + if (code === 0) { - console.log(chalk.green.bold("\n✨ Process finished successfully.")); + console.log(chalk[theme.successText].bold("\n✨ Process finished successfully.")); } else { if (count > 0) { console.log( - chalk.bold.cyan(`\nšŸš€ ErrLens Analysis (${count} Issue(s)):`) + chalk[theme.headerText].bold(`\nšŸš€ ErrLens Analysis (${count} Issue(s)):`) + ); + matches.forEach((m) => + console.log( + formatError(m, { theme: themeName, verbose }) + ) ); - matches.forEach((m) => console.log(formatError(m))); } else { console.log( - chalk.red.bold("\nāŒ Crash detected (No known fix in database):") + chalk[theme.unknownText].bold( + "\nāŒ Crash detected (No known fix in database):" + ) ); - console.log(chalk.gray(errorOutput)); + console.log(chalk[theme.rawText](errorOutput)); } } @@ -104,13 +136,14 @@ program }); child.on("error", (err) => { - const result = { code: 1, count: 0, matches: [] }; - if (isJson) { - console.log(JSON.stringify(result, null, 2)); + console.log( + JSON.stringify({ code: 1, count: 0, matches: [] }, null, 2) + ); } else { if (spinner) spinner.stop(); - console.log(chalk.red(`System Error: ${err.message}`)); + const theme = resolveTheme(themeName); + console.log(chalk[theme.errorText](`System Error: ${err.message}`)); } process.exit(1); @@ -118,32 +151,51 @@ program }); // ----------------- ANALYZE COMMAND ----------------- - program +program .command("analyze ") .description("Analyze a specific error string") .option("--lang ", "output language (e.g. hi, es, fr)", "en") .action(async (errorString, options) => { const { default: chalk } = await import("chalk"); const isJson = Boolean(program.opts().json); + const themeName = program.opts().theme; + const verbose = Boolean(program.opts().verbose); + + validateTheme(themeName); + const { count, matches } = findError(errorString, options.lang); const exitCode = count > 0 ? 1 : 0; if (isJson) { - console.log(JSON.stringify({ code: exitCode, count, matches }, null, 2)); + console.log( + JSON.stringify({ code: exitCode, count, matches: sanitizeMatches(matches) }, null, 2) + ); process.exit(exitCode); } if (count > 0) { - console.log(chalk.bold.cyan(`\nšŸš€ ErrLens Analysis (${count} Issue(s)):`)); - matches.forEach((m) => console.log(formatError(m))); + const theme = resolveTheme(themeName); + console.log( + chalk[theme.headerText].bold(`\nšŸš€ ErrLens Analysis (${count} Issue(s)):`) + ); + matches.forEach((m) => + console.log(formatError(m, { theme: themeName, verbose })) + ); } else { - console.log(chalk.red.bold("\nāŒ Crash detected (No known fix in database):")); - console.log(chalk.gray(errorString)); + const theme = resolveTheme(themeName); + console.log( + chalk[theme.unknownText].bold( + "\nāŒ Crash detected (No known fix in database):" + ) + ); + console.log(chalk[theme.rawText](errorString)); } process.exit(exitCode); }); - // ----------------- PARSE ----------------- -program.parse(process.argv); \ No newline at end of file +program.parseAsync(process.argv).catch((err) => { + console.error(err.message || err); + process.exit(1); +}); diff --git a/lib/auto.js b/lib/auto.js index 235ba89..ba051d4 100644 --- a/lib/auto.js +++ b/lib/auto.js @@ -1,19 +1,40 @@ +"use strict"; + const { findError } = require("./matcher"); const { formatError } = require("./formatter"); -const chalk = require("chalk"); +const { resolveTheme } = require("./themes"); +const chalkImport = require("chalk"); +const chalk = chalkImport.default || chalkImport; + +const themeName = process.env.ERRLENS_THEME || "default"; +const verbose = process.env.ERRLENS_VERBOSE === "1"; +const theme = resolveTheme(themeName); + +function safeStringify(err) { + try { + if (err !== null && typeof err === "object") { + return err.stack || err.message || String(err); + } + return String(err ?? "unknown error"); + } catch { + return "unknown error"; + } +} process.on("uncaughtException", (err) => { - // Use the message for the search, guard against non-Error throws - const msg = (err && err.message) ? err.message : String(err); + const msg = safeStringify(err); const { count, matches } = findError(msg); - + if (count > 0) { - console.log(chalk.cyan(`\nšŸ’” ErrLens detected ${count} issue(s):`)); - matches.forEach(match => console.log(formatError(match))); + console.log( + chalk[theme.infoLabel]( `\nšŸ’” ErrLens detected ${count} issue(s):`) + ); + matches.forEach((match) => + console.log(formatError(match, { theme: themeName, verbose })) + ); } else { - // If no match, show the raw error so you know what happened - console.error(chalk.red.bold("\n--- Raw System Error ---")); + console.error(chalk[theme.errorText].bold("\n--- Raw System Error ---")); console.error(err); } process.exit(1); -}); \ No newline at end of file +}); diff --git a/lib/formatter.js b/lib/formatter.js index e1fcf49..302a393 100644 --- a/lib/formatter.js +++ b/lib/formatter.js @@ -1,32 +1,59 @@ +"use strict"; + const chalkImport = require("chalk"); const chalk = chalkImport.default || chalkImport; const boxen = require("boxen"); +const { resolveTheme } = require("./themes"); + +function formatError(error, options = {}) { + const theme = resolveTheme(options.theme); + const verbose = Boolean(options.verbose); + + const fixList = error.fixes + .map((f) => chalk[theme.fixCheck](` āœ” ${f}`)) + .join("\n"); + + const explanationBlock = ` +${chalk[theme.whatLabel].bold("šŸ” WHAT:")} ${error.explanation}`; + + const whyBlock = ` +${chalk[theme.whyLabel].bold("šŸ’„ WHY:")} ${error.why}`; + + const fixesBlock = ` +${chalk[theme.fixesLabel].bold("āœ… FIXES:")} +${fixList}`; -function formatError(error) { - const fixList = error.fixes.map(f => chalk.green(` āœ” ${f}`)).join("\n"); + const exampleBlock = ` +${chalk[theme.exampleLabel].bold("šŸ“˜ EXAMPLE:")} +${chalk[theme.exampleCode](error.example)}`; - const content = ` -${chalk.cyan.bold("ID:")} ${error.name} + let content = ` +${chalk[theme.id].bold("ID:")} ${error.name} +${explanationBlock} +${whyBlock} +${fixesBlock} +${exampleBlock}`; -${chalk.yellow.bold("šŸ” WHAT:")} ${error.explanation} -${chalk.magenta.bold("šŸ’„ WHY:")} ${error.why} + if (verbose) { + const matchBlock = ` +${chalk[theme.id].bold("šŸ”Ž MATCH:")} ${chalk[theme.exampleCode](error.matchedText || error.match)}`; -${chalk.green.bold("āœ… FIXES:")} -${fixList} + const metaBlock = ` +${chalk[theme.id].bold("šŸ“ LENGTH:")} ${(error.matchedText || error.match).length} chars +${chalk[theme.id].bold("šŸ·ļø KEY:")} ${error.match}`; -${chalk.blue.bold("šŸ“˜ EXAMPLE:")} -${chalk.gray(error.example)} - `; + content += matchBlock + metaBlock; + } return boxen(content, { padding: 1, margin: { top: 1, bottom: 1 }, borderStyle: "round", - borderColor: "cyan", - title: chalk.bold("šŸ” ErrLens Analysis"), + borderColor: theme.border, + title: chalk[theme.title].bold("šŸ” ErrLens Analysis"), titleAlignment: "left", - width: 80 // Fixed width for consistent professional look + width: 80, }); } -module.exports = { formatError }; \ No newline at end of file +module.exports = { formatError }; diff --git a/lib/injector.js b/lib/injector.js index bfbeffa..d0d780e 100644 --- a/lib/injector.js +++ b/lib/injector.js @@ -1,44 +1,67 @@ +"use strict"; + const { findError } = require("./matcher"); const { formatError } = require("./formatter"); +const { resolveTheme } = require("./themes"); const chalkImport = require("chalk"); const chalk = chalkImport.default || chalkImport; -/** - * ERR_LENS INJECTOR - * Attach this to any project to get automatic analysis on crash. - */ +const themeName = process.env.ERRLENS_THEME || "default"; +const verbose = process.env.ERRLENS_VERBOSE === "1"; +const theme = resolveTheme(themeName); + +function safeStringify(value) { + try { + if (value !== null && typeof value === "object") { + return value.stack || value.message || String(value); + } + return String(value ?? "unknown error"); + } catch { + return "unknown error"; + } +} + process.on("uncaughtException", (err) => { - // We search the stack trace because sometimes the match string - // is hidden deep in the error history. - const errStr = err !== null && typeof err === "object" ? (err.stack || err.message) : String(err ?? "unknown error"); + const errStr = safeStringify(err); const { count, matches } = findError(errStr); - + if (count > 0) { - console.log(chalk.cyan.bold(`\nšŸ’” ErrLens Runtime Analysis (${count} Issues Found):`)); - matches.forEach(match => console.log(formatError(match))); + console.log( + chalk[theme.infoLabel].bold( + `\nšŸ’” ErrLens Runtime Analysis (${count} Issues Found):` + ) + ); + matches.forEach((match) => + console.log(formatError(match, { theme: themeName, verbose })) + ); } else { - // If we don't know the error, show the original one so the dev isn't lost - console.error(chalk.red.bold("\n--- Raw System Error ---")); + console.error(chalk[theme.errorText].bold("\n--- Raw System Error ---")); console.error(err); } - // Always exit with failure code 1 after a crash process.exit(1); }); -// Also handle Unhandled Promise Rejections (Async/Await errors) process.on("unhandledRejection", (reason) => { - const reasonStr = reason !== null && typeof reason === "object" ? (reason.stack || reason.message) : String(reason ?? "unknown error"); - const { count, matches } = findError(reasonStr); - - if (count > 0) { - console.log(chalk.magenta.bold(`\nšŸ“” ErrLens Async Error Detection:`)); - matches.forEach(match => console.log(formatError(match))); - } else { - console.error(chalk.red.bold("\n--- Unhandled Async Rejection ---")); - console.error(reason); - } - process.exit(1); + const reasonStr = safeStringify(reason); + const { count, matches } = findError(reasonStr); + + if (count > 0) { + console.log( + chalk[theme.asyncLabel].bold( + `\nšŸ“” ErrLens Async Error Detection:` + ) + ); + matches.forEach((match) => + console.log(formatError(match, { theme: themeName, verbose })) + ); + } else { + console.error( + chalk[theme.errorText].bold("\n--- Unhandled Async Rejection ---") + ); + console.error(reason); + } + process.exit(1); }); -console.log(chalk.dim("šŸ”§ ErrLens Protection Enabled.")); \ No newline at end of file +console.log(chalk[theme.protectionText]("šŸ”§ ErrLens Protection Enabled.")); diff --git a/lib/matcher.js b/lib/matcher.js index a4fa008..4db673f 100644 --- a/lib/matcher.js +++ b/lib/matcher.js @@ -77,7 +77,14 @@ function findError(input,lang = "en") { for (const match of foundMatches) { if (!seenNames.has(match.name)) { - uniqueMatches.push(translateEntry(match,locale)); + const translated = translateEntry(match, locale); + + // Extract the exact case-preserved substring from the input + const idx = lowerInput.indexOf(match.match.toLowerCase()); + translated.matchedText = + idx >= 0 ? input.slice(idx, idx + match.match.length) : match.match; + + uniqueMatches.push(translated); seenNames.add(match.name); } } diff --git a/lib/themes.js b/lib/themes.js new file mode 100644 index 0000000..a4090e1 --- /dev/null +++ b/lib/themes.js @@ -0,0 +1,127 @@ +"use strict"; + +const THEMES = { + default: { + label: "default", + border: "cyan", + title: "cyan", + id: "cyan", + whatLabel: "yellow", + whyLabel: "magenta", + fixesLabel: "green", + fixCheck: "green", + exampleLabel: "blue", + exampleCode: "gray", + headerText: "cyan", + successText: "green", + errorText: "red", + signalText: "red", + rawText: "gray", + unknownText: "red", + infoLabel: "cyan", + asyncLabel: "magenta", + protectionText: "dim", + }, + minimal: { + label: "minimal", + border: "gray", + title: "gray", + id: "white", + whatLabel: "white", + whyLabel: "white", + fixesLabel: "white", + fixCheck: "white", + exampleLabel: "white", + exampleCode: "gray", + headerText: "white", + successText: "white", + errorText: "white", + signalText: "white", + rawText: "gray", + unknownText: "white", + infoLabel: "white", + asyncLabel: "white", + protectionText: "gray", + }, + "high-contrast": { + label: "high-contrast", + border: "white", + title: "white", + id: "whiteBright", + whatLabel: "yellowBright", + whyLabel: "magentaBright", + fixesLabel: "greenBright", + fixCheck: "greenBright", + exampleLabel: "cyanBright", + exampleCode: "white", + headerText: "cyanBright", + successText: "greenBright", + errorText: "redBright", + signalText: "redBright", + rawText: "white", + unknownText: "redBright", + infoLabel: "cyanBright", + asyncLabel: "magentaBright", + protectionText: "white", + }, + dracula: { + label: "dracula", + border: "magenta", + title: "magenta", + id: "cyan", + whatLabel: "green", + whyLabel: "red", + fixesLabel: "yellow", + fixCheck: "green", + exampleLabel: "cyan", + exampleCode: "gray", + headerText: "magenta", + successText: "green", + errorText: "red", + signalText: "red", + rawText: "gray", + unknownText: "red", + infoLabel: "cyan", + asyncLabel: "magenta", + protectionText: "gray", + }, + monokai: { + label: "monokai", + border: "yellow", + title: "yellow", + id: "cyan", + whatLabel: "green", + whyLabel: "magenta", + fixesLabel: "cyan", + fixCheck: "green", + exampleLabel: "yellow", + exampleCode: "gray", + headerText: "yellow", + successText: "green", + errorText: "red", + signalText: "red", + rawText: "gray", + unknownText: "red", + infoLabel: "cyan", + asyncLabel: "magenta", + protectionText: "gray", + }, +}; + +const THEME_NAMES = Object.keys(THEMES); + +function resolveTheme(name) { + if (typeof name !== "string") return THEMES.default; + const normalized = name.toLowerCase(); + if (Object.prototype.hasOwnProperty.call(THEMES, normalized)) { + return THEMES[normalized]; + } + return THEMES.default; +} + +function isValidTheme(name) { + if (!name) return true; + return THEME_NAMES.includes(name.toLowerCase()); +} + +module.exports = { THEMES, THEME_NAMES, resolveTheme, isValidTheme };