From 36ab481d02c884826e543a4d265744e18baf7bce Mon Sep 17 00:00:00 2001 From: Todd Wolfe Date: Thu, 8 Oct 2026 15:06:18 -0400 Subject: [PATCH 1/2] Add humorous capabilities and prepare 0.2.0 --- CHANGELOG.md | 5 +++++ README.md | 28 +++++++++++++++++++++------- bin/cli.js | 2 +- package-lock.json | 4 ++-- package.json | 2 +- src/index.js | 19 +++++++++++++++++-- test/capabilities.test.js | 34 ++++++++++++++++++++++++++++++++++ test/index.test.js | 2 +- 8 files changed, 82 insertions(+), 14 deletions(-) create mode 100644 test/capabilities.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index f5e18f0..487bc51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.2.0 — 2026-10-08 + +- Rubber-duck translations: Keep the debugging guidance, add workplace-safe rubber-duck commentary. Plain output remains the default. +- Add npm and CI badges to the published README. + ## 0.1.1 — 2026-10-08 - Correct npm installation and npx documentation after the initial publication. diff --git a/README.md b/README.md index ab16150..48d819e 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ An explicit code takes precedence, followed by a recognized name, then a case-sensitive whole-word match in the message. Unknown errors return general guidance with code `UNKNOWN`, or preserve an explicit unknown code. Invalid input throws TypeError; unsupported modes throw RangeError. -Only `plain` is available in this MVP. Humorous modes are planned. +Modes: `plain` (default) and `rubber-duck`, which preserves the guidance and adds commentary. ### `renderTranslation(result)` @@ -98,12 +98,12 @@ and stack trace for context. No AI API or remote service is used. `error-translator [options]` -| Option | Behavior | -| ----------------- | --------------------- | -| `--help`, `-h` | Show usage | -| `--version`, `-v` | Show package version | -| `--json` | Print structured JSON | -| `--mode plain` | Select plain mode | +| Option | Behavior | +| ----------------- | -------------------------------- | +| `--help`, `-h` | Show usage | +| `--version`, `-v` | Show package version | +| `--json` | Print structured JSON | +| `--mode plain` | Select plain or rubber-duck mode | Quote messages containing spaces or shell punctuation. Use `--` before a message beginning with a dash. No stdin support is included in the MVP. @@ -144,3 +144,17 @@ are welcome. - [doggo-log](https://github.com/DeployAnyway/doggo-log) - [ship-it-meter](https://github.com/DeployAnyway/ship-it-meter) - [bro-say](https://github.com/DeployAnyway/bro-say) + +## Rubber-duck translations + +Keep the debugging guidance, add workplace-safe rubber-duck commentary. Plain output remains the default. + +```sh +npx @deployanyway/error-translator ECONNREFUSED --mode rubber-duck +``` + +API (import the named functions from this package): + +```js +translateError("ENOENT", { mode: "rubber-duck" }); +``` diff --git a/bin/cli.js b/bin/cli.js index d7f27e0..18f1dc8 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -16,7 +16,7 @@ try { }); if (values.help) { console.log( - 'Usage: error-translator [--json] [--mode plain]\n\nTranslate errors into human-readable guidance.\n\nOptions:\n -h, --help Show help\n -v, --version Show version\n --json Print a structured JSON result\n --mode plain Translation mode (MVP: plain only)\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.\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( diff --git a/package-lock.json b/package-lock.json index 7ed6747..754793c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@deployanyway/error-translator", - "version": "0.1.1", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@deployanyway/error-translator", - "version": "0.1.1", + "version": "0.2.0", "license": "MIT", "bin": { "error-translator": "bin/cli.js" diff --git a/package.json b/package.json index 350af2c..21fd1ba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@deployanyway/error-translator", - "version": "0.1.1", + "version": "0.2.0", "description": "Plain-English Node.js error explanations and debugging tips, because the stack trace has chosen violence.", "type": "module", "exports": "./src/index.js", diff --git a/src/index.js b/src/index.js index 533541e..dfeebaf 100644 --- a/src/index.js +++ b/src/index.js @@ -1,10 +1,20 @@ import { definitions } from "./definitions.js"; +const duckLines = { + 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.", + MODULE_NOT_FOUND: + "The dependency missed roll call. Check its invitation to node_modules.", + ERR_MODULE_NOT_FOUND: + "The module took a wrong turn. Extensions are street signs, not decorations.", + TypeError: "JavaScript received a surprise guest and forgot how to behave.", +}; /** * Translate a nonempty string, Error, or error-like object with a code/name/message. * Explicit codes take precedence over names and message matching. * @param {string | Error | {code?: string, name?: string, message?: string}} error - * @param {{mode?: 'plain'}} [options] + * @param {{mode?: 'plain' | 'rubber-duck'}} [options] * @returns {{code: string, title: string, explanation: string, likelyCauses: string[], suggestions: string[], mode: string}} */ export function translateError(error, options = {}) { @@ -12,7 +22,8 @@ export function translateError(error, options = {}) { throw new TypeError("Options must be an object."); } const mode = options.mode ?? "plain"; - if (mode !== "plain") throw new RangeError("Supported modes: plain."); + if (!["plain", "rubber-duck"].includes(mode)) + throw new RangeError("Supported modes: plain, rubber-duck."); let code; let name; let message; @@ -58,6 +69,10 @@ export function translateError(error, options = {}) { return { code: matched ?? "UNKNOWN", ...result, + explanation: + mode === "rubber-duck" + ? `${result.explanation} ${duckLines[matched] ?? "The duck recommends investigating before blaming the compiler."}` + : result.explanation, likelyCauses: [...result.likelyCauses], suggestions: [...result.suggestions], mode, diff --git a/test/capabilities.test.js b/test/capabilities.test.js new file mode 100644 index 0000000..8d321be --- /dev/null +++ b/test/capabilities.test.js @@ -0,0 +1,34 @@ +import { URL } from "node:url"; +import test from "node:test"; +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { translateError, renderTranslation } from "../src/index.js"; +test("duck commentary preserves every actionable suggestion and plain default", () => { + for (const code of [ + "ECONNREFUSED", + "ENOENT", + "EADDRINUSE", + "MODULE_NOT_FOUND", + "ERR_MODULE_NOT_FOUND", + "TypeError", + "CUSTOM", + ]) { + const plain = translateError(code); + const duck = translateError(code, { mode: "rubber-duck" }); + assert.deepEqual(duck.suggestions, plain.suggestions); + assert.deepEqual(duck.likelyCauses, plain.likelyCauses); + assert.ok(duck.explanation.startsWith(plain.explanation)); + assert.notEqual(duck.explanation, plain.explanation); + assert.ok(renderTranslation(duck).includes(duck.explanation)); + } + const cli = spawnSync( + process.execPath, + ["bin/cli.js", "ENOENT", "--mode", "rubber-duck", "--json"], + { cwd: new URL("..", import.meta.url), encoding: "utf8" }, + ); + assert.equal(cli.status, 0); + assert.deepEqual( + JSON.parse(cli.stdout), + translateError("ENOENT", { mode: "rubber-duck" }), + ); +}); diff --git a/test/index.test.js b/test/index.test.js index a843be1..9793404 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.1.1"); + assert.equal(cli("--version").stdout.trim(), "0.2.0"); assert.equal(cli("custom error").status, 0); }); test("CLI invalid arguments exit 2 with stderr", () => { From d259e7446df8334f668911156b8c59b4f16bb3da Mon Sep 17 00:00:00 2001 From: Todd Wolfe Date: Thu, 8 Oct 2026 15:07:40 -0400 Subject: [PATCH 2/2] Guard duck commentary against prototype names --- src/index.js | 2 +- test/capabilities.test.js | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index dfeebaf..a773c71 100644 --- a/src/index.js +++ b/src/index.js @@ -71,7 +71,7 @@ export function translateError(error, options = {}) { ...result, explanation: mode === "rubber-duck" - ? `${result.explanation} ${duckLines[matched] ?? "The duck recommends investigating before blaming the compiler."}` + ? `${result.explanation} ${Object.hasOwn(duckLines, matched ?? "") ? duckLines[matched] : "The duck recommends investigating before blaming the compiler."}` : result.explanation, likelyCauses: [...result.likelyCauses], suggestions: [...result.suggestions], diff --git a/test/capabilities.test.js b/test/capabilities.test.js index 8d321be..170cb0e 100644 --- a/test/capabilities.test.js +++ b/test/capabilities.test.js @@ -12,6 +12,8 @@ test("duck commentary preserves every actionable suggestion and plain default", "ERR_MODULE_NOT_FOUND", "TypeError", "CUSTOM", + "__proto__", + "constructor", ]) { const plain = translateError(code); const duck = translateError(code, { mode: "rubber-duck" });