From a381eeea3a4323a85fefbf70bfbfce367efcb62f Mon Sep 17 00:00:00 2001 From: jdalton Date: Wed, 22 Apr 2026 11:49:08 -0400 Subject: [PATCH 1/5] docs(claude): align Error Messages with fleet doctrine, add references doc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restructure the CLI-specific Error Messages section to match the updated fleet doctrine from socket-repo-template: - Keep the four ingredients (What / Where / Saw vs. wanted / Fix). - Add audience-based length guidance (library API terse / CLI verbose / programmatic rule-only). `InputError`/`AuthError` usages are verbose-tier. - Tighten baseline rules to one-liners; drop narrative phrasing. - Preserve the CLI-specific examples (--pull-request, socket init, AuthError) — they earn their keep as real anti-pattern fodder. Add `docs/references/error-messages.md` with fleet-wide worked examples so CLAUDE.md stays tight and the rich anti-patterns live once. --- CLAUDE.md | 39 ++++++++----- docs/references/error-messages.md | 92 +++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 13 deletions(-) create mode 100644 docs/references/error-messages.md diff --git a/CLAUDE.md b/CLAUDE.md index a5a213285..1c5fc3768 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -164,26 +164,39 @@ Follow [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). User-facing onl ### Error Messages -Errors are a UX surface. Every message must let the reader fix the problem without reading the source. Four ingredients, in order: +An error message is UI. The reader should be able to fix the problem from the message alone, without opening your source. -1. **What**: the rule that was violated (the contract, not the symptom) -2. **Where**: the exact flag, file, key, line, or record — never "somewhere in config" -3. **Saw vs. wanted**: the offending value and the allowed shape/set -4. **Fix**: one concrete action to resolve it +Every message needs four ingredients, in order: -- Imperative voice (`pass --limit=50`), not passive (`--limit was wrong`) -- Never say "invalid" without what made it invalid. `Invalid ecosystem: "foo"` is a symptom; `--reach-ecosystems must be one of: npm, pypi, maven (saw: "foo")` is a rule -- If two records collide, name both — not just the second one found -- Suggest, don't auto-correct. An error that silently repairs state hides the bug in the next run +1. **What** — the rule that was broken (e.g. "must be a non-negative integer"), not the fallout ("invalid"). +2. **Where** — the exact flag, file, line, key, or record. Not "somewhere in config". +3. **Saw vs. wanted** — the bad value and the allowed shape or set. +4. **Fix** — one concrete action, in imperative voice (`pass --limit=50`, not `--limit was wrong`). -**Examples:** +Length depends on the audience: + +- **Library API errors** (thrown from a published package): terse. Callers may match on the message text, so every word counts. All four ingredients often fit in one sentence. +- **CLI / validator / config errors** (developer reading a terminal): verbose. Give each ingredient its own words so the reader can fix it without re-running the tool. Most `InputError`/`AuthError` cases land here. +- **Programmatic errors** (internal assertions, invariant checks): terse, rule only. No end user will see it; short keeps the check readable. + +Rules for every message: + +- Imperative voice for the fix — `pass --limit=50`, not `--limit was wrong`. +- Never "invalid" on its own. `Invalid ecosystem: "foo"` is fallout; `--reach-ecosystems must be one of: npm, pypi, maven (saw: "foo")` is a rule. +- On a collision, name **both** sides, not just the second one found. +- Suggest, don't auto-correct. Silently fixing state hides the bug next time. +- Bloat check: if removing a word keeps the information, drop it. + +**CLI examples:** - ✅ `throw new InputError('--pull-request must be a non-negative integer (saw: "abc"); pass a number like --pull-request=42')` - ✅ `` throw new InputError(`No .socket directory found in ${cwd}; run \`socket init\` to create one`) `` - ✅ `throw new AuthError('Socket API rejected the token (401); run `socket login` or set SOCKET_CLI_API_TOKEN')` -- ❌ `throw new InputError('Invalid value for --limit: ${limit}')` (symptom, no rule, no fix) -- ❌ `throw new Error('Authentication failed')` (no where, no fix, wrong error type) -- ❌ `logger.error('Error occurred'); return` (doesn't set exit code) +- ❌ `throw new InputError('Invalid value for --limit: ${limit}')` — fallout, no rule, no fix +- ❌ `throw new Error('Authentication failed')` — no where, no fix, wrong error type +- ❌ `logger.error('Error occurred'); return` — doesn't set exit code + +See `docs/references/error-messages.md` for cross-fleet worked examples and anti-patterns. ### Command Pattern diff --git a/docs/references/error-messages.md b/docs/references/error-messages.md new file mode 100644 index 000000000..25e788481 --- /dev/null +++ b/docs/references/error-messages.md @@ -0,0 +1,92 @@ +# Error Messages — Worked Examples + +Companion to the `## Error Messages` section of `CLAUDE.md`. That section +holds the rules; this file holds longer examples and anti-patterns that +would bloat CLAUDE.md if inlined. + +## The four ingredients + +Every message needs, in order: + +1. **What** — the rule that was broken. +2. **Where** — the exact file, line, key, field, or CLI flag. +3. **Saw vs. wanted** — the bad value and the allowed shape or set. +4. **Fix** — one concrete action, in imperative voice. + +## Library API errors (terse) + +Callers may match on the message text, so stability matters. Aim for one +sentence. + +| ✗ / ✓ | Message | Notes | +| --- | --- | --- | +| ✗ | `Error: invalid component` | No rule, no saw, no where. | +| ✗ | `The "name" component of type "npm" failed validation because the provided value "" is empty, which is not allowed because names are required; please provide a non-empty name.` | Restates the rule three times. | +| ✓ | `npm "name" component is required` | Rule + where + implied saw (missing). Six words. | +| ✗ | `Error: bad name` | No rule. | +| ✓ | `name "__proto__" cannot start with an underscore` | Rule, where (`name`), saw (`__proto__`), fix implied. | + +## Validator / config / build-tool errors (verbose) + +The reader is looking at a file and wants to fix the record without +re-running the tool. Give each ingredient its own words. + +✗ `Error: invalid tour config` + +✓ `tour.json: part 3 ("Parsing & Normalization") is missing "filename". Add a single-word lowercase filename (e.g. "parsing") to this part — one per part is required to route //part/3 at publish time.` + +Breakdown: + +- **What**: `is missing "filename"` — the rule is "each part has a filename". +- **Where**: `tour.json: part 3 ("Parsing & Normalization")` — file + record + human label. +- **Saw vs. wanted**: saw = missing; wanted = a single-word lowercase filename, with `"parsing"` as a concrete model. +- **Fix**: `Add … to this part` — imperative, specific. + +The trailing `to route //part/3 at publish time` is optional. Include a *why* clause only when the rule is non-obvious; skip it for rules the reader already knows (e.g. "names can't start with an underscore"). + +## Programmatic errors (terse, rule only) + +Internal assertions and invariant checks. No end user will read them; +terse keeps the assertion readable when you skim the code. + +- ✓ `assert(queue.length > 0)` with message `queue drained before worker exit` +- ✓ `pool size must be positive` +- ✗ `An unexpected error occurred while trying to acquire a connection from the pool because the pool size was not positive.` — nothing a maintainer can act on that the rule itself doesn't already say. + +## Common anti-patterns + +**"Invalid X" with no rule.** + +- ✗ `Invalid filename 'My Part'` +- ✓ `filename 'My Part' must be [a-z]+ (lowercase, no spaces)` + +**Passive voice on the fix.** + +- ✗ `"filename" was missing` +- ✓ `add "filename" to part 3` + +**Naming only one side of a collision.** + +- ✗ `duplicate key "foo"` (which record won, which lost?) +- ✓ `duplicate key "foo" in config.json (lines 12 and 47) — rename one` + +**Silently auto-correcting.** + +- ✗ Stripping a trailing slash from a URL and continuing. The next run will hit the same bug; nothing learned. +- ✓ `url "https://api/" has a trailing slash — remove it`. + +**Bloat that restates the rule.** + +- ✗ `The value provided for "timeout" is invalid because timeouts must be positive numbers and the value you provided was not a positive number.` +- ✓ `timeout must be a positive number (saw: -5)` + +## Voice & tone + +- Imperative for the fix: `rename`, `add`, `remove`, `set`. +- Present tense for the rule: `must be`, `cannot`, `is required`. +- No apology ("Sorry, …"), no blame ("You provided …"). State the rule and the fix. +- Don't end with "please"; it doesn't add information and it makes the message feel longer than it is. + +## Bloat check + +Before shipping a message, cross out any word that, if removed, leaves the information intact. If only rhythm or politeness disappears, drop it. From 01516e4671afc97eba6a3b836fe7238dbb586ba6 Mon Sep 17 00:00:00 2001 From: jdalton Date: Wed, 22 Apr 2026 12:09:39 -0400 Subject: [PATCH 2/5] docs(claude): reference joinAnd / joinOr helpers in Error Messages Point readers at @socketsecurity/lib/arrays' list-formatting helpers from CLAUDE.md (one-line rule) and the worked-examples references doc (new "Formatting lists of values" section). --- CLAUDE.md | 1 + docs/references/error-messages.md | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 1c5fc3768..8519f5656 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -186,6 +186,7 @@ Rules for every message: - On a collision, name **both** sides, not just the second one found. - Suggest, don't auto-correct. Silently fixing state hides the bug next time. - Bloat check: if removing a word keeps the information, drop it. +- For allowed-set / conflict lists, use `joinAnd` / `joinOr` from `@socketsecurity/lib/arrays` — `must be one of: ${joinOr(allowed)}` reads better than a hand-formatted list. **CLI examples:** diff --git a/docs/references/error-messages.md b/docs/references/error-messages.md index 25e788481..97d446045 100644 --- a/docs/references/error-messages.md +++ b/docs/references/error-messages.md @@ -80,6 +80,24 @@ terse keeps the assertion readable when you skim the code. - ✗ `The value provided for "timeout" is invalid because timeouts must be positive numbers and the value you provided was not a positive number.` - ✓ `timeout must be a positive number (saw: -5)` +## Formatting lists of values + +When the error needs to show an allowed set, a list of conflicting +records, or multiple missing fields, use the list formatters from +`@socketsecurity/lib/arrays` rather than hand-joining with commas: + +- `joinAnd(['a', 'b', 'c'])` → `"a, b, and c"` — for conjunctions ("missing foo, bar, and baz") +- `joinOr(['npm', 'pypi', 'maven'])` → `"npm, pypi, or maven"` — for disjunctions ("must be one of: …") + +Both wrap `Intl.ListFormat`, so the Oxford comma and one-/two-item cases come out right for free (`joinOr(['a'])` → `"a"`; `joinOr(['a', 'b'])` → `"a or b"`). + +- ✗ `--reach-ecosystems must be one of: npm, pypi, maven (saw: "foo")` — hand-joined, breaks if the list has one or two entries. +- ✓ `` `--reach-ecosystems must be one of: ${joinOr(ALLOWED)} (saw: "foo")` `` +- ✗ `missing keys: filename slug title` — no separators, no grammar. +- ✓ `` `missing keys: ${joinAnd(missing)}` `` → `"missing keys: filename, slug, and title"` + +Use `joinOr` whenever the error is "must be one of X", `joinAnd` whenever it's "all of X are required / missing / in conflict". + ## Voice & tone - Imperative for the fix: `rename`, `add`, `remove`, `set`. From 3a1c47a8045c3d1b5a84afc847dabd4c98e92181 Mon Sep 17 00:00:00 2001 From: jdalton Date: Wed, 22 Apr 2026 14:09:44 -0400 Subject: [PATCH 3/5] chore: bump @socketsecurity/lib to 5.24.0 and adopt error helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fleet-wide migration to the caught-value helpers in @socketsecurity/lib/errors. - pnpm-workspace.yaml: catalog bump 5.21.0 → 5.24.0. - 18 src files under packages/cli/src: replace every `e instanceof Error ? e.message : String(e)` and `UNKNOWN_ERROR` fallback with `errorMessage(e)`; replace bare `x instanceof Error` boolean checks with `isError(x)`; replace `e instanceof Error ? e.stack : undefined` with `errorStack(e)`. - packages/cli/src/utils/error/errors.mts: drop the locally-defined `isErrnoException` (which accepted any `code !== undefined`) and re-export the library's stricter string-code variant. - packages/cli/src/commands/manifest/convert-{gradle,sbt}-to-maven.mts: rename a local `const errorMessage` → `summary` to free the identifier for the imported helper. - packages/cli/src/utils/telemetry/service.mts: rename two local `const errorMessage = …` variables to `errMsg` inside catch blocks for the same reason. - docs/references/error-messages.md: pick up the new "Working with caught values" section from socket-repo-template. Out of scope (intentionally left): - Exit-code checks on child-process results (`'code' in e` where `code` is a number, e.g. display.mts:257). `isErrnoException` requires a string code and would wrongly return false. - The local `getErrorMessage` / `getErrorMessageOr` helpers in errors.mts — callers outside this file still use them; a broader refactor to the library `errorMessage` is follow-up. Pre-commit tests skipped (DISABLE_PRECOMMIT_TEST); `pnpm run type` and `pnpm run lint` pass. --- docs/references/error-messages.md | 72 ++++++++++++++++--- packages/cli/src/cli-entry.mts | 3 +- .../commands/analytics/AnalyticsRenderer.mts | 3 +- .../commands/audit-log/AuditLogRenderer.mts | 3 +- .../manifest/convert-gradle-to-maven.mts | 9 +-- .../manifest/convert-sbt-to-maven.mts | 9 +-- .../threat-feed/ThreatFeedRenderer.mts | 3 +- packages/cli/src/utils/basics/spawn.mts | 5 +- .../cli/src/utils/command/registry-core.mts | 10 +-- packages/cli/src/utils/debug.mts | 9 ++- packages/cli/src/utils/error/display.mts | 18 +++-- packages/cli/src/utils/error/errors.mts | 13 +--- packages/cli/src/utils/git/github.mts | 5 +- .../cli/src/utils/git/gitlab-provider.mts | 3 +- packages/cli/src/utils/process/os.mts | 5 +- .../cli/src/utils/telemetry/integration.mts | 3 +- packages/cli/src/utils/telemetry/service.mts | 13 ++-- packages/cli/src/utils/update/checker.mts | 9 +-- packages/cli/src/utils/update/manager.mts | 13 ++-- packages/cli/src/utils/update/notifier.mts | 3 +- pnpm-lock.yaml | 33 ++++----- pnpm-workspace.yaml | 2 +- 22 files changed, 154 insertions(+), 92 deletions(-) diff --git a/docs/references/error-messages.md b/docs/references/error-messages.md index 97d446045..997bdf85f 100644 --- a/docs/references/error-messages.md +++ b/docs/references/error-messages.md @@ -18,13 +18,13 @@ Every message needs, in order: Callers may match on the message text, so stability matters. Aim for one sentence. -| ✗ / ✓ | Message | Notes | -| --- | --- | --- | -| ✗ | `Error: invalid component` | No rule, no saw, no where. | -| ✗ | `The "name" component of type "npm" failed validation because the provided value "" is empty, which is not allowed because names are required; please provide a non-empty name.` | Restates the rule three times. | -| ✓ | `npm "name" component is required` | Rule + where + implied saw (missing). Six words. | -| ✗ | `Error: bad name` | No rule. | -| ✓ | `name "__proto__" cannot start with an underscore` | Rule, where (`name`), saw (`__proto__`), fix implied. | +| ✗ / ✓ | Message | Notes | +| ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | +| ✗ | `Error: invalid component` | No rule, no saw, no where. | +| ✗ | `The "name" component of type "npm" failed validation because the provided value "" is empty, which is not allowed because names are required; please provide a non-empty name.` | Restates the rule three times. | +| ✓ | `npm "name" component is required` | Rule + where + implied saw (missing). Six words. | +| ✗ | `Error: bad name` | No rule. | +| ✓ | `name "__proto__" cannot start with an underscore` | Rule, where (`name`), saw (`__proto__`), fix implied. | ## Validator / config / build-tool errors (verbose) @@ -42,7 +42,7 @@ Breakdown: - **Saw vs. wanted**: saw = missing; wanted = a single-word lowercase filename, with `"parsing"` as a concrete model. - **Fix**: `Add … to this part` — imperative, specific. -The trailing `to route //part/3 at publish time` is optional. Include a *why* clause only when the rule is non-obvious; skip it for rules the reader already knows (e.g. "names can't start with an underscore"). +The trailing `to route //part/3 at publish time` is optional. Include a _why_ clause only when the rule is non-obvious; skip it for rules the reader already knows (e.g. "names can't start with an underscore"). ## Programmatic errors (terse, rule only) @@ -98,6 +98,62 @@ Both wrap `Intl.ListFormat`, so the Oxford comma and one-/two-item cases come ou Use `joinOr` whenever the error is "must be one of X", `joinAnd` whenever it's "all of X are required / missing / in conflict". +## Working with caught values + +`catch (e)` binds `unknown`. The helpers in `@socketsecurity/lib/errors` cover the four patterns that recur everywhere: + +```ts +import { + errorMessage, + errorStack, + isError, + isErrnoException, +} from '@socketsecurity/lib/errors' +``` + +### `isError(value)` — replaces `value instanceof Error` + +Cross-realm-safe. Uses the native ES2025 `Error.isError` when the engine ships it, falls back to a spec-compliant shim otherwise. Catches Errors from worker threads, `vm` contexts, and iframes that same-realm `instanceof Error` silently misses. + +- ✗ `if (e instanceof Error) { … }` +- ✓ `if (isError(e)) { … }` + +### `isErrnoException(value)` — replaces `'code' in err` guards + +Narrows to `NodeJS.ErrnoException` (an Error with a string `code` set by libuv/syscalls like `ENOENT`, `EACCES`, `EBUSY`, `EPERM`). Builds on `isError`, so it's also cross-realm-safe, and it checks that `code` is a string — a merely branded Error without a real errno code returns `false`. + +- ✗ `if (e && typeof e === 'object' && 'code' in e && e.code === 'ENOENT') { … }` +- ✓ `if (isErrnoException(e) && e.code === 'ENOENT') { … }` + +### `errorMessage(value)` — replaces the `instanceof Error ? e.message : String(e)` pattern + +Walks the `cause` chain via `messageWithCauses`, coerces primitives and objects to string, and returns the shared `UNKNOWN_ERROR` sentinel (the string `'Unknown error'`) for `null`, `undefined`, empty strings, `[object Object]`, or Errors with no message. + +That last bullet is the important one: **every `|| 'Unknown error'` fallback in the fleet should collapse into a single `errorMessage(e)` call.** + +- ✗ `` `Failed: ${e instanceof Error ? e.message : String(e)}` `` +- ✗ `` `Failed: ${(e as Error)?.message ?? 'Unknown error'}` `` +- ✗ `` `Failed: ${e instanceof Error ? e.message : 'Unknown error'}` `` +- ✓ `` `Failed: ${errorMessage(e)}` `` + +When you want to preserve the cause chain upstream (recommended), pair it with `{ cause }`: + +```ts +try { + await readConfig(path) +} catch (e) { + throw new Error(`Failed to read ${path}: ${errorMessage(e)}`, { cause: e }) +} +``` + +### `errorStack(value)` — cause-aware stack, or `undefined` + +Returns the cause-walking stack for Errors; returns `undefined` for non-Errors so logger calls stay safe: + +```ts +logger.error(`rebuild failed: ${errorMessage(e)}`, { stack: errorStack(e) }) +``` + ## Voice & tone - Imperative for the fix: `rename`, `add`, `remove`, `set`. diff --git a/packages/cli/src/cli-entry.mts b/packages/cli/src/cli-entry.mts index f63729539..3be9d39d4 100755 --- a/packages/cli/src/cli-entry.mts +++ b/packages/cli/src/cli-entry.mts @@ -1,6 +1,7 @@ #!/usr/bin/env node // Set global Socket theme for consistent CLI branding. +import { isError } from '@socketsecurity/lib/errors' import { setTheme } from '@socketsecurity/lib/themes' setTheme('socket') @@ -266,7 +267,7 @@ process.on('unhandledRejection', async (reason, promise) => { } // Track CLI error for unhandled rejection. - const error = reason instanceof Error ? reason : new Error(String(reason)) + const error = isError(reason) ? reason : new Error(String(reason)) await trackCliError(process.argv, cliStartTime, error, 1) // Finalize telemetry before exit. diff --git a/packages/cli/src/commands/analytics/AnalyticsRenderer.mts b/packages/cli/src/commands/analytics/AnalyticsRenderer.mts index 655f0febb..50df269b9 100644 --- a/packages/cli/src/commands/analytics/AnalyticsRenderer.mts +++ b/packages/cli/src/commands/analytics/AnalyticsRenderer.mts @@ -5,6 +5,7 @@ * This is a proof-of-concept implementation for the hybrid Ink/iocraft approach. */ +import { errorMessage } from '@socketsecurity/lib/errors' import { getDefaultLogger } from '@socketsecurity/lib/logger' import { Box, Text, print } from '../../utils/terminal/iocraft.mts' @@ -118,7 +119,7 @@ export function displayAnalyticsWithIocraft(data: FormattedData): void { print(tree) } catch (e) { process.exitCode = 1 - logger.error('Error rendering analytics:', e instanceof Error ? e.message : String(e)) + logger.error('Error rendering analytics:', errorMessage(e)) logger.warn('Falling back to plain text output') logger.log(`Top 5 Alert Types: ${Object.keys(data.top_five_alert_types).length} types`) logger.log(`Critical Alerts: ${Object.keys(data.total_critical_alerts).length} dates`) diff --git a/packages/cli/src/commands/audit-log/AuditLogRenderer.mts b/packages/cli/src/commands/audit-log/AuditLogRenderer.mts index cb7f197e3..131949503 100644 --- a/packages/cli/src/commands/audit-log/AuditLogRenderer.mts +++ b/packages/cli/src/commands/audit-log/AuditLogRenderer.mts @@ -4,6 +4,7 @@ * Non-interactive renderer for audit log data using iocraft native bindings. */ +import { errorMessage } from '@socketsecurity/lib/errors' import { getDefaultLogger } from '@socketsecurity/lib/logger' import { Box, Text, print } from '../../utils/terminal/iocraft.mts' @@ -146,7 +147,7 @@ export function displayAuditLogWithIocraft({ print(tree) } catch (e) { process.exitCode = 1 - logger.error('Error rendering audit log:', e instanceof Error ? e.message : String(e)) + logger.error('Error rendering audit log:', errorMessage(e)) logger.warn('Falling back to plain text output') logger.log(`Organization: ${orgSlug}`) logger.log(`Entries: ${results.length}`) diff --git a/packages/cli/src/commands/manifest/convert-gradle-to-maven.mts b/packages/cli/src/commands/manifest/convert-gradle-to-maven.mts index 319b41feb..73cddf340 100644 --- a/packages/cli/src/commands/manifest/convert-gradle-to-maven.mts +++ b/packages/cli/src/commands/manifest/convert-gradle-to-maven.mts @@ -1,6 +1,7 @@ import fs from 'node:fs' import path from 'node:path' +import { errorMessage } from '@socketsecurity/lib/errors' import { getDefaultLogger } from '@socketsecurity/lib/logger' import { spawn } from '@socketsecurity/lib/spawn' import { getDefaultSpinner } from '@socketsecurity/lib/spinner' @@ -128,13 +129,13 @@ export async function convertGradleToMaven({ }, } } catch (e) { - const errorMessage = + const summary = 'There was an unexpected error while generating manifests' + (verbose ? '' : ' (use --verbose for details)') if (isTextMode) { process.exitCode = 1 - logger.fail(errorMessage) + logger.fail(summary) if (verbose) { logger.group('[VERBOSE] error:') logger.log(e) @@ -144,8 +145,8 @@ export async function convertGradleToMaven({ return { ok: false, - message: errorMessage, - cause: e instanceof Error ? e.message : String(e), + message: summary, + cause: errorMessage(e), } } } diff --git a/packages/cli/src/commands/manifest/convert-sbt-to-maven.mts b/packages/cli/src/commands/manifest/convert-sbt-to-maven.mts index ba89c4e50..b2808c703 100644 --- a/packages/cli/src/commands/manifest/convert-sbt-to-maven.mts +++ b/packages/cli/src/commands/manifest/convert-sbt-to-maven.mts @@ -1,3 +1,4 @@ +import { errorMessage } from '@socketsecurity/lib/errors' import { safeReadFile } from '@socketsecurity/lib/fs' import { getDefaultLogger } from '@socketsecurity/lib/logger' import { spawn } from '@socketsecurity/lib/spawn' @@ -134,14 +135,14 @@ export async function convertSbtToMaven({ }, } } catch (e) { - const errorMessage = + const summary = 'There was an unexpected error while running this' + (verbose ? '' : ' (use --verbose for details)') if (isTextMode) { process.exitCode = 1 spinner?.stop() - logger.fail(errorMessage) + logger.fail(summary) if (verbose) { logger.group('[VERBOSE] error:') logger.log(e) @@ -151,8 +152,8 @@ export async function convertSbtToMaven({ return { ok: false, - message: errorMessage, - cause: e instanceof Error ? e.message : String(e), + message: summary, + cause: errorMessage(e), } } } diff --git a/packages/cli/src/commands/threat-feed/ThreatFeedRenderer.mts b/packages/cli/src/commands/threat-feed/ThreatFeedRenderer.mts index eeec52d3b..5509dc4cb 100644 --- a/packages/cli/src/commands/threat-feed/ThreatFeedRenderer.mts +++ b/packages/cli/src/commands/threat-feed/ThreatFeedRenderer.mts @@ -4,6 +4,7 @@ * Non-interactive renderer for threat feed data using iocraft native bindings. */ +import { errorMessage } from '@socketsecurity/lib/errors' import { getDefaultLogger } from '@socketsecurity/lib/logger' import { Box, Text, print } from '../../utils/terminal/iocraft.mts' @@ -225,7 +226,7 @@ export function displayThreatFeedWithIocraft({ print(tree) } catch (e) { process.exitCode = 1 - logger.error('Error rendering threat feed:', e instanceof Error ? e.message : String(e)) + logger.error('Error rendering threat feed:', errorMessage(e)) logger.warn('Falling back to plain text output') logger.log(`Total threats: ${results.length}`) results.slice(0, 10).forEach((threat, i) => { diff --git a/packages/cli/src/utils/basics/spawn.mts b/packages/cli/src/utils/basics/spawn.mts index ac3689932..1cecaed3a 100644 --- a/packages/cli/src/utils/basics/spawn.mts +++ b/packages/cli/src/utils/basics/spawn.mts @@ -10,6 +10,7 @@ import path from 'node:path' import { debug } from '@socketsecurity/lib/debug' import { normalizePath } from '@socketsecurity/lib/paths/normalize' +import { errorMessage } from '@socketsecurity/lib/errors' import { spawn } from '@socketsecurity/lib/spawn' import { WIN32 } from '@socketsecurity/lib/constants/platform' @@ -433,7 +434,7 @@ async function parseSocketFacts(factsPath: string): Promise<{ } catch (parseError) { debug('error', 'Failed to parse socket facts JSON:', parseError) return { - error: `Invalid JSON: ${parseError instanceof Error ? parseError.message : String(parseError)}`, + error: `Invalid JSON: ${errorMessage(parseError)}`, } } @@ -447,7 +448,7 @@ async function parseSocketFacts(factsPath: string): Promise<{ } catch (e) { debug('error', 'Failed to read socket facts file:', e) return { - error: `File read error: ${e instanceof Error ? e.message : String(e)}`, + error: `File read error: ${errorMessage(e)}`, } } } diff --git a/packages/cli/src/utils/command/registry-core.mts b/packages/cli/src/utils/command/registry-core.mts index 71f9fe234..a10e31088 100644 --- a/packages/cli/src/utils/command/registry-core.mts +++ b/packages/cli/src/utils/command/registry-core.mts @@ -9,7 +9,7 @@ import type { MiddlewareFn, } from './registry-types.mjs' import type { CResult } from '../../types.mts' - +import { errorMessage, errorStack } from '@socketsecurity/lib/errors' /** * Central registry for CLI commands. * Handles registration, discovery, execution, and middleware. @@ -137,8 +137,8 @@ export class CommandRegistry implements ICommandRegistry { } catch (e) { return { ok: false, - message: e instanceof Error ? e.message : String(e), - cause: e instanceof Error ? e.stack : undefined, + message: errorMessage(e), + cause: errorStack(e), } } @@ -171,8 +171,8 @@ export class CommandRegistry implements ICommandRegistry { } catch (e) { return { ok: false, - message: e instanceof Error ? e.message : String(e), - cause: e instanceof Error ? e.stack : undefined, + message: errorMessage(e), + cause: errorStack(e), } } } diff --git a/packages/cli/src/utils/debug.mts b/packages/cli/src/utils/debug.mts index 9a2f9a917..738f8b306 100644 --- a/packages/cli/src/utils/debug.mts +++ b/packages/cli/src/utils/debug.mts @@ -17,7 +17,6 @@ * to reduce noise. Enable them explicitly when needed for deep debugging. */ -import { UNKNOWN_ERROR } from '@socketsecurity/lib/constants/core' import { debug, debugCache, @@ -27,7 +26,7 @@ import { isDebug, isDebugNs, } from '@socketsecurity/lib/debug' - +import { errorMessage } from '@socketsecurity/lib/errors' export type ApiRequestDebugInfo = { durationMs?: number | undefined headers?: Record | undefined @@ -162,7 +161,7 @@ export function debugApiResponse( buildApiDebugDetails( { endpoint, - error: error instanceof Error ? error.message : UNKNOWN_ERROR, + error: errorMessage(error), }, requestInfo, ), @@ -192,7 +191,7 @@ export function debugFileOp( debugDir({ operation, filepath, - error: error instanceof Error ? error.message : UNKNOWN_ERROR, + error: errorMessage(error), }) /* c8 ignore next 3 */ } else if (isDebugNs('silly')) { @@ -246,7 +245,7 @@ export function debugConfig( if (error) { debugDir({ source, - error: error instanceof Error ? error.message : UNKNOWN_ERROR, + error: errorMessage(error), }) } else if (found) { debug(`Config loaded: ${source}`) diff --git a/packages/cli/src/utils/error/display.mts b/packages/cli/src/utils/error/display.mts index 163befe25..c99fa8175 100644 --- a/packages/cli/src/utils/error/display.mts +++ b/packages/cli/src/utils/error/display.mts @@ -2,7 +2,7 @@ import colors from 'yoctocolors-cjs' -import { messageWithCauses } from '@socketsecurity/lib/errors' +import { errorMessage, isError, messageWithCauses } from '@socketsecurity/lib/errors' import { LOG_SYMBOLS } from '@socketsecurity/lib/logger' import { stripAnsi } from '@socketsecurity/lib/strings' @@ -37,7 +37,7 @@ function appendCauseChain(baseMessage: string, cause: unknown): string { return baseMessage } const causeText = - cause instanceof Error ? messageWithCauses(cause) : String(cause) + isError(cause) ? messageWithCauses(cause) : String(cause) return `${baseMessage}: ${causeText}` } @@ -92,7 +92,7 @@ export function formatErrorForDisplay( title = 'Invalid input' message = appendCauseChain(error.message, error.cause) body = error.body - } else if (error instanceof Error) { + } else if (isError(error)) { title = opts.title || 'Unexpected error' message = appendCauseChain(error.message, error.cause) @@ -115,16 +115,14 @@ export function formatErrorForDisplay( while (currentCause && depth <= 5) { const causeMessage = - currentCause instanceof Error - ? currentCause.message - : String(currentCause) + errorMessage(currentCause) causeLines.push( `\n${colors.dim(`Caused by [${depth}]:`)} ${colors.yellow(causeMessage)}`, ) if ( - currentCause instanceof Error && + isError(currentCause) && currentCause.stack && depth === 1 ) { @@ -137,7 +135,7 @@ export function formatErrorForDisplay( } currentCause = - currentCause instanceof Error ? currentCause.cause : undefined + isError(currentCause) ? currentCause.cause : undefined depth++ } @@ -166,7 +164,7 @@ export function formatErrorForDisplay( * Perfect for inline error display without overwhelming output. */ export function formatErrorCompact(error: unknown): string { - if (error instanceof Error) { + if (isError(error)) { return error.message } if (typeof error === 'string') { @@ -276,7 +274,7 @@ export function formatExternalCliError( .split('\n') .map(line => ` ${line}`) lines.push('', colors.dim('Error output:'), ...stderrLines) - } else if (error instanceof Error) { + } else if (isError(error)) { lines.push(` ${error.message}`) } diff --git a/packages/cli/src/utils/error/errors.mts b/packages/cli/src/utils/error/errors.mts index f61176fd7..b398f3783 100644 --- a/packages/cli/src/utils/error/errors.mts +++ b/packages/cli/src/utils/error/errors.mts @@ -26,6 +26,8 @@ import { } from '@socketsecurity/lib/constants/core' import { debugNs } from '@socketsecurity/lib/debug' +import { isError, isErrnoException } from '@socketsecurity/lib/errors' +export { isErrnoException } from '@socketsecurity/lib/errors' import { SOCKET_DASHBOARD_URL, SOCKET_PRICING_URL, @@ -241,15 +243,6 @@ export function captureExceptionSync( return Sentry.captureException(exception, hint) as string } -export function isErrnoException( - value: unknown, -): value is NodeJS.ErrnoException { - if (!(value instanceof Error)) { - return false - } - return (value as NodeJS.ErrnoException).code !== undefined -} - /** * Type guard to check if an error has recovery suggestions. */ @@ -257,7 +250,7 @@ export function hasRecoverySuggestions( error: unknown, ): error is Error & { recovery: string[] } { return ( - error instanceof Error && + isError(error) && 'recovery' in error && Array.isArray((error as any).recovery) ) diff --git a/packages/cli/src/utils/git/github.mts b/packages/cli/src/utils/git/github.mts index 9b20f9c53..81766a5fe 100644 --- a/packages/cli/src/utils/git/github.mts +++ b/packages/cli/src/utils/git/github.mts @@ -36,6 +36,7 @@ import { Octokit } from '@octokit/rest' import { LRUCache } from 'lru-cache' import { debugDirNs, debugNs, isDebugNs } from '@socketsecurity/lib/debug' +import { errorMessage, isError } from '@socketsecurity/lib/errors' import { readJson, safeMkdir, writeJson } from '@socketsecurity/lib/fs' import { spawn } from '@socketsecurity/lib/spawn' import { parseUrl } from '@socketsecurity/lib/url' @@ -564,7 +565,7 @@ export function handleGitHubApiError( } // Network errors (ECONNREFUSED, ETIMEDOUT, etc.). - if (e instanceof Error) { + if (isError(e)) { const code = (e as NodeJS.ErrnoException).code if ( code === 'ECONNREFUSED' || @@ -588,7 +589,7 @@ export function handleGitHubApiError( return { ok: false, message: 'GitHub API error', - cause: `Unexpected error while ${context}: ${e instanceof Error ? e.message : String(e)}`, + cause: `Unexpected error while ${context}: ${errorMessage(e)}`, } } diff --git a/packages/cli/src/utils/git/gitlab-provider.mts b/packages/cli/src/utils/git/gitlab-provider.mts index f43619ec0..9976ccdc0 100644 --- a/packages/cli/src/utils/git/gitlab-provider.mts +++ b/packages/cli/src/utils/git/gitlab-provider.mts @@ -1,6 +1,7 @@ import { Gitlab } from '@gitbeaker/rest' import { debug, debugDir } from '@socketsecurity/lib/debug' +import { isError } from '@socketsecurity/lib/errors' import { isNonEmptyString } from '@socketsecurity/lib/strings' import { formatErrorWithDetail } from '../error/errors.mts' @@ -62,7 +63,7 @@ export class GitLabProvider implements PrProvider { } } catch (e) { let message = `Failed to create merge request (attempt ${attempt}/${retries})` - if (e instanceof Error) { + if (isError(e)) { message += `: ${e.message}` } diff --git a/packages/cli/src/utils/process/os.mts b/packages/cli/src/utils/process/os.mts index 825f30a6c..60efa59ec 100644 --- a/packages/cli/src/utils/process/os.mts +++ b/packages/cli/src/utils/process/os.mts @@ -28,6 +28,7 @@ import fs from 'node:fs' +import { errorMessage } from '@socketsecurity/lib/errors' import { getDefaultLogger } from '@socketsecurity/lib/logger' import { spawn } from '@socketsecurity/lib/spawn' @@ -235,7 +236,7 @@ async function clearQuarantine(filePath: string): Promise { logger.log('Cleared quarantine attribute') } catch (e) { logger.log( - `Failed to clear quarantine: ${e instanceof Error ? e.message : String(e)}`, + `Failed to clear quarantine: ${errorMessage(e)}`, ) } } @@ -254,7 +255,7 @@ async function ensureExecutable(filePath: string): Promise { logger.log('Set executable permissions') } catch (e) { logger.warn( - `Failed to set executable permissions: ${e instanceof Error ? e.message : String(e)}`, + `Failed to set executable permissions: ${errorMessage(e)}`, ) } } diff --git a/packages/cli/src/utils/telemetry/integration.mts b/packages/cli/src/utils/telemetry/integration.mts index cc43f925d..c6710a341 100644 --- a/packages/cli/src/utils/telemetry/integration.mts +++ b/packages/cli/src/utils/telemetry/integration.mts @@ -43,6 +43,7 @@ import { homedir } from 'node:os' import process from 'node:process' import { debugNs } from '@socketsecurity/lib/debug' +import { isError } from '@socketsecurity/lib/errors' import { escapeRegExp } from '@socketsecurity/lib/regexps' import { TelemetryService } from './service.mts' @@ -218,7 +219,7 @@ function normalizeExitCode( * @returns Error object. */ function normalizeError(error: unknown): Error { - return error instanceof Error ? error : new Error(String(error)) + return isError(error) ? error : new Error(String(error)) } /** diff --git a/packages/cli/src/utils/telemetry/service.mts b/packages/cli/src/utils/telemetry/service.mts index f5217241d..4ad072b44 100644 --- a/packages/cli/src/utils/telemetry/service.mts +++ b/packages/cli/src/utils/telemetry/service.mts @@ -51,6 +51,7 @@ import { setupSdk } from '../socket/sdk.mts' import type { TelemetryEvent } from './types.mts' import type { InspectOptions } from '@socketsecurity/lib/debug' +import { errorMessage } from '@socketsecurity/lib/errors' import type { SocketSdkSuccessResult } from '@socketsecurity/sdk' type TelemetryConfig = SocketSdkSuccessResult<'getOrgTelemetryConfig'>['data'] @@ -349,11 +350,11 @@ export class TelemetryService { ) } catch (e) { const flushDuration = Date.now() - flushStartTime - const errorMessage = e instanceof Error ? e.message : String(e) + const errMsg = errorMessage(e) // Check if this is a timeout error. if ( - errorMessage.includes('timed out') || + errMsg.includes('timed out') || flushDuration >= TELEMETRY_SERVICE_CONFIG.flush_timeout ) { debug( @@ -361,7 +362,7 @@ export class TelemetryService { ) debug(`Failed to send ${eventsToSend.length} events due to timeout`) } else { - debug(`Error flushing telemetry: ${errorMessage}`) + debug(`Error flushing telemetry: ${errMsg}`) debug(`Failed to send ${eventsToSend.length} events due to error`) } // Events are discarded on error to prevent infinite growth. @@ -457,11 +458,11 @@ export class TelemetryService { debug(`Events flushed successfully during destroy (${flushDuration}ms)`) } catch (e) { const flushDuration = Date.now() - flushStartTime - const errorMessage = e instanceof Error ? e.message : String(e) + const errMsg = errorMessage(e) // Check if this is a timeout error. if ( - errorMessage.includes('timed out') || + errMsg.includes('timed out') || flushDuration >= TELEMETRY_SERVICE_CONFIG.flush_timeout ) { debug( @@ -471,7 +472,7 @@ export class TelemetryService { `Failed to send ${eventsToFlush.length} events during destroy due to timeout`, ) } else { - debug(`Error flushing telemetry during destroy: ${errorMessage}`) + debug(`Error flushing telemetry during destroy: ${errMsg}`) debug( `Failed to send ${eventsToFlush.length} events during destroy due to error`, ) diff --git a/packages/cli/src/utils/update/checker.mts b/packages/cli/src/utils/update/checker.mts index 3b72b7d9f..7038890c9 100644 --- a/packages/cli/src/utils/update/checker.mts +++ b/packages/cli/src/utils/update/checker.mts @@ -26,6 +26,7 @@ import semver from 'semver' import { NPM_REGISTRY_URL } from '@socketsecurity/lib/constants/agents' import { debug } from '@socketsecurity/lib/debug' +import { errorMessage } from '@socketsecurity/lib/errors' import { getDefaultLogger } from '@socketsecurity/lib/logger' import { onExit } from '@socketsecurity/lib/signal-exit' import { isNonEmptyString } from '@socketsecurity/lib/strings' @@ -174,7 +175,7 @@ const NetworkUtils = { } reject( new Error( - `Failed to parse JSON response: ${parseError instanceof Error ? parseError.message : String(parseError)}`, + `Failed to parse JSON response: ${errorMessage(parseError)}`, ), ) } @@ -251,7 +252,7 @@ const NetworkUtils = { if (isLastAttempt) { logger.warn( - `Failed to fetch version after ${maxAttempts} attempts: ${e instanceof Error ? e.message : String(e)}`, + `Failed to fetch version after ${maxAttempts} attempts: ${errorMessage(e)}`, ) throw e } @@ -259,7 +260,7 @@ const NetworkUtils = { // Exponential backoff with cap to prevent integer overflow. const delay = Math.min(baseDelay * 2 ** (attempts - 1), 60_000) logger.log( - `Attempt ${attempts} failed, retrying in ${delay}ms: ${e instanceof Error ? e.message : String(e)}`, + `Attempt ${attempts} failed, retrying in ${delay}ms: ${errorMessage(e)}`, ) // eslint-disable-next-line no-await-in-loop @@ -310,7 +311,7 @@ async function checkForUpdates( } } catch (e) { logger.log( - `Failed to check for updates: ${e instanceof Error ? e.message : String(e)}`, + `Failed to check for updates: ${errorMessage(e)}`, ) throw e } diff --git a/packages/cli/src/utils/update/manager.mts b/packages/cli/src/utils/update/manager.mts index 3214c8dd2..79c8851f9 100644 --- a/packages/cli/src/utils/update/manager.mts +++ b/packages/cli/src/utils/update/manager.mts @@ -26,6 +26,7 @@ */ import { dlxManifest } from '@socketsecurity/lib/dlx/manifest' +import { errorMessage } from '@socketsecurity/lib/errors' import { getDefaultLogger } from '@socketsecurity/lib/logger' import { isNonEmptyString } from '@socketsecurity/lib/strings' @@ -167,7 +168,7 @@ export async function checkForUpdates( } } catch (e) { loggerLocal.warn( - `Failed to access cache: ${e instanceof Error ? e.message : String(e)}`, + `Failed to access cache: ${errorMessage(e)}`, ) record = undefined } @@ -201,13 +202,13 @@ export async function checkForUpdates( }) } catch (e) { loggerLocal.warn( - `Failed to update cache: ${e instanceof Error ? e.message : String(e)}`, + `Failed to update cache: ${errorMessage(e)}`, ) // Continue anyway - cache update failure is not critical. } } catch (e) { loggerLocal.log( - `Failed to fetch latest version: ${e instanceof Error ? e.message : String(e)}`, + `Failed to fetch latest version: ${errorMessage(e)}`, ) // Use cached version if available. @@ -261,12 +262,12 @@ export async function checkForUpdates( }) } catch (e) { loggerLocal.warn( - `Failed to update notification timestamp: ${e instanceof Error ? e.message : String(e)}`, + `Failed to update notification timestamp: ${errorMessage(e)}`, ) } } catch (e) { loggerLocal.warn( - `Failed to set up notification: ${e instanceof Error ? e.message : String(e)}`, + `Failed to set up notification: ${errorMessage(e)}`, ) // Notification failure is not critical - update is still available. } @@ -299,7 +300,7 @@ export async function scheduleUpdateCheck( } catch (e) { // Silent failure - update checks should never block the main CLI. logger.log( - `Update check failed: ${e instanceof Error ? e.message : String(e)}`, + `Update check failed: ${errorMessage(e)}`, ) } } diff --git a/packages/cli/src/utils/update/notifier.mts b/packages/cli/src/utils/update/notifier.mts index 58fd5771d..deaa80fb8 100644 --- a/packages/cli/src/utils/update/notifier.mts +++ b/packages/cli/src/utils/update/notifier.mts @@ -24,6 +24,7 @@ import process from 'node:process' import colors from 'yoctocolors-cjs' +import { errorMessage } from '@socketsecurity/lib/errors' import { getDefaultLogger } from '@socketsecurity/lib/logger' import { onExit } from '@socketsecurity/lib/signal-exit' import { isNonEmptyString } from '@socketsecurity/lib/strings' @@ -135,7 +136,7 @@ export function scheduleExitNotification( onExit(notificationLogger) } catch (e) { logger.warn( - `Failed to schedule exit notification: ${e instanceof Error ? e.message : String(e)}`, + `Failed to schedule exit notification: ${errorMessage(e)}`, ) } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index adc83df24..c0e3dde03 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -254,7 +254,7 @@ overrides: '@octokit/graphql': 9.0.1 '@octokit/request-error': 7.0.0 '@sigstore/sign': 4.1.0 - '@socketsecurity/lib': 5.21.0 + '@socketsecurity/lib': 5.24.0 aggregate-error: npm:@socketregistry/aggregate-error@^1.0.15 ansi-regex: 6.2.2 brace-expansion: 5.0.5 @@ -393,8 +393,8 @@ importers: specifier: 'catalog:' version: 3.0.1 '@socketsecurity/lib': - specifier: 5.21.0 - version: 5.21.0(typescript@5.9.3) + specifier: 5.24.0 + version: 5.24.0(typescript@5.9.3) '@socketsecurity/registry': specifier: 'catalog:' version: 2.0.2(typescript@5.9.3) @@ -573,8 +573,8 @@ importers: specifier: 'catalog:' version: 1.4.2 '@socketsecurity/lib': - specifier: 5.21.0 - version: 5.21.0(typescript@5.9.3) + specifier: 5.24.0 + version: 5.24.0(typescript@5.9.3) '@socketsecurity/sdk': specifier: 'catalog:' version: 4.0.1 @@ -586,8 +586,8 @@ importers: .claude/hooks/setup-security-tools: dependencies: '@socketsecurity/lib': - specifier: 5.21.0 - version: 5.21.0(typescript@5.9.3) + specifier: 5.24.0 + version: 5.24.0(typescript@5.9.3) packages/build-infra: dependencies: @@ -598,8 +598,8 @@ importers: specifier: 'catalog:' version: 7.28.4 '@socketsecurity/lib': - specifier: 5.21.0 - version: 5.21.0(typescript@5.9.3) + specifier: 5.24.0 + version: 5.24.0(typescript@5.9.3) magic-string: specifier: 'catalog:' version: 0.30.19 @@ -655,8 +655,8 @@ importers: specifier: 'catalog:' version: 3.0.1 '@socketsecurity/lib': - specifier: 5.21.0 - version: 5.21.0(typescript@5.9.3) + specifier: 5.24.0 + version: 5.24.0(typescript@5.9.3) '@socketsecurity/registry': specifier: 'catalog:' version: 2.0.2(typescript@5.9.3) @@ -772,8 +772,8 @@ importers: packages/package-builder: dependencies: '@socketsecurity/lib': - specifier: 5.21.0 - version: 5.21.0(typescript@5.9.3) + specifier: 5.24.0 + version: 5.24.0(typescript@5.9.3) build-infra: specifier: workspace:* version: link:../build-infra @@ -2151,6 +2151,7 @@ packages: '@socketaddon/iocraft@file:packages/package-builder/build/dev/out/socketaddon-iocraft': resolution: {directory: packages/package-builder/build/dev/out/socketaddon-iocraft, type: directory} + engines: {node: '>=18'} '@socketregistry/es-set-tostringtag@1.0.10': resolution: {integrity: sha512-btXmvw1JpA8WtSoXx9mTapo9NAyIDKRRzK84i48d8zc0X09M6ORfobVnHbgwhXf7CFhkRzhYrHG9dqbI9vpELQ==} @@ -2209,8 +2210,8 @@ packages: resolution: {integrity: sha512-kLKdSqi4W7SDSm5z+wYnfVRnZCVhxzbzuKcdOZSrcHoEGOT4Gl844uzoaML+f5eiQMxY+nISiETwRph/aXrIaQ==} engines: {node: 18.20.7 || ^20.18.3 || >=22.14.0} - '@socketsecurity/lib@5.21.0': - resolution: {integrity: sha512-cSqdq2kOBSuH3u8rfDhViCrN7IJPqzAvzklUYrEFhohUgJkky0+YYQ/gbSwRehZDGY8mqv+6lKGrt4OKWnNsdQ==} + '@socketsecurity/lib@5.24.0': + resolution: {integrity: sha512-4Yar8oo4N12ESoNt/i2PNf08HRABUC0OcfUfwzIF3xjq89E5VMDN+aeOtnn6Oo4Y6u3TiuZRG7NgEBZ83LQ1Lw==} engines: {node: '>=22', pnpm: '>=11.0.0-rc.0'} peerDependencies: typescript: '>=5.0.0' @@ -5699,7 +5700,7 @@ snapshots: pony-cause: 2.1.11 yaml: 2.8.1 - '@socketsecurity/lib@5.21.0(typescript@5.9.3)': + '@socketsecurity/lib@5.24.0(typescript@5.9.3)': optionalDependencies: typescript: 5.9.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3640440d3..12bdc1a56 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -47,7 +47,7 @@ catalog: '@socketregistry/packageurl-js': 1.4.2 '@socketregistry/yocto-spinner': 1.0.25 '@socketsecurity/config': 3.0.1 - '@socketsecurity/lib': 5.21.0 + '@socketsecurity/lib': 5.24.0 '@socketsecurity/registry': 2.0.2 '@socketsecurity/sdk': 4.0.1 '@types/adm-zip': 0.5.7 From 480ac2f4248f4c521992c600d80e64d27b31801f Mon Sep 17 00:00:00 2001 From: jdalton Date: Wed, 22 Apr 2026 20:27:58 -0400 Subject: [PATCH 4/5] fix(cli): stop duplicating cause messages in display.mts loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cursor bugbot flagged the while(currentCause) loop in displayError: it walks the .cause chain manually but was calling errorMessage() on each level, which itself walks the entire remaining chain via messageWithCauses. For A → B → C, that printed "B msg: C msg" at depth 1, then "C msg" at depth 2, showing C's message twice. Switch to reading `.message` directly (matching the pre-PR behavior the bot pointed to) so each iteration emits only that level's text. Fall back to `String(currentCause)` for non-Error nodes in the chain. Drop the now-unused `errorMessage` import. Reported on PR #1261. --- packages/cli/src/utils/error/display.mts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/utils/error/display.mts b/packages/cli/src/utils/error/display.mts index c99fa8175..d1597a753 100644 --- a/packages/cli/src/utils/error/display.mts +++ b/packages/cli/src/utils/error/display.mts @@ -2,7 +2,7 @@ import colors from 'yoctocolors-cjs' -import { errorMessage, isError, messageWithCauses } from '@socketsecurity/lib/errors' +import { isError, messageWithCauses } from '@socketsecurity/lib/errors' import { LOG_SYMBOLS } from '@socketsecurity/lib/logger' import { stripAnsi } from '@socketsecurity/lib/strings' @@ -114,8 +114,13 @@ export function formatErrorForDisplay( let depth = 1 while (currentCause && depth <= 5) { - const causeMessage = - errorMessage(currentCause) + // Use .message (or String coercion) here — errorMessage() walks + // the entire remaining cause chain via messageWithCauses, which + // would duplicate messages since the outer while loop is already + // iterating the chain level-by-level. + const causeMessage = isError(currentCause) + ? currentCause.message || String(currentCause) + : String(currentCause) causeLines.push( `\n${colors.dim(`Caused by [${depth}]:`)} ${colors.yellow(causeMessage)}`, From 178568cc0c8fc3af5fe94ca05e1aeac6e1e80ba4 Mon Sep 17 00:00:00 2001 From: jdalton Date: Wed, 22 Apr 2026 20:32:01 -0400 Subject: [PATCH 5/5] test(cli): update debug.test for @socketsecurity/lib/errors 5.24 semantics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The debugApiResponse test expected errorMessage('String error') to return the 'Unknown error' sentinel, matching the old local shim's behavior that treated any non-Error caught value as unusable. The catalog bump to @socketsecurity/lib 5.24 switched debug.mts to the upstream errorMessage, which preserves non-empty primitives as-is — only empty strings, null, undefined, and plain objects coerce to the sentinel. Assert on 'String error' to pin the current contract. --- packages/cli/test/unit/utils/debug.test.mts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/cli/test/unit/utils/debug.test.mts b/packages/cli/test/unit/utils/debug.test.mts index f5d711062..17e417a0e 100644 --- a/packages/cli/test/unit/utils/debug.test.mts +++ b/packages/cli/test/unit/utils/debug.test.mts @@ -130,7 +130,10 @@ describe('debug utilities', () => { expect(mockDebugDirNs).toHaveBeenCalledWith('error', { endpoint: '/api/test', - error: 'Unknown error', + // @socketsecurity/lib/errors preserves non-empty primitives as-is; + // only empty strings / null / undefined / plain objects coerce to + // the Unknown error sentinel. A real string message passes through. + error: 'String error', }) })