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..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( - `Failed to extract all basics tools. Missing: ${missingTools.join(', ')}`, + `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`, ) } @@ -186,7 +187,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 +200,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 +221,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..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( - `Failed to create merge request after ${retries} attempts: ${owner}/${repo}#${head}`, + `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`, ) } @@ -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..4862940fb 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 (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/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..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,8 +29,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 (${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`, ) } } 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..1e273d93f 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 attempts/, + ) }) 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 attempts/, + ) 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\)/, ) }) })