Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
28 changes: 21 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)`

Expand All @@ -98,12 +98,12 @@ and stack trace for context. No AI API or remote service is used.

`error-translator <code or message> [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.
Expand Down Expand Up @@ -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" });
```
2 changes: 1 addition & 1 deletion bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ try {
});
if (values.help) {
console.log(
'Usage: error-translator <error code or quoted message> [--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 <error code or quoted message> [--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(
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
19 changes: 17 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,29 @@
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 = {}) {
if (!options || typeof options !== "object" || Array.isArray(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;
Expand Down Expand Up @@ -58,6 +69,10 @@ export function translateError(error, options = {}) {
return {
code: matched ?? "UNKNOWN",
...result,
explanation:
mode === "rubber-duck"
? `${result.explanation} ${Object.hasOwn(duckLines, matched ?? "") ? duckLines[matched] : "The duck recommends investigating before blaming the compiler."}`
: result.explanation,
likelyCauses: [...result.likelyCauses],
suggestions: [...result.suggestions],
mode,
Expand Down
36 changes: 36 additions & 0 deletions test/capabilities.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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",
"__proto__",
"constructor",
]) {
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" }),
);
});
2 changes: 1 addition & 1 deletion test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
Loading