From 37f4a8cc6e2efc02411b503190cbc93677b6a49c Mon Sep 17 00:00:00 2001 From: jdalton Date: Wed, 22 Apr 2026 11:41:48 -0400 Subject: [PATCH 1/3] fix(cli): align utils/ miscellaneous error messages with 4-ingredient strategy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Final PR in the error-message series. Covers everything not already touched by #1255-#1259: utils/basics, utils/config, utils/fs, utils/git, utils/npm, utils/promise, utils/terminal, and the flags module at the root of the CLI tree. Sources: - flags.mts: 2 throws (--max-old-space-size, --max-semi-space-size) — name the flag, show the offending value, suggest a concrete megabyte value. - utils/config.mts: 1 throw (SOCKET_CLI_CONFIG base64 decode) — explains the replacement-character symptom and how to re-encode. - utils/basics/vfs-extract.mts: 4 throws (SEA VFS extraction for Python + security tools) — name the missing paths, the exit codes, and point at the "rebuild the SEA binary" fix. - utils/promise/queue.mts: 1 throw (PromiseQueue concurrency guard) — show the offending value and suggest 4/8. - utils/npm/spec.mts: 1 throw (PURL conversion) — show the input, state what a valid npm spec looks like. - utils/git/operations.mts: 1 throw (git-not-on-PATH) — point at install and the local-path env-var override. - utils/git/gitlab-provider.mts: 2 throws (no token, PR creation after retries) — name the token scope, the retry count, the repo/head refs. - utils/fs/path-resolve.mts: 1 throw (npm path-walk iteration cap) — name the start path, current directory, and what usually causes the cycle (symlinks). - utils/terminal/iocraft.mts: 1 throw (native-module load failure) — show the underlying error and the offending platform/arch triple. Skipped (already informative): - github-provider.mts pass-through errors (forward inner CResult cause/message) - gitlab-provider.mts try/catch wrappers that call formatErrorWithDetail (inner error has context) - 'process.exit called' sentinel throws in npm/pnpm/yarn/with- subcommands paths (test harness re-raise markers, not user-facing) Tests updated: - test/unit/utils/promise/queue.test.mts (2 assertions) - test/unit/utils/npm/spec.test.mts (2 assertions) - test/unit/utils/git/gitlab-provider.test.mts (3 assertions) Full suite (343 files / 5225 tests) passes. Completes the series: #1255 (commands/) → #1256 (utils/dlx/) → #1257 (utils/update + utils/command/) → #1258 (env/ + constants/) → #1259 (test/) → this. --- packages/cli/src/flags.mts | 4 ++-- packages/cli/src/utils/basics/vfs-extract.mts | 10 ++++++---- packages/cli/src/utils/config.mts | 4 +++- packages/cli/src/utils/fs/path-resolve.mts | 2 +- packages/cli/src/utils/git/gitlab-provider.mts | 4 ++-- packages/cli/src/utils/git/operations.mts | 4 +++- packages/cli/src/utils/npm/spec.mts | 4 +++- packages/cli/src/utils/promise/queue.mts | 4 +++- packages/cli/src/utils/terminal/iocraft.mts | 3 +-- .../cli/test/unit/utils/git/gitlab-provider.test.mts | 10 +++++++--- packages/cli/test/unit/utils/npm/spec.test.mts | 4 ++-- packages/cli/test/unit/utils/promise/queue.test.mts | 4 ++-- 12 files changed, 35 insertions(+), 22 deletions(-) diff --git a/packages/cli/src/flags.mts b/packages/cli/src/flags.mts index 2fa9122d6..8fb0d4e3a 100644 --- a/packages/cli/src/flags.mts +++ b/packages/cli/src/flags.mts @@ -64,12 +64,12 @@ function getRawSpaceSizeFlags(): RawSpaceSizeFlags { // Validate numeric flags (should be guaranteed by meow type='number', but defensive). if (Number.isNaN(maxOldSpaceSize) || maxOldSpaceSize < 0) { throw new Error( - `Invalid value for --max-old-space-size: ${cli.flags['maxOldSpaceSize']}`, + `--max-old-space-size must be a non-negative integer in megabytes (saw: "${cli.flags['maxOldSpaceSize']}"); pass a whole number like --max-old-space-size=4096 for 4GB`, ) } if (Number.isNaN(maxSemiSpaceSize) || maxSemiSpaceSize < 0) { throw new Error( - `Invalid value for --max-semi-space-size: ${cli.flags['maxSemiSpaceSize']}`, + `--max-semi-space-size must be a non-negative integer in megabytes (saw: "${cli.flags['maxSemiSpaceSize']}"); pass a whole number like --max-semi-space-size=128`, ) } diff --git a/packages/cli/src/utils/basics/vfs-extract.mts b/packages/cli/src/utils/basics/vfs-extract.mts index fc2866f78..9301a4baa 100644 --- a/packages/cli/src/utils/basics/vfs-extract.mts +++ b/packages/cli/src/utils/basics/vfs-extract.mts @@ -176,7 +176,7 @@ export async function extractBasicsTools( const missingTools = tools.filter(t => !extractedPaths[t]) if (missingTools.length) { throw new Error( - `Failed to extract all basics tools. Missing: ${missingTools.join(', ')}`, + `socket-basics VFS extraction returned ${Object.keys(extractedPaths).length}/${tools.length} tools (missing: ${missingTools.join(', ')}); the SEA bundle is incomplete — rebuild with all basics tools included`, ) } @@ -186,7 +186,9 @@ export async function extractBasicsTools( const pythonExe = isPlatWin ? 'python3.exe' : 'python3' const pythonDir = extractedPaths['python'] if (!pythonDir) { - throw new Error('Python extraction path not found') + throw new Error( + `extractedPaths.python is undefined after VFS extraction (expected a directory path); the basics SEA bundle is missing Python — rebuild the SEA binary`, + ) } const pythonPath = normalizePath(path.join(pythonDir, 'bin', pythonExe)) @@ -197,7 +199,7 @@ export async function extractBasicsTools( if (!validateResult || validateResult.code !== 0) { throw new Error( - `Python validation failed: ${validateResult?.stderr || 'Unable to execute Python'}`, + `extracted Python at ${pythonPath} failed to run with exit code ${validateResult?.code ?? 'null'} (stderr: ${validateResult?.stderr || ''}); the extracted binary may be corrupt or missing a shared lib — rebuild the SEA binary`, ) } @@ -218,7 +220,7 @@ export async function extractBasicsTools( if (!toolValidateResult || toolValidateResult.code !== 0) { throw new Error( - `${tool} validation failed: ${toolValidateResult?.stderr || `Unable to execute ${tool}`}`, + `extracted ${tool} at ${toolPath} failed to run with exit code ${toolValidateResult?.code ?? 'null'} (stderr: ${toolValidateResult?.stderr || ''}); the extracted binary may be corrupt or missing a shared lib — rebuild the SEA binary`, ) } diff --git a/packages/cli/src/utils/config.mts b/packages/cli/src/utils/config.mts index 244c71212..c4309e8b9 100644 --- a/packages/cli/src/utils/config.mts +++ b/packages/cli/src/utils/config.mts @@ -140,7 +140,9 @@ function getConfigValues(retryCount = 0): LocalConfig { const decoded = Buffer.from(rawString, 'base64').toString('utf8') // Check for invalid UTF-8 sequences (replacement character). if (decoded.includes('\ufffd')) { - throw new Error('Invalid UTF-8 in base64-encoded config') + throw new Error( + `SOCKET_CLI_CONFIG contains invalid UTF-8 after base64-decode (replacement-character in output); the env var may have been truncated or double-encoded — re-export it with \`echo '{...}' | base64\``, + ) } const parsed = JSON.parse(decoded) // Only copy supported config keys to prevent prototype pollution. diff --git a/packages/cli/src/utils/fs/path-resolve.mts b/packages/cli/src/utils/fs/path-resolve.mts index 6e5b47683..540cac487 100644 --- a/packages/cli/src/utils/fs/path-resolve.mts +++ b/packages/cli/src/utils/fs/path-resolve.mts @@ -55,7 +55,7 @@ export function findNpmDirPathSync(npmBinPath: string): string | undefined { while (true) { if (iterations >= MAX_ITERATIONS) { throw new Error( - `path traversal exceeded maximum iterations of ${MAX_ITERATIONS}`, + `npm path resolution walked ${MAX_ITERATIONS} directories without finding lib/node_modules/npm starting from "${npmBinPath}" (current: "${thePath}"); check for a circular symlink or corrupt node install`, ) } iterations += 1 diff --git a/packages/cli/src/utils/git/gitlab-provider.mts b/packages/cli/src/utils/git/gitlab-provider.mts index f43619ec0..061329567 100644 --- a/packages/cli/src/utils/git/gitlab-provider.mts +++ b/packages/cli/src/utils/git/gitlab-provider.mts @@ -97,7 +97,7 @@ export class GitLabProvider implements PrProvider { } throw new Error( - `Failed to create merge request after ${retries} attempts: ${owner}/${repo}#${head}`, + `GitLab API rejected createMergeRequest for ${owner}/${repo} (head="${head}") after ${retries} retries with exponential backoff; check GL_TOKEN permissions (needs api scope), the target branch exists, and GitLab is reachable`, ) } @@ -326,6 +326,6 @@ function getGitLabToken(): string { } throw new Error( - 'GitLab token not found. Set GITLAB_TOKEN environment variable.', + `GitLab access requires a token but process.env.GITLAB_TOKEN is not set; create a personal access token with the \`api\` scope at https://gitlab.com/-/user_settings/personal_access_tokens and export GITLAB_TOKEN=`, ) } diff --git a/packages/cli/src/utils/git/operations.mts b/packages/cli/src/utils/git/operations.mts index c1e8214b5..7334a39c5 100644 --- a/packages/cli/src/utils/git/operations.mts +++ b/packages/cli/src/utils/git/operations.mts @@ -55,7 +55,9 @@ async function getGitPath(): Promise { if (!_gitPath) { const result = await whichReal('git', { nothrow: true }) if (!result || Array.isArray(result)) { - throw new Error('git not found in PATH') + throw new Error( + `git executable not found on PATH (whichReal returned ${Array.isArray(result) ? 'multiple matches' : 'null'}); install git and ensure it is on PATH, or set SOCKET_CLI_GIT_PATH to point at a specific binary`, + ) } _gitPath = result } diff --git a/packages/cli/src/utils/npm/spec.mts b/packages/cli/src/utils/npm/spec.mts index a84edb0a6..66c329c9c 100644 --- a/packages/cli/src/utils/npm/spec.mts +++ b/packages/cli/src/utils/npm/spec.mts @@ -184,7 +184,9 @@ export function safeNpmSpecToPurl(pkgSpec: string): string | undefined { export function npmSpecToPurl(pkgSpec: string): string { const purl = safeNpmSpecToPurl(pkgSpec) if (!purl) { - throw new Error(`Failed to convert ${NPM} spec to PURL: ${pkgSpec}`) + throw new Error( + `cannot convert npm spec "${pkgSpec}" to PURL (safeNpmSpecToPurl returned null); valid npm specs look like "lodash@4.17.21" or "@scope/pkg@^1.0.0" — check the spec for typos or unsupported forms`, + ) } return purl } diff --git a/packages/cli/src/utils/promise/queue.mts b/packages/cli/src/utils/promise/queue.mts index c66dccc67..2d8e0c756 100644 --- a/packages/cli/src/utils/promise/queue.mts +++ b/packages/cli/src/utils/promise/queue.mts @@ -27,7 +27,9 @@ export class PromiseQueue { this.maxQueueLength = maxQueueLength } if (maxConcurrency < 1) { - throw new Error('maxConcurrency must be at least 1') + throw new Error( + `PromiseQueue maxConcurrency must be >= 1 (saw: ${maxConcurrency}); pass a positive integer like 4 or 8`, + ) } } diff --git a/packages/cli/src/utils/terminal/iocraft.mts b/packages/cli/src/utils/terminal/iocraft.mts index 510172a16..72de595d0 100644 --- a/packages/cli/src/utils/terminal/iocraft.mts +++ b/packages/cli/src/utils/terminal/iocraft.mts @@ -27,8 +27,7 @@ function getIocraft(): typeof iocraft { iocraftInstance = loaded.default || loaded } catch (e) { throw new Error( - `Failed to load iocraft native module: ${e}\n` + - `Make sure @socketaddon/iocraft is installed and your platform is supported.`, + `could not load @socketaddon/iocraft native module (${(e as Error).message}); reinstall socket-cli to pull the prebuilt for your platform, or check that your platform (${process.platform}-${process.arch}) has a published prebuilt`, ) } } diff --git a/packages/cli/test/unit/utils/git/gitlab-provider.test.mts b/packages/cli/test/unit/utils/git/gitlab-provider.test.mts index 7424f0865..dded9ece4 100644 --- a/packages/cli/test/unit/utils/git/gitlab-provider.test.mts +++ b/packages/cli/test/unit/utils/git/gitlab-provider.test.mts @@ -72,7 +72,7 @@ describe('git/gitlab-provider', () => { it('throws error when no token available', () => { delete process.env['GITLAB_TOKEN'] expect(() => new GitLabProvider()).toThrow( - 'GitLab token not found. Set GITLAB_TOKEN environment variable.', + /GitLab access requires a token but process\.env\.GITLAB_TOKEN is not set/, ) }) }) @@ -193,7 +193,9 @@ describe('git/gitlab-provider', () => { retries: 2, title: 'Test', }), - ).rejects.toThrow('Failed to create merge request after 2 attempts') + ).rejects.toThrow( + /GitLab API rejected createMergeRequest for owner\/repo .*after 2 retries/, + ) }) it('does not retry on 400 errors', async () => { @@ -212,7 +214,9 @@ describe('git/gitlab-provider', () => { retries: 3, title: 'Test', }), - ).rejects.toThrow('Failed to create merge request after 3 attempts') + ).rejects.toThrow( + /GitLab API rejected createMergeRequest for owner\/repo .*after 3 retries/, + ) expect(mockCreate).toHaveBeenCalledTimes(1) }) diff --git a/packages/cli/test/unit/utils/npm/spec.test.mts b/packages/cli/test/unit/utils/npm/spec.test.mts index efcd87528..5f9762a96 100644 --- a/packages/cli/test/unit/utils/npm/spec.test.mts +++ b/packages/cli/test/unit/utils/npm/spec.test.mts @@ -455,7 +455,7 @@ describe('npm-spec utilities', () => { // Make the fallback parsing fail by providing an empty string that would result in empty name. expect(() => npmSpecToPurl('')).toThrow( - 'Failed to convert npm spec to PURL:', + /cannot convert npm spec/, ) }) @@ -467,7 +467,7 @@ describe('npm-spec utilities', () => { // Make fallback parsing fail by providing empty string. expect(() => npmSpecToPurl('')).toThrow( - 'Failed to convert npm spec to PURL: ', + /cannot convert npm spec ""/, ) }) diff --git a/packages/cli/test/unit/utils/promise/queue.test.mts b/packages/cli/test/unit/utils/promise/queue.test.mts index 2cdeb50fd..3863c7ed8 100644 --- a/packages/cli/test/unit/utils/promise/queue.test.mts +++ b/packages/cli/test/unit/utils/promise/queue.test.mts @@ -150,10 +150,10 @@ describe('PromiseQueue', () => { it('should throw error for invalid concurrency', () => { expect(() => new PromiseQueue(0)).toThrow( - 'maxConcurrency must be at least 1', + /PromiseQueue maxConcurrency must be >= 1 \(saw: 0\)/, ) expect(() => new PromiseQueue(-1)).toThrow( - 'maxConcurrency must be at least 1', + /PromiseQueue maxConcurrency must be >= 1 \(saw: -1\)/, ) }) }) From bb6c44d626fb741160f6de4666ea5915443d546e Mon Sep 17 00:00:00 2001 From: jdalton Date: Wed, 22 Apr 2026 11:49:04 -0400 Subject: [PATCH 2/3] fix(cli): address Cursor bugbot findings on error messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four issues flagged by Cursor bugbot on #1260: 1. (Medium) gitlab-provider.mts: error said 'check GL_TOKEN permissions' but the actual env var is GITLAB_TOKEN (as the same file's getGitLabToken confirms). Fixed to GITLAB_TOKEN. 2. (Medium) git/operations.mts: error suggested 'set SOCKET_CLI_GIT_PATH to point at a specific binary' — that env var is not read anywhere. Removed the false suggestion; kept the real fix (install git and put it on PATH) with package-manager examples. 3. (Low) terminal/iocraft.mts: '(e as Error).message' evaluates to undefined when a non-Error is thrown. Switched to 'e instanceof Error ? e.message : String(e)' for safe stringification. 4. (Low) gitlab-provider.mts: error said 'after ${retries} retries' but the loop runs attempt 1..retries inclusive — retries is the total attempt count, not retries beyond the first. Reworded to 'attempts'. Matching test assertions updated. Caught by https://github.com/SocketDev/socket-cli/pull/1260 bugbot review. --- packages/cli/src/utils/git/gitlab-provider.mts | 2 +- packages/cli/src/utils/git/operations.mts | 2 +- packages/cli/src/utils/terminal/iocraft.mts | 2 +- packages/cli/test/unit/utils/git/gitlab-provider.test.mts | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/utils/git/gitlab-provider.mts b/packages/cli/src/utils/git/gitlab-provider.mts index 061329567..1110d78d2 100644 --- a/packages/cli/src/utils/git/gitlab-provider.mts +++ b/packages/cli/src/utils/git/gitlab-provider.mts @@ -97,7 +97,7 @@ export class GitLabProvider implements PrProvider { } throw new Error( - `GitLab API rejected createMergeRequest for ${owner}/${repo} (head="${head}") after ${retries} retries with exponential backoff; check GL_TOKEN permissions (needs api scope), the target branch exists, and GitLab is reachable`, + `GitLab API rejected createMergeRequest for ${owner}/${repo} (head="${head}") after ${retries} attempts with exponential backoff; check GITLAB_TOKEN permissions (needs api scope), that the target branch exists, and that GitLab is reachable`, ) } diff --git a/packages/cli/src/utils/git/operations.mts b/packages/cli/src/utils/git/operations.mts index 7334a39c5..4862940fb 100644 --- a/packages/cli/src/utils/git/operations.mts +++ b/packages/cli/src/utils/git/operations.mts @@ -56,7 +56,7 @@ async function getGitPath(): Promise { const result = await whichReal('git', { nothrow: true }) if (!result || Array.isArray(result)) { throw new Error( - `git executable not found on PATH (whichReal returned ${Array.isArray(result) ? 'multiple matches' : 'null'}); install git and ensure it is on PATH, or set SOCKET_CLI_GIT_PATH to point at a specific binary`, + `git executable not found on PATH (whichReal returned ${Array.isArray(result) ? 'multiple matches' : 'null'}); install git (e.g. \`brew install git\`, \`apt install git\`) and make sure it is reachable on PATH`, ) } _gitPath = result diff --git a/packages/cli/src/utils/terminal/iocraft.mts b/packages/cli/src/utils/terminal/iocraft.mts index 72de595d0..7311c61be 100644 --- a/packages/cli/src/utils/terminal/iocraft.mts +++ b/packages/cli/src/utils/terminal/iocraft.mts @@ -27,7 +27,7 @@ function getIocraft(): typeof iocraft { iocraftInstance = loaded.default || loaded } catch (e) { throw new Error( - `could not load @socketaddon/iocraft native module (${(e as Error).message}); reinstall socket-cli to pull the prebuilt for your platform, or check that your platform (${process.platform}-${process.arch}) has a published prebuilt`, + `could not load @socketaddon/iocraft native module (${e instanceof Error ? e.message : String(e)}); reinstall socket-cli to pull the prebuilt for your platform, or check that your platform (${process.platform}-${process.arch}) has a published prebuilt`, ) } } diff --git a/packages/cli/test/unit/utils/git/gitlab-provider.test.mts b/packages/cli/test/unit/utils/git/gitlab-provider.test.mts index dded9ece4..1e273d93f 100644 --- a/packages/cli/test/unit/utils/git/gitlab-provider.test.mts +++ b/packages/cli/test/unit/utils/git/gitlab-provider.test.mts @@ -194,7 +194,7 @@ describe('git/gitlab-provider', () => { title: 'Test', }), ).rejects.toThrow( - /GitLab API rejected createMergeRequest for owner\/repo .*after 2 retries/, + /GitLab API rejected createMergeRequest for owner\/repo .*after 2 attempts/, ) }) @@ -215,7 +215,7 @@ describe('git/gitlab-provider', () => { title: 'Test', }), ).rejects.toThrow( - /GitLab API rejected createMergeRequest for owner\/repo .*after 3 retries/, + /GitLab API rejected createMergeRequest for owner\/repo .*after 3 attempts/, ) expect(mockCreate).toHaveBeenCalledTimes(1) From 579638dca52fe2af338cddf5a2341f4bf5ff46e8 Mon Sep 17 00:00:00 2001 From: jdalton Date: Wed, 22 Apr 2026 12:14:26 -0400 Subject: [PATCH 3/3] chore(cli): use joinAnd + getErrorCause helpers in utils/ misc - basics/vfs-extract.mts: missingTools list now renders as prose via joinAnd('a, b, and c'). - terminal/iocraft.mts: inline `e instanceof Error ? e.message : String(e)` swapped for getErrorCause(e). require() of a native binding can throw non-Error values, so the safe-stringify with UNKNOWN_ERROR fallback is correct here. No behavior change for Error throws. --- packages/cli/src/utils/basics/vfs-extract.mts | 3 ++- packages/cli/src/utils/terminal/iocraft.mts | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/utils/basics/vfs-extract.mts b/packages/cli/src/utils/basics/vfs-extract.mts index 9301a4baa..61b93ccb7 100644 --- a/packages/cli/src/utils/basics/vfs-extract.mts +++ b/packages/cli/src/utils/basics/vfs-extract.mts @@ -16,6 +16,7 @@ import { createHash } from 'node:crypto' import { homedir } from 'node:os' import path from 'node:path' +import { joinAnd } from '@socketsecurity/lib/arrays' import { getDefaultLogger } from '@socketsecurity/lib/logger' import { normalizePath } from '@socketsecurity/lib/paths/normalize' import { spawn } from '@socketsecurity/lib/spawn' @@ -176,7 +177,7 @@ export async function extractBasicsTools( const missingTools = tools.filter(t => !extractedPaths[t]) if (missingTools.length) { throw new Error( - `socket-basics VFS extraction returned ${Object.keys(extractedPaths).length}/${tools.length} tools (missing: ${missingTools.join(', ')}); the SEA bundle is incomplete — rebuild with all basics tools included`, + `socket-basics VFS extraction returned ${Object.keys(extractedPaths).length}/${tools.length} tools (missing: ${joinAnd(missingTools)}); the SEA bundle is incomplete — rebuild with all basics tools included`, ) } diff --git a/packages/cli/src/utils/terminal/iocraft.mts b/packages/cli/src/utils/terminal/iocraft.mts index 7311c61be..b72c0cae2 100644 --- a/packages/cli/src/utils/terminal/iocraft.mts +++ b/packages/cli/src/utils/terminal/iocraft.mts @@ -7,6 +7,8 @@ import { createRequire } from 'node:module' +import { getErrorCause } from '../error/errors.mts' + import type iocraft from '@socketaddon/iocraft' // Re-export iocraft types for direct access when needed. @@ -27,7 +29,7 @@ function getIocraft(): typeof iocraft { iocraftInstance = loaded.default || loaded } catch (e) { throw new Error( - `could not load @socketaddon/iocraft native module (${e instanceof Error ? e.message : String(e)}); reinstall socket-cli to pull the prebuilt for your platform, or check that your platform (${process.platform}-${process.arch}) has a published prebuilt`, + `could not load @socketaddon/iocraft native module (${getErrorCause(e)}); reinstall socket-cli to pull the prebuilt for your platform, or check that your platform (${process.platform}-${process.arch}) has a published prebuilt`, ) } }