diff --git a/CHANGELOG.md b/CHANGELOG.md index f67890a..1c007e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.4.0 + +46 built-in definitions cover filesystem, permissions, networking, module loading, streams, HTTP response lifecycle and JavaScript errors. Each has a specific explanation, likely causes and practical checks. Rubber-duck commentary has a distinct original line for every supported definition. No commands run automatically and unknown errors remain explicitly unrecognized. + +```sh + + ## 0.3.0 — 2026-10-08 - Useful structured API/CLI additions described in README. @@ -22,3 +29,4 @@ - Plain-English translations for ECONNREFUSED, ENOENT, EADDRINUSE, MODULE_NOT_FOUND, ERR_MODULE_NOT_FOUND, and TypeError. - Structured results, text rendering, and CLI with JSON output. - Automated tests, linting, formatting, and Node 22/24 CI. +``` diff --git a/MIGRATION.md b/MIGRATION.md index c14f761..5058fa7 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -3,3 +3,9 @@ Node 22.13+ is required. Existing core APIs remain available; TypeScript declarations and CommonJS exports are new. The CLI now lives in src/cli.js behind the same executable path. Input/stdout behavior for new options is documented in README. Install 0.3.0 with npm. Seeds and exact humorous wording are version-specific. Do not treat jokes or heuristic scores as production evidence. + +## 0.3.0 to 0.4.0 + +46 distinct error definitions with explanations, causes and practical checks; original duck commentary for each; structured errorCatalog API and --catalog CLI. + +Default behavior is retained except that the expanded error catalog now recognizes additional errors. New rotation and release-plan features are opt-in. diff --git a/README.md b/README.md index 9e16799..ae1a523 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,24 @@ # error-translator -> **Version 0.3.0:** install from npm with Node 22.13+ or Node 24. See MIGRATION.md for changes from 0.2.0. +## Browse the debugging library (0.4.0) + +46 built-in definitions cover filesystem, permissions, networking, module loading, streams, HTTP response lifecycle and JavaScript errors. Each has a specific explanation, likely causes and practical checks. Rubber-duck commentary has a distinct original line for every supported definition. No commands run automatically and unknown errors remain explicitly unrecognized. + +```sh +npx @deployanyway/error-translator ERR_HTTP_HEADERS_SENT --mode rubber-duck +npx @deployanyway/error-translator --catalog --json +npx @deployanyway/error-translator --list +``` + +```js +import { errorCatalog, translateError } from "@deployanyway/error-translator"; +console.log(errorCatalog({ mode: "plain" })); +console.log(translateError({ code: "EACCES", message: "open config.json" })); +``` + +`errorCatalog(options?)` returns independent structured translations for the complete catalog. `--catalog` prints all guidance, or JSON with `--json`. Explicit codes still take precedence over message matching. This is a focused Node/JavaScript debugging catalog, not diagnosis of every platform or framework error. Technical reference: https://nodejs.org/api/errors.html. + +> **Version 0.4.0:** install from npm with Node 22.13+ or Node 24. See MIGRATION.md for changes from 0.2.0. [![npm version](https://img.shields.io/npm/v/%40deployanyway%2Ferror-translator)](https://www.npmjs.com/package/@deployanyway/error-translator) [![CI](https://github.com/DeployAnyway/error-translator/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/DeployAnyway/error-translator/actions/workflows/ci.yml) @@ -84,14 +102,54 @@ Invalid result shapes throw TypeError. ### Built-in errors -| Code/name | Guidance | -| -------------------- | ------------------------------------------ | -| ECONNREFUSED | Service availability, host, port, firewall | -| ENOENT | Missing paths and working directories | -| EADDRINUSE | Port conflicts and duplicate instances | -| MODULE_NOT_FOUND | CommonJS dependencies and paths | -| ERR_MODULE_NOT_FOUND | ES module dependencies, paths, extensions | -| TypeError | Unexpected types and null/undefined values | +| Code/name | Guidance | +| ------------------------------ | --------------------------------- | +| EACCES | Permission denied | +| EPERM | Operation not permitted | +| EEXIST | Target already exists | +| ENOTDIR | Expected a directory | +| EISDIR | Expected a file | +| ENOTEMPTY | Directory is not empty | +| EMFILE | Too many open files | +| ENFILE | System file table exhausted | +| ENOSPC | No space left | +| EROFS | Read-only filesystem | +| EBUSY | Resource busy | +| EXDEV | Cross-device operation | +| EPIPE | Broken pipe | +| ECONNRESET | Connection reset | +| ETIMEDOUT | Operation timed out | +| ENOTFOUND | Name lookup failed | +| EAI_AGAIN | Temporary name lookup failure | +| EADDRNOTAVAIL | Local address unavailable | +| ECONNABORTED | Connection aborted | +| ENETUNREACH | Network unreachable | +| EHOSTUNREACH | Host unreachable | +| EINVAL | Invalid argument | +| ABORT_ERR | Operation aborted | +| ERR_INVALID_ARG_TYPE | Wrong argument type | +| ERR_INVALID_ARG_VALUE | Invalid argument value | +| ERR_OUT_OF_RANGE | Value outside allowed range | +| ERR_HTTP_HEADERS_SENT | Headers already sent | +| ERR_HTTP_INVALID_STATUS_CODE | Invalid HTTP status | +| ERR_INVALID_URL | Invalid URL | +| ERR_PACKAGE_PATH_NOT_EXPORTED | Package path not exported | +| ERR_PACKAGE_IMPORT_NOT_DEFINED | Package import not defined | +| ERR_INVALID_PACKAGE_CONFIG | Invalid package configuration | +| ERR_UNKNOWN_FILE_EXTENSION | Unknown module file extension | +| ERR_IMPORT_ATTRIBUTE_MISSING | Required import attribute missing | +| ERR_STREAM_WRITE_AFTER_END | Write after stream end | +| ERR_STREAM_DESTROYED | Stream destroyed | +| ERR_SOCKET_BAD_PORT | Invalid socket port | +| SyntaxError | Invalid JavaScript syntax | +| ReferenceError | Name is not available | +| RangeError | Value exceeds a permitted range | +| ECONNREFUSED | Connection refused | +| ENOENT | File or directory not found | +| EADDRINUSE | Address already in use | +| MODULE_NOT_FOUND | Module not found | +| ERR_MODULE_NOT_FOUND | ES module not found | +| TypeError | Unexpected value type | Translations describe likely causes, not a diagnosis. Keep the original error and stack trace for context. No AI API or remote service is used. diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index ba21b4e..3810819 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,3 +1,9 @@ -# 0.3.0 — 2026-10-08 +# 0.4.0 — 2026-10-08 -Adds useful API/CLI capabilities, TypeScript and CommonJS, coverage and installed-archive gates, and consistent contributor documentation. See README and MIGRATION.md for behavior and compatibility. Published through the repository’s trusted GitHub Actions workflow. +46 distinct error definitions with explanations, causes and practical checks; original duck commentary for each; structured errorCatalog API and --catalog CLI. + +Requires Node 22.13+ or 24. See README for copyable CLI and API examples. All required source, type, coverage and installed archive checks passed locally; CI must pass on the final PR head before merging. Original content, MIT code, no backend calls from the libraries. + +## Compatibility + +Existing APIs remain available; new modes are explicit. Expanded error matching may recognize codes that were previously unknown. diff --git a/index.d.cts b/index.d.cts index a210e34..8f44227 100644 --- a/index.d.cts +++ b/index.d.cts @@ -21,3 +21,5 @@ export function translateErrors( ): Translation[]; export function renderTranslation(result: Translation): string; export function listErrors(): string[]; + +export function errorCatalog(options?: TranslationOptions): Translation[]; diff --git a/index.d.ts b/index.d.ts index a210e34..8f44227 100644 --- a/index.d.ts +++ b/index.d.ts @@ -21,3 +21,5 @@ export function translateErrors( ): Translation[]; export function renderTranslation(result: Translation): string; export function listErrors(): string[]; + +export function errorCatalog(options?: TranslationOptions): Translation[]; diff --git a/package-lock.json b/package-lock.json index ad6d16e..661b1be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@deployanyway/error-translator", - "version": "0.3.0", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@deployanyway/error-translator", - "version": "0.3.0", + "version": "0.4.0", "license": "MIT", "bin": { "error-translator": "bin/cli.js" diff --git a/package.json b/package.json index 3dd0a2f..97a575d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@deployanyway/error-translator", - "version": "0.3.0", + "version": "0.4.0", "description": "Plain-English Node.js error explanations and debugging tips, because the stack trace has chosen violence.", "type": "module", "exports": { diff --git a/src/cli.js b/src/cli.js index 4053bbb..b680f7b 100644 --- a/src/cli.js +++ b/src/cli.js @@ -6,6 +6,7 @@ import { renderTranslation, translateErrors, listErrors, + errorCatalog, } from "./index.js"; import { readStdin } from "./input.js"; @@ -19,12 +20,13 @@ try { json: { type: "boolean" }, batch: { type: "boolean" }, list: { type: "boolean" }, + catalog: { type: "boolean" }, mode: { type: "string", default: "plain" }, }, }); if (values.help) { console.log( - 'Usage: error-translator [--json] [--mode plain|rubber-duck]\n\nTranslate errors into human-readable guidance. Without arguments, read stdin (256 KiB). --batch reads a JSON array of 1–100 errors. --list lists supported codes.\n\nOptions:\n -h, --help Show help\n -v, --version Show version\n --json Print a structured JSON result\n --mode plain|rubber-duck Keep useful guidance; add duck commentary\n\nExamples:\n error-translator ECONNREFUSED\n error-translator "TypeError: value is not a function" --json\n\nExit codes: 0 translation/help/version; 2 invalid arguments.', + 'Usage: error-translator [--json] [--mode plain|rubber-duck]\n\nTranslate errors into human-readable guidance. Without arguments, read stdin (256 KiB). --batch reads a JSON array of 1–100 errors. --list lists supported codes. --catalog prints all explanations and checks.\n\nOptions:\n -h, --help Show help\n -v, --version Show version\n --json Print a structured JSON result\n --mode plain|rubber-duck Keep useful guidance; add duck commentary\n\nExamples:\n error-translator ECONNREFUSED\n error-translator "TypeError: value is not a function" --json\n\nExit codes: 0 translation/help/version; 2 invalid arguments.', ); } else if (values.version) { console.log( @@ -32,6 +34,19 @@ try { readFileSync(new URL("../package.json", import.meta.url), "utf8"), ).version, ); + } else if (values.catalog) { + if (positionals.length || values.batch || values.list) + throw new TypeError( + "--catalog does not accept input, --batch or --list.", + ); + const catalog = errorCatalog({ mode: values.mode }); + console.log( + values.json + ? JSON.stringify(catalog, null, 2) + : catalog + .map((item) => item.code + " — " + renderTranslation(item)) + .join("\n\n---\n\n"), + ); } else if (values.list) { if (positionals.length || values.batch) throw new TypeError("--list does not accept input or --batch."); diff --git a/src/definitions.js b/src/definitions.js index 7474583..ebad2b3 100644 --- a/src/definitions.js +++ b/src/definitions.js @@ -1,5 +1,7 @@ +import { extraDefinitions } from "./extra-definitions.js"; // Keep technical guidance here so contributors can add errors without changing logic. export const definitions = { + ...extraDefinitions, ECONNREFUSED: { title: "Connection refused", explanation: diff --git a/src/extra-definitions.js b/src/extra-definitions.js new file mode 100644 index 0000000..92decce --- /dev/null +++ b/src/extra-definitions.js @@ -0,0 +1,494 @@ +// Original explanations and practical checks; no automatic remediation. +export const extraDefinitions = { + EACCES: { + title: "Permission denied", + explanation: "The process lacks permission for the requested operation.", + likelyCauses: [ + "A directory or file belongs to another account", + "The application runs under a restricted service identity", + ], + suggestions: [ + "Check the service identity and permissions on each parent directory.", + "Grant only the access the application needs; avoid running everything as administrator.", + ], + }, + EPERM: { + title: "Operation not permitted", + explanation: + "The operating system rejected an operation even though the target may exist.", + likelyCauses: [ + "A protected resource or locked file", + "An operation forbidden by the current platform or identity", + ], + suggestions: [ + "Inspect the operation, ownership and platform restrictions.", + "Close the process holding the resource and retry only if the operation is intended.", + ], + }, + EEXIST: { + title: "Target already exists", + explanation: + "A creation operation requires a new target, but that target already exists.", + likelyCauses: ["A repeated initialization step", "A filename collision"], + suggestions: [ + "Check the target before creating it and make initialization idempotent.", + "Use a distinct name rather than deleting an existing target blindly.", + ], + }, + ENOTDIR: { + title: "Expected a directory", + explanation: + "A path component used as a directory is actually another kind of file.", + likelyCauses: [ + "A file appears in the middle of the path", + "A configuration value points to the wrong target", + ], + suggestions: [ + "Inspect each path component and its type.", + "Correct the directory configuration before retrying.", + ], + }, + EISDIR: { + title: "Expected a file", + explanation: "The operation received a directory where it expected a file.", + likelyCauses: [ + "The filename is missing from a configured path", + "A directory was passed to a file operation", + ], + suggestions: [ + "Check the complete path and inspect the target type.", + "Use the appropriate directory API when working with directories.", + ], + }, + ENOTEMPTY: { + title: "Directory is not empty", + explanation: + "A directory operation requires an empty target, but entries remain.", + likelyCauses: [ + "Generated files remain in the directory", + "Another process is still writing files", + ], + suggestions: [ + "List the remaining entries and identify their owner.", + "Remove only confirmed disposable content or choose a new directory.", + ], + }, + EMFILE: { + title: "Too many open files", + explanation: "The process has exhausted its available file descriptors.", + likelyCauses: [ + "Handles are not closed", + "Too many files or sockets are opened concurrently", + ], + suggestions: [ + "Close handles in cleanup paths and bound concurrent work.", + "Inspect descriptor usage before considering a higher process limit.", + ], + }, + ENFILE: { + title: "System file table exhausted", + explanation: "The system cannot allocate another file handle.", + likelyCauses: [ + "Many processes hold open files", + "The host has reached a system resource limit", + ], + suggestions: [ + "Inspect host-wide handle usage and identify runaway processes.", + "Reduce concurrency and resolve leaks before changing system limits.", + ], + }, + ENOSPC: { + title: "No space left", + explanation: + "The filesystem cannot allocate the space or resources required by the operation.", + likelyCauses: [ + "The volume is full", + "Filesystem quotas or resource limits are exhausted", + ], + suggestions: [ + "Check free space, quotas and the volume receiving the write.", + "Clear verified disposable data and investigate rapid growth.", + ], + }, + EROFS: { + title: "Read-only filesystem", + explanation: "The target filesystem does not permit writes.", + likelyCauses: [ + "A read-only container mount", + "A protected or recovery-mounted volume", + ], + suggestions: [ + "Inspect mount settings and choose an intended writable data location.", + "Change mount configuration only when the write is part of the deployment design.", + ], + }, + EBUSY: { + title: "Resource busy", + explanation: + "A resource cannot complete the requested operation while another operation holds it.", + likelyCauses: [ + "A mounted or open resource", + "A concurrent operation has not finished", + ], + suggestions: [ + "Identify the process or operation holding the resource.", + "Wait for its completion or coordinate ownership instead of looping without a limit.", + ], + }, + EXDEV: { + title: "Cross-device operation", + explanation: + "An operation such as a rename crosses filesystem boundaries that it cannot cross directly.", + likelyCauses: [ + "Temporary and destination files live on different volumes", + "A container mount creates a filesystem boundary", + ], + suggestions: [ + "Create temporary files on the destination filesystem when atomic rename is required.", + "Use an intentional copy-and-cleanup workflow when atomic movement is unnecessary.", + ], + }, + EPIPE: { + title: "Broken pipe", + explanation: + "The application wrote to a stream whose receiving end has closed.", + likelyCauses: [ + "A downstream command exited", + "A peer disconnected before the write completed", + ], + suggestions: [ + "Handle stream errors and stop producing data when the consumer closes.", + "Distinguish an expected pipeline close from an unexpected connection failure.", + ], + }, + ECONNRESET: { + title: "Connection reset", + explanation: + "A connection was forcibly closed while the application was using it.", + likelyCauses: [ + "The remote peer closed the connection", + "An intermediary interrupted the session", + ], + suggestions: [ + "Inspect peer logs, timeout settings and recent connection changes.", + "Retry only safe operations with a bounded backoff and avoid duplicate side effects.", + ], + }, + ETIMEDOUT: { + title: "Operation timed out", + explanation: + "A connection or operation did not finish within its allowed time.", + likelyCauses: [ + "An unreachable or slow peer", + "A timeout shorter than the expected operation", + ], + suggestions: [ + "Check network reachability, service latency and the configured timeout.", + "Use bounded retries only for operations that are safe to repeat.", + ], + }, + ENOTFOUND: { + title: "Name lookup failed", + explanation: + "The requested hostname could not be resolved by the lookup operation.", + likelyCauses: [ + "A misspelled hostname", + "Resolver configuration or lookup resources failed", + ], + suggestions: [ + "Check the exact hostname and DNS behavior from the same runtime environment.", + "Do not assume every lookup failure proves the hostname does not exist.", + ], + }, + EAI_AGAIN: { + title: "Temporary name lookup failure", + explanation: "Name resolution reported a temporary failure.", + likelyCauses: [ + "A resolver is temporarily unavailable", + "A transient network or DNS problem", + ], + suggestions: [ + "Check the resolver and network from the affected host.", + "Apply bounded retry with backoff and report persistent failures.", + ], + }, + EADDRNOTAVAIL: { + title: "Local address unavailable", + explanation: + "The application tried to use an address that is not available on the local system.", + likelyCauses: [ + "Binding to an address on another host", + "An interface address changed", + ], + suggestions: [ + "Inspect local interfaces and the configured bind address.", + "Use the intended local interface rather than a remote destination address.", + ], + }, + ECONNABORTED: { + title: "Connection aborted", + explanation: "A connection was aborted before the operation completed.", + likelyCauses: [ + "A connection failure during setup or transfer", + "A local or intermediary cancellation", + ], + suggestions: [ + "Inspect the failure timing and both ends of the connection.", + "Handle cancellation separately from application errors when appropriate.", + ], + }, + ENETUNREACH: { + title: "Network unreachable", + explanation: "The host has no usable route to the requested network.", + likelyCauses: [ + "Missing route or disconnected interface", + "Network isolation in a container or host", + ], + suggestions: [ + "Inspect connectivity and routing in the application environment.", + "Confirm the destination network is intended to be reachable.", + ], + }, + EHOSTUNREACH: { + title: "Host unreachable", + explanation: + "The destination host could not be reached through the available network path.", + likelyCauses: [ + "The destination is unavailable", + "Routing or filtering blocks the path", + ], + suggestions: [ + "Check the destination address and routing from the affected host.", + "Inspect network policy rather than disabling protection blindly.", + ], + }, + EINVAL: { + title: "Invalid argument", + explanation: "The operating system rejected an argument to the operation.", + likelyCauses: [ + "A value outside the operation contract", + "A platform-specific unsupported value", + ], + suggestions: [ + "Inspect the syscall and the exact values passed to it.", + "Validate values against the API and platform requirements.", + ], + }, + ABORT_ERR: { + title: "Operation aborted", + explanation: "An operation was cancelled through an abort mechanism.", + likelyCauses: [ + "An AbortSignal was triggered", + "A caller timeout cancelled the operation", + ], + suggestions: [ + "Inspect who owns the abort controller and when cancellation occurred.", + "Treat expected cancellation separately and clean up resources.", + ], + }, + ERR_INVALID_ARG_TYPE: { + title: "Wrong argument type", + explanation: "A Node API received an argument of the wrong type.", + likelyCauses: [ + "A missing or incorrect value", + "An unexpected return type passed to another API", + ], + suggestions: [ + "Read the parameter name in the error and inspect its actual value.", + "Validate the argument at the boundary where it enters your application.", + ], + }, + ERR_INVALID_ARG_VALUE: { + title: "Invalid argument value", + explanation: "An argument has a value the API does not accept.", + likelyCauses: [ + "A valid type with an unsupported value", + "An invalid option combination", + ], + suggestions: [ + "Inspect the named parameter and documented accepted values.", + "Validate options before calling the API.", + ], + }, + ERR_OUT_OF_RANGE: { + title: "Value outside allowed range", + explanation: "A numeric or size argument falls outside the API range.", + likelyCauses: [ + "An invalid offset or length", + "A computed limit exceeds the supported bounds", + ], + suggestions: [ + "Check the reported minimum and maximum and the calculation producing the value.", + "Add boundary tests for zero, limits and oversized input.", + ], + }, + ERR_HTTP_HEADERS_SENT: { + title: "Headers already sent", + explanation: + "The server tried to change headers after the response headers had been sent.", + likelyCauses: [ + "Two code paths respond to the same request", + "An asynchronous callback runs after the response is complete", + ], + suggestions: [ + "Trace every response path and return after sending the response.", + "Coordinate asynchronous work so only one path owns the final response.", + ], + }, + ERR_HTTP_INVALID_STATUS_CODE: { + title: "Invalid HTTP status", + explanation: "The response status code is outside the accepted API range.", + likelyCauses: [ + "An undefined value becomes a status", + "A business code is mistaken for an HTTP status", + ], + suggestions: [ + "Check the status value before assigning or sending it.", + "Keep internal result codes separate from HTTP status codes.", + ], + }, + ERR_INVALID_URL: { + title: "Invalid URL", + explanation: "The URL parser cannot interpret the supplied URL.", + likelyCauses: [ + "A malformed absolute URL", + "A relative URL without a valid base", + ], + suggestions: [ + "Inspect the original input and supply an explicit base for relative paths.", + "Validate user-controlled URL inputs before making requests.", + ], + }, + ERR_PACKAGE_PATH_NOT_EXPORTED: { + title: "Package path not exported", + explanation: "A package exports map does not expose the requested subpath.", + likelyCauses: [ + "Importing an internal package file", + "A dependency changed its public entry points", + ], + suggestions: [ + "Inspect the package exports and use a documented public entry.", + "Upgrade or pin compatible dependencies instead of relying on private paths.", + ], + }, + ERR_PACKAGE_IMPORT_NOT_DEFINED: { + title: "Package import not defined", + explanation: "A package imports map has no matching internal specifier.", + likelyCauses: [ + "A missing imports mapping", + "A specifier is used outside its intended package scope", + ], + suggestions: [ + "Check package.json imports and the exact specifier.", + "Keep internal import aliases inside the package that defines them.", + ], + }, + ERR_INVALID_PACKAGE_CONFIG: { + title: "Invalid package configuration", + explanation: "Node could not use the package configuration.", + likelyCauses: ["Malformed package.json", "An invalid configuration field"], + suggestions: [ + "Read the named package.json and validate its JSON and relevant fields.", + "Check the first configuration error before reinstalling dependencies.", + ], + }, + ERR_UNKNOWN_FILE_EXTENSION: { + title: "Unknown module file extension", + explanation: + "The module loader does not recognize the file extension in this context.", + likelyCauses: [ + "An unsupported file type is imported", + "The required build or loader is missing", + ], + suggestions: [ + "Check the imported extension and the runtime module configuration.", + "Compile to a supported format or use a documented loader setup.", + ], + }, + ERR_IMPORT_ATTRIBUTE_MISSING: { + title: "Required import attribute missing", + explanation: "A module import requires an attribute that was not provided.", + likelyCauses: [ + "A JSON import lacks the required type attribute", + "An import was copied from a different runtime version", + ], + suggestions: [ + "Check the module type and import syntax for your supported Node version.", + "Use the required import attributes or an appropriate file-reading API.", + ], + }, + ERR_STREAM_WRITE_AFTER_END: { + title: "Write after stream end", + explanation: "Data was written after the writable stream had ended.", + likelyCauses: [ + "A late callback writes to a closed stream", + "Multiple owners end and write the same stream", + ], + suggestions: [ + "Trace the stream lifecycle and coordinate the writer with completion.", + "Test delayed callbacks and ensure end is called once work is complete.", + ], + }, + ERR_STREAM_DESTROYED: { + title: "Stream destroyed", + explanation: "An operation used a stream after it had been destroyed.", + likelyCauses: [ + "A prior error destroyed the stream", + "Cleanup ran while work was still pending", + ], + suggestions: [ + "Inspect the original stream error and cancellation timing.", + "Stop dependent work when the stream closes or is destroyed.", + ], + }, + ERR_SOCKET_BAD_PORT: { + title: "Invalid socket port", + explanation: "A socket port value is not accepted by the API.", + likelyCauses: [ + "A malformed environment variable", + "A number outside the port range", + ], + suggestions: [ + "Validate and parse the port configuration before creating the connection.", + "Do not substitute a silent default for a visibly invalid production setting.", + ], + }, + SyntaxError: { + title: "Invalid JavaScript syntax", + explanation: "The parser cannot interpret the source or input syntax.", + likelyCauses: [ + "Malformed JavaScript or JSON", + "Syntax unsupported by the current runtime", + ], + suggestions: [ + "Read the first reported location and inspect nearby delimiters and tokens.", + "Confirm the input format and runtime version before changing unrelated code.", + ], + }, + ReferenceError: { + title: "Name is not available", + explanation: + "An expression refers to a binding that is not available in its current scope or initialization state.", + likelyCauses: [ + "A misspelled or out-of-scope variable", + "Access before initialization", + ], + suggestions: [ + "Inspect the referenced name and its scope at the reported line.", + "Check initialization order and add a focused regression test.", + ], + }, + RangeError: { + title: "Value exceeds a permitted range", + explanation: + "An operation received a value outside its allowed range or exhausted a range-related resource.", + likelyCauses: [ + "An invalid numeric size or index", + "Excessive recursion or allocation", + ], + suggestions: [ + "Read the exact message to distinguish numeric bounds from stack exhaustion.", + "Inspect boundary values and recursion termination rather than treating every RangeError alike.", + ], + }, +}; diff --git a/src/index.js b/src/index.js index 3e94828..a20ed7e 100644 --- a/src/index.js +++ b/src/index.js @@ -8,6 +8,55 @@ export function translateErrors(errors, options = {}) { return Array.from(errors, (error) => translateError(error, options)); } const duckLines = { + EACCES: "The file has a guest list. Your process is not on it.", + EPERM: "The operating system has declined your ambitious proposal.", + EEXIST: "The target is already here and has not agreed to be replaced.", + ENOTDIR: "That hallway is actually a cupboard.", + EISDIR: "You asked for one file and received the whole neighborhood.", + ENOTEMPTY: "The directory still has roommates.", + EMFILE: "Your process opened every door and forgot to close them.", + ENFILE: "The host has run out of chairs for file handles.", + ENOSPC: "The disk has eaten the final snack.", + EROFS: "This filesystem is a museum: look, but do not rearrange.", + EBUSY: "The resource is in another meeting.", + EXDEV: "The rename has reached the border and needs a different passport.", + EPIPE: "The audience left before your stream finished the speech.", + ECONNRESET: "The other end hung up without the usual pleasantries.", + ETIMEDOUT: "The operation is late and has not brought a believable excuse.", + ENOTFOUND: "The hostname is not answering roll call.", + EAI_AGAIN: "DNS requests a brief intermission, not eternal optimism.", + EADDRNOTAVAIL: "That address does not live on this machine.", + ECONNABORTED: "The connection left before the conversation was finished.", + ENETUNREACH: "The packet has no road to the destination.", + EHOSTUNREACH: + "The route exists; the destination is still playing hard to reach.", + EINVAL: "The argument has failed the entrance interview.", + ABORT_ERR: "The caller pulled the emergency stop. Ask why before restarting.", + ERR_INVALID_ARG_TYPE: + "The parameter ordered a string and received a surprise guest.", + ERR_INVALID_ARG_VALUE: "The type is correct. Its life choices are not.", + ERR_OUT_OF_RANGE: "The number wandered beyond the approved fence.", + ERR_HTTP_HEADERS_SENT: + "The response left the station. You cannot redecorate its headers now.", + ERR_HTTP_INVALID_STATUS_CODE: + "That number is not a useful mood for an HTTP response.", + ERR_INVALID_URL: + "The address has syntax ambitions but no navigable destination.", + ERR_PACKAGE_PATH_NOT_EXPORTED: "You are trying the package staff entrance.", + ERR_PACKAGE_IMPORT_NOT_DEFINED: + "The alias has not been introduced to this package.", + ERR_INVALID_PACKAGE_CONFIG: + "The package instructions have become abstract art.", + ERR_UNKNOWN_FILE_EXTENSION: "The loader does not speak this file dialect.", + ERR_IMPORT_ATTRIBUTE_MISSING: + "The import arrived without its required name badge.", + ERR_STREAM_WRITE_AFTER_END: + "The stream said goodbye. Your callback is still talking.", + ERR_STREAM_DESTROYED: "The stream has retired. Please respect its notice.", + ERR_SOCKET_BAD_PORT: "The port number did not pass the harbor inspection.", + SyntaxError: "The parser has found punctuation with questionable intentions.", + ReferenceError: "That name is not available for comment in this scope.", + RangeError: "The value or call stack has climbed over the fence.", ECONNREFUSED: "Your app knocked. The service has apparently gone for coffee.", ENOENT: "The file is playing hide-and-seek. It is currently winning.", EADDRINUSE: "Two servers reserved the same chair. Only one gets to sit.", @@ -114,3 +163,8 @@ export function renderTranslation(result) { : []), ].join("\n\n"); } + +/** Read the built-in guidance as independent structured records. */ +export function errorCatalog(options = {}) { + return listErrors().map((code) => translateError({ code }, options)); +} diff --git a/test/catalog.test.js b/test/catalog.test.js new file mode 100644 index 0000000..7227ff5 --- /dev/null +++ b/test/catalog.test.js @@ -0,0 +1,48 @@ +import { URL } from "node:url"; +import test from "node:test"; +import assert from "node:assert/strict"; +import { listErrors, translateError } from "../src/index.js"; +test("expanded technical catalog covers practical errors without pretending to recognize unknown codes", () => { + assert.equal(listErrors().length, 46); + for (const code of listErrors()) { + const result = translateError({ code }); + assert.equal(result.code, code); + assert.ok(result.likelyCauses.length >= 2); + assert.ok(result.suggestions.length >= 2); + result.suggestions.pop(); + assert.ok(translateError(code).suggestions.length >= 2); + } + assert.match( + translateError("ERR_HTTP_HEADERS_SENT").suggestions.join(" "), + /response/, + ); + assert.match(translateError("ENOSPC").suggestions.join(" "), /space/); + assert.match(translateError("EACCES").suggestions.join(" "), /permissions/); + assert.equal( + translateError({ code: "CUSTOM", message: "ENOENT" }).title, + "Unrecognized error", + ); + assert.equal( + translateError("ERR_STREAM_WRITE_AFTER_END: write after end").code, + "ERR_STREAM_WRITE_AFTER_END", + ); +}); +import { errorCatalog } from "../src/index.js"; +import { spawnSync } from "node:child_process"; +test("structured catalogs are independent and CLI JSON agrees with API", () => { + const catalog = errorCatalog({ mode: "rubber-duck" }); + assert.equal(catalog.length, 46); + catalog[0].suggestions.pop(); + assert.equal(errorCatalog()[0].suggestions.length, 2); + assert.throws(() => errorCatalog({ mode: "unknown" }), RangeError); + const run = (args) => + spawnSync(process.execPath, ["bin/cli.js", ...args], { + cwd: new URL("..", import.meta.url), + encoding: "utf8", + }); + let result = run(["--catalog", "--json"]); + assert.equal(result.status, 0); + assert.deepEqual(JSON.parse(result.stdout), errorCatalog()); + assert.match(run(["--catalog"]).stdout, /EACCES/); + assert.equal(run(["--catalog", "ENOENT"]).status, 2); +}); diff --git a/test/index.test.js b/test/index.test.js index 50ad081..a043c19 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -92,7 +92,7 @@ test("CLI text, JSON, help, version, unknown errors", () => { ); assert.equal(cli("--help").status, 0); assert.ok(cli("--help").stdout.includes("Usage:")); - assert.equal(cli("--version").stdout.trim(), "0.3.0"); + assert.equal(cli("--version").stdout.trim(), "0.4.0"); assert.equal(cli("custom error").status, 0); }); test("CLI invalid arguments exit 2 with stderr", () => { diff --git a/test/types.cts b/test/types.cts index 4a8c22d..d09a40b 100644 --- a/test/types.cts +++ b/test/types.cts @@ -4,3 +4,4 @@ api.translateErrors(["ENOENT", new Error("TypeError")], { }); // @ts-expect-error invalid literal api.translateError("x", { mode: "pirate" }); +api.errorCatalog({ mode: "plain" })[0].suggestions; diff --git a/test/types.mts b/test/types.mts index 1a21628..8bce457 100644 --- a/test/types.mts +++ b/test/types.mts @@ -4,3 +4,5 @@ api.translateErrors(["ENOENT", new Error("TypeError")], { }); // @ts-expect-error invalid literal api.translateError("x", { mode: "pirate" }); +import { errorCatalog } from "@deployanyway/error-translator"; +errorCatalog({ mode: "rubber-duck" })[0].suggestions;