Skip to content

Commit e272f33

Browse files
authored
fix(cli): error messages for utils/update + utils/command + error library migration (#1257)
* fix(cli): align utils/update/ + utils/command/ error messages with 4-ingredient strategy Rewrites error messages across packages/cli/src/utils/update/ and packages/cli/src/utils/command/ to follow the What / Where / Saw vs. wanted / Fix strategy from CLAUDE.md. Sources: - utils/update/checker.mts: 8 messages (URL validation, package name / registry URL validation, version-response validation). Each now names the function, the received type, and what a valid value looks like. - utils/update/manager.mts: 3 messages (mirror guards for name / version / ttl). Still warn-and-return-false, but the text now tells the caller exactly which option was wrong. - utils/command/registry-core.mts: 6 messages (command / alias registration conflicts, middleware next() misuse, flag parsing failures). Each now names the offending command, flag name, or index so debuggers don't need to read source. Tests updated: - test/unit/utils/update/checker.test.mts: 6 assertions (switched to regex) - test/unit/utils/update/manager.test.mts: 3 assertions (switched to expect.stringContaining) - test/unit/utils/command/registry-core.test.mts: 5 assertions All 153 tests in the affected suites pass. Follows strategy from #1254. Part of the multi-PR series started by #1255 (commands/) and continued in #1256 (utils/dlx/). * fix(cli): address Cursor bugbot findings on error messages Two issues flagged by Cursor bugbot on #1257: 1. (Medium) registry-core.mts middleware error reported the wrong offending middleware index. `index` tracks the highest dispatched position, not the caller; when dispatch(i) is re-entered after a double-next(), the offender held middleware[i - 1]'s next closure. Fixed to use `i - 1` with a comment explaining why. 2. (Low) checker.mts error referenced a non-existent `UpdateChecker.fetch()` — the object is actually exported as `NetworkUtils`. Renamed in both the error and its test regex. Caught by #1257 bugbot review. * docs(claude): align Error Messages with fleet doctrine, add references doc 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. * 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). * chore: bump @socketsecurity/lib to 5.24.0 and adopt error helpers 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. * fix(cli): stop duplicating cause messages in display.mts loop 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. * test(cli): update debug.test for @socketsecurity/lib/errors 5.24 semantics 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.
1 parent e90bc97 commit e272f33

27 files changed

Lines changed: 374 additions & 130 deletions

CLAUDE.md

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -164,26 +164,40 @@ Follow [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). User-facing onl
164164

165165
### Error Messages
166166

167-
Errors are a UX surface. Every message must let the reader fix the problem without reading the source. Four ingredients, in order:
167+
An error message is UI. The reader should be able to fix the problem from the message alone, without opening your source.
168168

169-
1. **What**: the rule that was violated (the contract, not the symptom)
170-
2. **Where**: the exact flag, file, key, line, or record — never "somewhere in config"
171-
3. **Saw vs. wanted**: the offending value and the allowed shape/set
172-
4. **Fix**: one concrete action to resolve it
169+
Every message needs four ingredients, in order:
173170

174-
- Imperative voice (`pass --limit=50`), not passive (`--limit was wrong`)
175-
- 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
176-
- If two records collide, name both — not just the second one found
177-
- Suggest, don't auto-correct. An error that silently repairs state hides the bug in the next run
171+
1. **What** — the rule that was broken (e.g. "must be a non-negative integer"), not the fallout ("invalid").
172+
2. **Where** — the exact flag, file, line, key, or record. Not "somewhere in config".
173+
3. **Saw vs. wanted** — the bad value and the allowed shape or set.
174+
4. **Fix** — one concrete action, in imperative voice (`pass --limit=50`, not `--limit was wrong`).
178175

179-
**Examples:**
176+
Length depends on the audience:
177+
178+
- **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.
179+
- **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.
180+
- **Programmatic errors** (internal assertions, invariant checks): terse, rule only. No end user will see it; short keeps the check readable.
181+
182+
Rules for every message:
183+
184+
- Imperative voice for the fix — `pass --limit=50`, not `--limit was wrong`.
185+
- Never "invalid" on its own. `Invalid ecosystem: "foo"` is fallout; `--reach-ecosystems must be one of: npm, pypi, maven (saw: "foo")` is a rule.
186+
- On a collision, name **both** sides, not just the second one found.
187+
- Suggest, don't auto-correct. Silently fixing state hides the bug next time.
188+
- Bloat check: if removing a word keeps the information, drop it.
189+
- 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.
190+
191+
**CLI examples:**
180192

181193
-`throw new InputError('--pull-request must be a non-negative integer (saw: "abc"); pass a number like --pull-request=42')`
182194
-`` throw new InputError(`No .socket directory found in ${cwd}; run \`socket init\` to create one`) ``
183195
-`throw new AuthError('Socket API rejected the token (401); run `socket login` or set SOCKET_CLI_API_TOKEN')`
184-
-`throw new InputError('Invalid value for --limit: ${limit}')` (symptom, no rule, no fix)
185-
-`throw new Error('Authentication failed')` (no where, no fix, wrong error type)
186-
-`logger.error('Error occurred'); return` (doesn't set exit code)
196+
-`throw new InputError('Invalid value for --limit: ${limit}')` — fallout, no rule, no fix
197+
-`throw new Error('Authentication failed')` — no where, no fix, wrong error type
198+
-`logger.error('Error occurred'); return` — doesn't set exit code
199+
200+
See `docs/references/error-messages.md` for cross-fleet worked examples and anti-patterns.
187201

188202
### Command Pattern
189203

docs/references/error-messages.md

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
# Error Messages — Worked Examples
2+
3+
Companion to the `## Error Messages` section of `CLAUDE.md`. That section
4+
holds the rules; this file holds longer examples and anti-patterns that
5+
would bloat CLAUDE.md if inlined.
6+
7+
## The four ingredients
8+
9+
Every message needs, in order:
10+
11+
1. **What** — the rule that was broken.
12+
2. **Where** — the exact file, line, key, field, or CLI flag.
13+
3. **Saw vs. wanted** — the bad value and the allowed shape or set.
14+
4. **Fix** — one concrete action, in imperative voice.
15+
16+
## Library API errors (terse)
17+
18+
Callers may match on the message text, so stability matters. Aim for one
19+
sentence.
20+
21+
| ✗ / ✓ | Message | Notes |
22+
| ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- |
23+
|| `Error: invalid component` | No rule, no saw, no where. |
24+
|| `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. |
25+
|| `npm "name" component is required` | Rule + where + implied saw (missing). Six words. |
26+
|| `Error: bad name` | No rule. |
27+
|| `name "__proto__" cannot start with an underscore` | Rule, where (`name`), saw (`__proto__`), fix implied. |
28+
29+
## Validator / config / build-tool errors (verbose)
30+
31+
The reader is looking at a file and wants to fix the record without
32+
re-running the tool. Give each ingredient its own words.
33+
34+
`Error: invalid tour config`
35+
36+
`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 /<slug>/part/3 at publish time.`
37+
38+
Breakdown:
39+
40+
- **What**: `is missing "filename"` — the rule is "each part has a filename".
41+
- **Where**: `tour.json: part 3 ("Parsing & Normalization")` — file + record + human label.
42+
- **Saw vs. wanted**: saw = missing; wanted = a single-word lowercase filename, with `"parsing"` as a concrete model.
43+
- **Fix**: `Add … to this part` — imperative, specific.
44+
45+
The trailing `to route /<slug>/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").
46+
47+
## Programmatic errors (terse, rule only)
48+
49+
Internal assertions and invariant checks. No end user will read them;
50+
terse keeps the assertion readable when you skim the code.
51+
52+
-`assert(queue.length > 0)` with message `queue drained before worker exit`
53+
-`pool size must be positive`
54+
-`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.
55+
56+
## Common anti-patterns
57+
58+
**"Invalid X" with no rule.**
59+
60+
-`Invalid filename 'My Part'`
61+
-`filename 'My Part' must be [a-z]+ (lowercase, no spaces)`
62+
63+
**Passive voice on the fix.**
64+
65+
-`"filename" was missing`
66+
-`add "filename" to part 3`
67+
68+
**Naming only one side of a collision.**
69+
70+
-`duplicate key "foo"` (which record won, which lost?)
71+
-`duplicate key "foo" in config.json (lines 12 and 47) — rename one`
72+
73+
**Silently auto-correcting.**
74+
75+
- ✗ Stripping a trailing slash from a URL and continuing. The next run will hit the same bug; nothing learned.
76+
-`url "https://api/" has a trailing slash — remove it`.
77+
78+
**Bloat that restates the rule.**
79+
80+
-`The value provided for "timeout" is invalid because timeouts must be positive numbers and the value you provided was not a positive number.`
81+
-`timeout must be a positive number (saw: -5)`
82+
83+
## Formatting lists of values
84+
85+
When the error needs to show an allowed set, a list of conflicting
86+
records, or multiple missing fields, use the list formatters from
87+
`@socketsecurity/lib/arrays` rather than hand-joining with commas:
88+
89+
- `joinAnd(['a', 'b', 'c'])``"a, b, and c"` — for conjunctions ("missing foo, bar, and baz")
90+
- `joinOr(['npm', 'pypi', 'maven'])``"npm, pypi, or maven"` — for disjunctions ("must be one of: …")
91+
92+
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"`).
93+
94+
-`--reach-ecosystems must be one of: npm, pypi, maven (saw: "foo")` — hand-joined, breaks if the list has one or two entries.
95+
-`` `--reach-ecosystems must be one of: ${joinOr(ALLOWED)} (saw: "foo")` ``
96+
-`missing keys: filename slug title` — no separators, no grammar.
97+
-`` `missing keys: ${joinAnd(missing)}` ```"missing keys: filename, slug, and title"`
98+
99+
Use `joinOr` whenever the error is "must be one of X", `joinAnd` whenever it's "all of X are required / missing / in conflict".
100+
101+
## Working with caught values
102+
103+
`catch (e)` binds `unknown`. The helpers in `@socketsecurity/lib/errors` cover the four patterns that recur everywhere:
104+
105+
```ts
106+
import {
107+
errorMessage,
108+
errorStack,
109+
isError,
110+
isErrnoException,
111+
} from '@socketsecurity/lib/errors'
112+
```
113+
114+
### `isError(value)` — replaces `value instanceof Error`
115+
116+
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.
117+
118+
-`if (e instanceof Error) { … }`
119+
-`if (isError(e)) { … }`
120+
121+
### `isErrnoException(value)` — replaces `'code' in err` guards
122+
123+
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`.
124+
125+
-`if (e && typeof e === 'object' && 'code' in e && e.code === 'ENOENT') { … }`
126+
-`if (isErrnoException(e) && e.code === 'ENOENT') { … }`
127+
128+
### `errorMessage(value)` — replaces the `instanceof Error ? e.message : String(e)` pattern
129+
130+
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.
131+
132+
That last bullet is the important one: **every `|| 'Unknown error'` fallback in the fleet should collapse into a single `errorMessage(e)` call.**
133+
134+
-`` `Failed: ${e instanceof Error ? e.message : String(e)}` ``
135+
-`` `Failed: ${(e as Error)?.message ?? 'Unknown error'}` ``
136+
-`` `Failed: ${e instanceof Error ? e.message : 'Unknown error'}` ``
137+
-`` `Failed: ${errorMessage(e)}` ``
138+
139+
When you want to preserve the cause chain upstream (recommended), pair it with `{ cause }`:
140+
141+
```ts
142+
try {
143+
await readConfig(path)
144+
} catch (e) {
145+
throw new Error(`Failed to read ${path}: ${errorMessage(e)}`, { cause: e })
146+
}
147+
```
148+
149+
### `errorStack(value)` — cause-aware stack, or `undefined`
150+
151+
Returns the cause-walking stack for Errors; returns `undefined` for non-Errors so logger calls stay safe:
152+
153+
```ts
154+
logger.error(`rebuild failed: ${errorMessage(e)}`, { stack: errorStack(e) })
155+
```
156+
157+
## Voice & tone
158+
159+
- Imperative for the fix: `rename`, `add`, `remove`, `set`.
160+
- Present tense for the rule: `must be`, `cannot`, `is required`.
161+
- No apology ("Sorry, …"), no blame ("You provided …"). State the rule and the fix.
162+
- Don't end with "please"; it doesn't add information and it makes the message feel longer than it is.
163+
164+
## Bloat check
165+
166+
Before shipping a message, cross out any word that, if removed, leaves the information intact. If only rhythm or politeness disappears, drop it.

packages/cli/src/cli-entry.mts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/env node
22

33
// Set global Socket theme for consistent CLI branding.
4+
import { isError } from '@socketsecurity/lib/errors'
45
import { setTheme } from '@socketsecurity/lib/themes'
56
setTheme('socket')
67

@@ -266,7 +267,7 @@ process.on('unhandledRejection', async (reason, promise) => {
266267
}
267268

268269
// Track CLI error for unhandled rejection.
269-
const error = reason instanceof Error ? reason : new Error(String(reason))
270+
const error = isError(reason) ? reason : new Error(String(reason))
270271
await trackCliError(process.argv, cliStartTime, error, 1)
271272

272273
// Finalize telemetry before exit.

packages/cli/src/commands/analytics/AnalyticsRenderer.mts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* This is a proof-of-concept implementation for the hybrid Ink/iocraft approach.
66
*/
77

8+
import { errorMessage } from '@socketsecurity/lib/errors'
89
import { getDefaultLogger } from '@socketsecurity/lib/logger'
910

1011
import { Box, Text, print } from '../../utils/terminal/iocraft.mts'
@@ -118,7 +119,7 @@ export function displayAnalyticsWithIocraft(data: FormattedData): void {
118119
print(tree)
119120
} catch (e) {
120121
process.exitCode = 1
121-
logger.error('Error rendering analytics:', e instanceof Error ? e.message : String(e))
122+
logger.error('Error rendering analytics:', errorMessage(e))
122123
logger.warn('Falling back to plain text output')
123124
logger.log(`Top 5 Alert Types: ${Object.keys(data.top_five_alert_types).length} types`)
124125
logger.log(`Critical Alerts: ${Object.keys(data.total_critical_alerts).length} dates`)

packages/cli/src/commands/audit-log/AuditLogRenderer.mts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* Non-interactive renderer for audit log data using iocraft native bindings.
55
*/
66

7+
import { errorMessage } from '@socketsecurity/lib/errors'
78
import { getDefaultLogger } from '@socketsecurity/lib/logger'
89

910
import { Box, Text, print } from '../../utils/terminal/iocraft.mts'
@@ -146,7 +147,7 @@ export function displayAuditLogWithIocraft({
146147
print(tree)
147148
} catch (e) {
148149
process.exitCode = 1
149-
logger.error('Error rendering audit log:', e instanceof Error ? e.message : String(e))
150+
logger.error('Error rendering audit log:', errorMessage(e))
150151
logger.warn('Falling back to plain text output')
151152
logger.log(`Organization: ${orgSlug}`)
152153
logger.log(`Entries: ${results.length}`)

packages/cli/src/commands/manifest/convert-gradle-to-maven.mts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import fs from 'node:fs'
22
import path from 'node:path'
33

4+
import { errorMessage } from '@socketsecurity/lib/errors'
45
import { getDefaultLogger } from '@socketsecurity/lib/logger'
56
import { spawn } from '@socketsecurity/lib/spawn'
67
import { getDefaultSpinner } from '@socketsecurity/lib/spinner'
@@ -128,13 +129,13 @@ export async function convertGradleToMaven({
128129
},
129130
}
130131
} catch (e) {
131-
const errorMessage =
132+
const summary =
132133
'There was an unexpected error while generating manifests' +
133134
(verbose ? '' : ' (use --verbose for details)')
134135

135136
if (isTextMode) {
136137
process.exitCode = 1
137-
logger.fail(errorMessage)
138+
logger.fail(summary)
138139
if (verbose) {
139140
logger.group('[VERBOSE] error:')
140141
logger.log(e)
@@ -144,8 +145,8 @@ export async function convertGradleToMaven({
144145

145146
return {
146147
ok: false,
147-
message: errorMessage,
148-
cause: e instanceof Error ? e.message : String(e),
148+
message: summary,
149+
cause: errorMessage(e),
149150
}
150151
}
151152
}

packages/cli/src/commands/manifest/convert-sbt-to-maven.mts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { errorMessage } from '@socketsecurity/lib/errors'
12
import { safeReadFile } from '@socketsecurity/lib/fs'
23
import { getDefaultLogger } from '@socketsecurity/lib/logger'
34
import { spawn } from '@socketsecurity/lib/spawn'
@@ -134,14 +135,14 @@ export async function convertSbtToMaven({
134135
},
135136
}
136137
} catch (e) {
137-
const errorMessage =
138+
const summary =
138139
'There was an unexpected error while running this' +
139140
(verbose ? '' : ' (use --verbose for details)')
140141

141142
if (isTextMode) {
142143
process.exitCode = 1
143144
spinner?.stop()
144-
logger.fail(errorMessage)
145+
logger.fail(summary)
145146
if (verbose) {
146147
logger.group('[VERBOSE] error:')
147148
logger.log(e)
@@ -151,8 +152,8 @@ export async function convertSbtToMaven({
151152

152153
return {
153154
ok: false,
154-
message: errorMessage,
155-
cause: e instanceof Error ? e.message : String(e),
155+
message: summary,
156+
cause: errorMessage(e),
156157
}
157158
}
158159
}

packages/cli/src/commands/threat-feed/ThreatFeedRenderer.mts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* Non-interactive renderer for threat feed data using iocraft native bindings.
55
*/
66

7+
import { errorMessage } from '@socketsecurity/lib/errors'
78
import { getDefaultLogger } from '@socketsecurity/lib/logger'
89

910
import { Box, Text, print } from '../../utils/terminal/iocraft.mts'
@@ -225,7 +226,7 @@ export function displayThreatFeedWithIocraft({
225226
print(tree)
226227
} catch (e) {
227228
process.exitCode = 1
228-
logger.error('Error rendering threat feed:', e instanceof Error ? e.message : String(e))
229+
logger.error('Error rendering threat feed:', errorMessage(e))
229230
logger.warn('Falling back to plain text output')
230231
logger.log(`Total threats: ${results.length}`)
231232
results.slice(0, 10).forEach((threat, i) => {

packages/cli/src/utils/basics/spawn.mts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import path from 'node:path'
1010

1111
import { debug } from '@socketsecurity/lib/debug'
1212
import { normalizePath } from '@socketsecurity/lib/paths/normalize'
13+
import { errorMessage } from '@socketsecurity/lib/errors'
1314
import { spawn } from '@socketsecurity/lib/spawn'
1415

1516
import { WIN32 } from '@socketsecurity/lib/constants/platform'
@@ -433,7 +434,7 @@ async function parseSocketFacts(factsPath: string): Promise<{
433434
} catch (parseError) {
434435
debug('error', 'Failed to parse socket facts JSON:', parseError)
435436
return {
436-
error: `Invalid JSON: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
437+
error: `Invalid JSON: ${errorMessage(parseError)}`,
437438
}
438439
}
439440

@@ -447,7 +448,7 @@ async function parseSocketFacts(factsPath: string): Promise<{
447448
} catch (e) {
448449
debug('error', 'Failed to read socket facts file:', e)
449450
return {
450-
error: `File read error: ${e instanceof Error ? e.message : String(e)}`,
451+
error: `File read error: ${errorMessage(e)}`,
451452
}
452453
}
453454
}

0 commit comments

Comments
 (0)