From d018b35a441ca0113dbad9f1cb970f99a8b87abe Mon Sep 17 00:00:00 2001 From: jdalton Date: Wed, 22 Apr 2026 10:31:23 -0400 Subject: [PATCH 1/4] fix(cli): align utils/dlx/ error messages with 4-ingredient strategy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrites error messages across packages/cli/src/utils/dlx/ to follow the What / Where / Saw vs. wanted / Fix strategy from CLAUDE.md. Sources: - spawn.mts: 27 messages - 6x 'Unexpected resolution type for ' — now name the resolver function and the actual resolution.type seen - Archive/platform errors name the supported formats/platforms - Python DLX errors surface the lock file path and cache dir - PyPI fetch errors include the URL that failed - Security errors (zip-slip, symlink escape) tell user to delete the cached asset and report upstream - resolve-binary.mts: 4 messages (socket-patch, trivy, trufflehog, opengrep platform support) — each now lists supported platforms and suggests how to install the tool manually - vfs-extract.mts: 5 messages (SEA VFS extraction failures) — each names what went wrong with the bundle and how to recover (usually: rebuild SEA) Internal invariant errors stay as plain Error (not InputError) but are informative enough that if they ever fire, the user can open a useful bug report. Tests updated: test/unit/utils/dlx/resolve-binary.test.mts (1 substring match switched to regex). Follows strategy from #1254. Part of the multi-PR series started by #1255 (commands/). --- packages/cli/src/utils/dlx/resolve-binary.mts | 12 +-- packages/cli/src/utils/dlx/spawn.mts | 84 +++++++++++-------- packages/cli/src/utils/dlx/vfs-extract.mts | 14 ++-- .../unit/utils/dlx/resolve-binary.test.mts | 2 +- 4 files changed, 64 insertions(+), 48 deletions(-) diff --git a/packages/cli/src/utils/dlx/resolve-binary.mts b/packages/cli/src/utils/dlx/resolve-binary.mts index 6d7e8aaa1..4d31e229b 100644 --- a/packages/cli/src/utils/dlx/resolve-binary.mts +++ b/packages/cli/src/utils/dlx/resolve-binary.mts @@ -167,8 +167,7 @@ export function resolveSocketPatch(): BinaryResolution { if (!assetName) { throw new Error( - `socket-patch is not available for platform ${platformKey}. ` + - `Supported platforms: ${Object.keys(SOCKET_PATCH_ASSETS).join(', ')}`, + `socket-patch has no prebuilt binary for "${platformKey}" (supported: ${Object.keys(SOCKET_PATCH_ASSETS).join(', ')}); upgrade socket-cli, build socket-patch from source, or set SOCKET_CLI_SOCKET_PATCH_LOCAL_PATH to point at a local build`, ) } @@ -246,8 +245,7 @@ export function resolveTrivy(): BinaryResolution { const platform = os.platform() const arch = os.arch() throw new Error( - `Trivy is not available for platform ${platform}-${arch}. ` + - 'Supported platforms: darwin-arm64, darwin-x64, linux-arm64, linux-x64, win32-x64', + `Trivy has no prebuilt binary for "${platform}-${arch}" (supported: darwin-arm64, darwin-x64, linux-arm64, linux-x64, win32-x64); run socket-cli on a supported platform or install Trivy manually and point \`trivy\` at it on PATH`, ) } @@ -310,8 +308,7 @@ export function resolveTrufflehog(): BinaryResolution { const platform = os.platform() const arch = os.arch() throw new Error( - `TruffleHog is not available for platform ${platform}-${arch}. ` + - 'Supported platforms: darwin-arm64, darwin-x64, linux-arm64, linux-x64, win32-arm64, win32-x64', + `TruffleHog has no prebuilt binary for "${platform}-${arch}" (supported: darwin-arm64, darwin-x64, linux-arm64, linux-x64, win32-arm64, win32-x64); run socket-cli on a supported platform or install TruffleHog manually and point \`trufflehog\` at it on PATH`, ) } @@ -363,8 +360,7 @@ export function resolveOpengrep(): BinaryResolution { if (!assetName) { throw new Error( - `OpenGrep is not available for platform ${platformKey}. ` + - `Supported platforms: ${Object.keys(OPENGREP_ASSETS).join(', ')}`, + `OpenGrep has no prebuilt binary for "${platformKey}" (supported: ${Object.keys(OPENGREP_ASSETS).join(', ')}); run socket-cli on a supported platform or install OpenGrep manually and point \`opengrep\` at it on PATH`, ) } diff --git a/packages/cli/src/utils/dlx/spawn.mts b/packages/cli/src/utils/dlx/spawn.mts index 2bcc8e6f3..3f14fef5f 100644 --- a/packages/cli/src/utils/dlx/spawn.mts +++ b/packages/cli/src/utils/dlx/spawn.mts @@ -116,14 +116,14 @@ function validatePackageName(name: string): void { if (!validNamePattern.test(name)) { throw new InputError( - `Invalid package name "${name}". Package names must contain only lowercase letters, numbers, hyphens, underscores, dots, and optionally a scope (@org/package).`, + `package name "${name}" must match /^(@scope\\/)?[a-z0-9-~][a-z0-9-._~]*$/ (lowercase letters, digits, -, _, ., ~, with optional @scope/); rename the package or check for typos`, ) } // Check for path traversal attempts. if (name.includes('..') || (name.includes('/') && !name.startsWith('@'))) { throw new InputError( - `Invalid package name "${name}". Package names cannot contain path traversal sequences.`, + `package name "${name}" contains path traversal characters (".." or a "/" outside of @scope/); pass a plain name like "lodash" or "@org/pkg"`, ) } } @@ -232,7 +232,7 @@ async function downloadGitHubReleaseBinary( } } throw new InputError( - 'Timeout waiting for another process to download GitHub release', + `timed out waiting for another socket process to finish downloading ${owner}/${repo}@${version} (${assetName}); if no other socket process is running, remove stale lock files under ${path.dirname(binaryPath)} and retry`, ) } throw e @@ -267,8 +267,7 @@ async function downloadGitHubReleaseBinary( const entryPath = path.resolve(path.join(cacheDir, entry.entryName)) if (!entryPath.startsWith(normalizedCacheDir)) { throw new InputError( - `Archive contains path traversal: ${entry.entryName}. ` + - `This may indicate a compromised release asset.`, + `archive entry "${entry.entryName}" resolves outside the cache dir (${normalizedCacheDir}) — this looks like a zip-slip attack; do NOT trust this release asset, report it to the upstream project, and delete ${result.binaryPath}`, ) } } @@ -286,8 +285,7 @@ async function downloadGitHubReleaseBinary( if (!resolvedTarget.startsWith(normalizedCacheDir)) { await fs.unlink(fullPath) throw new InputError( - `Archive contains unsafe symbolic link: ${file}. ` + - `This may indicate a compromised release asset.`, + `extracted symlink ${file} targets ${resolvedTarget} which is outside the cache dir (${normalizedCacheDir}); do NOT trust this release asset, report it to the upstream project, and delete ${cacheDir}`, ) } } @@ -298,19 +296,20 @@ async function downloadGitHubReleaseBinary( const tarPath = await whichReal('tar', { nothrow: true }) if (!tarPath || Array.isArray(tarPath)) { throw new InputError( - 'tar is required to extract GitHub release archives. Please install tar for your system.', + `tar is required to extract ${assetName} but was not found on PATH; install tar (e.g. \`apt install tar\`, \`brew install gnu-tar\`) and re-run`, ) } await spawn(tarPath, ['-xzf', result.binaryPath, '-C', cacheDir], {}) } else { - throw new InputError(`Unsupported archive format: ${assetName}`) + throw new InputError( + `archive format of ${assetName} is not supported (expected .zip or .tar.gz / .tgz); check the asset name in bundle-tools.json and the release's actual asset list`, + ) } // Verify binary was extracted. if (!existsSync(binaryPath)) { throw new InputError( - `Binary ${binaryFileName} not found after extracting ${assetName}. ` + - `Expected at: ${binaryPath}`, + `archive ${assetName} extracted but ${binaryFileName} was not found inside (expected at ${binaryPath}); the release's archive layout may have changed — verify asset contents and update bundle-tools.json`, ) } @@ -408,7 +407,9 @@ export async function spawnCoanaDlx( // Use dlx version (resolveCoana only returns 'local' or 'dlx' types). if (resolution.type !== 'dlx') { - throw new Error('Unexpected resolution type for coana') + throw new Error( + `internal: resolveCoana returned resolution.type="${resolution.type}" (expected "dlx"); this is a resolver contract bug — re-run with --debug and report the output`, + ) } const result = await spawnDlx( { @@ -484,7 +485,9 @@ export async function spawnCdxgenDlx( // Use dlx version (resolveCdxgen only returns 'local' or 'dlx' types). if (resolution.type !== 'dlx') { - throw new Error('Unexpected resolution type for cdxgen') + throw new Error( + `internal: resolveCdxgen returned resolution.type="${resolution.type}" (expected "dlx"); this is a resolver contract bug — re-run with --debug and report the output`, + ) } return await spawnDlx( resolution.details, @@ -554,7 +557,9 @@ export async function spawnSfwDlx( // Use dlx version (resolveSfw only returns 'local' or 'dlx' types). if (resolution.type !== 'dlx') { - throw new Error('Unexpected resolution type for sfw') + throw new Error( + `internal: resolveSfw returned resolution.type="${resolution.type}" (expected "dlx"); this is a resolver contract bug — re-run with --debug and report the output`, + ) } return await spawnDlx( resolution.details, @@ -675,21 +680,25 @@ async function spawnToolVfs( ): Promise { if (!areExternalToolsAvailable()) { throw new Error( - `Cannot spawn ${tool} from VFS - tools not available in SEA mode`, + `cannot spawn ${tool} from VFS: external tools were not bundled into this SEA binary; rebuild the SEA with INLINED_SOCKET_CLI_INCLUDE_EXTERNAL_TOOLS=1 or run the non-SEA CLI`, ) } // Extract tools from VFS (returns paths directly). const toolPaths = await extractExternalTools() if (!toolPaths) { - throw new Error(`Failed to extract ${tool} from VFS`) + throw new Error( + `failed to extract ${tool} from VFS (extractExternalTools returned null); the embedded tool archive may be corrupt — rebuild the SEA binary`, + ) } // Get tool path. const toolPath = toolPaths[tool] if (!toolPath) { - throw new Error(`Tool path not found for ${tool}`) + throw new Error( + `VFS extraction succeeded but ${tool} was not in the output map (got: ${Object.keys(toolPaths).join(', ') || 'empty'}); the SEA bundle is missing ${tool} — rebuild with it included`, + ) } const { env: spawnEnv, ...dlxOptions } = { @@ -938,7 +947,9 @@ function getPythonStandaloneInfo(): { assetName: string; url: string } { platformTriple = arch === 'arm64' ? 'aarch64-pc-windows-msvc' : 'x86_64-pc-windows-msvc' } else { - throw new InputError(`Unsupported platform: ${platform}`) + throw new InputError( + `python-build-standalone does not ship a prebuilt for os.platform()="${platform}" (supported: darwin, linux, win32); install Python manually and point socket at it via PATH`, + ) } // Asset name format matches checksums in bundle-tools.json. @@ -1000,7 +1011,7 @@ async function downloadPython(pythonDir: string): Promise { const tarPath = await whichReal('tar', { nothrow: true }) if (!tarPath || Array.isArray(tarPath)) { throw new InputError( - 'tar is required to extract Python. Please install tar for your system.', + `tar is required to extract the Python standalone archive but was not found on PATH; install tar (e.g. \`apt install tar\`, \`brew install gnu-tar\`) and re-run`, ) } await spawn(tarPath, ['-xzf', result.binaryPath, '-C', pythonDir], {}) @@ -1046,8 +1057,7 @@ export async function ensurePythonDlx(retryCount = 0): Promise { if (retryCount >= MAX_RETRIES) { throw new InputError( - `Failed to acquire Python installation lock after ${MAX_RETRIES} retries. ` + - 'Please check for filesystem issues or competing processes.', + `could not acquire the Python install lock after ${MAX_RETRIES} retries at ${lockFile}; another socket process may be stuck, or the lock file is stale — remove it manually and retry, or check that ${pythonDir} is writable`, ) } @@ -1107,7 +1117,7 @@ export async function ensurePythonDlx(retryCount = 0): Promise { } } throw new InputError( - 'Timeout waiting for Python download by another process', + `timed out after 60s waiting for another socket process to finish downloading Python to ${pythonDir}; if no other socket process is running, remove ${lockFile} and retry`, ) } throw e @@ -1118,7 +1128,7 @@ export async function ensurePythonDlx(retryCount = 0): Promise { if (!existsSync(pythonBin)) { throw new InputError( - `Python binary not found after extraction: ${pythonBin}`, + `Python archive extracted but ${pythonBin} does not exist; the standalone archive layout may have changed — check the asset contents under ${pythonDir} and update the bin-path logic in spawn.mts`, ) } @@ -1218,7 +1228,9 @@ async function downloadPyPiWheel( try { const response = await socketHttpRequest(pypiUrl) if (!response.ok) { - throw new Error(`PyPI API returned ${response.status}`) + throw new Error( + `PyPI returned HTTP ${response.status} for ${pypiUrl} (expected 200); check the package name and version, or retry if the registry is rate-limiting`, + ) } const data = response.json() as { urls?: Array<{ filename: string; url: string }> @@ -1235,14 +1247,13 @@ async function downloadPyPiWheel( // If we can't fetch from API, construct URL directly (may not work for all packages). // This is a fallback; the API approach is more reliable. throw new InputError( - `Failed to fetch PyPI package info for ${packageName}@${version}: ${getErrorCause(e)}`, + `could not fetch PyPI metadata for ${packageName}==${version} from ${pypiUrl} (${getErrorCause(e)}); check your network or proxy settings, or try again if PyPI is rate-limiting`, ) } if (!wheelUrl) { throw new InputError( - `No wheel found for ${packageName}@${version} on PyPI. ` + - 'This package may only be available as a source distribution.', + `${packageName}==${version} has no py3-none-any wheel on PyPI (only sdist available); pin to a version that ships a wheel or install from source manually`, ) } @@ -1275,8 +1286,7 @@ export async function ensureSocketPyCli( if (retryCount >= MAX_RETRIES) { throw new InputError( - `Failed to acquire Socket Python CLI installation lock after ${MAX_RETRIES} retries. ` + - 'Please check for filesystem issues or competing processes.', + `could not acquire the Socket Python CLI install lock after ${MAX_RETRIES} retries; another socket process may be stuck, or the lock file is stale — check for stale lock files under the Python cache dir and retry`, ) } @@ -1386,7 +1396,7 @@ export async function ensureSocketPyCli( }) } else { throw new InputError( - `Failed to download verified socketsecurity wheel for version ${pyCliVersion}`, + `could not download the verified socketsecurity==${pyCliVersion} wheel (downloadPyPiWheel returned null — likely a checksum mismatch or missing wheel asset); re-run with --debug for details, or bump the version in bundle-tools.json if the checksum needs refreshing`, ) } } else { @@ -1454,7 +1464,7 @@ export async function spawnSocketPyCliVfs( }) } else { throw new Error( - `Failed to download verified socketsecurity wheel for version ${pyCliVersion}`, + `failed to download socketsecurity==${pyCliVersion} wheel from PyPI (downloadPyPiWheel returned null — likely a checksum mismatch or missing py3-none-any wheel); re-run with --debug for details`, ) } } else { @@ -1673,7 +1683,9 @@ async function spawnTrivyDlx( const resolution = resolveTrivy() if (resolution.type !== 'github-release') { - throw new Error('Unexpected resolution type for trivy') + throw new Error( + `internal: resolveTrivy returned resolution.type="${resolution.type}" (expected "github-release"); this is a resolver contract bug — re-run with --debug and report the output`, + ) } const { env: spawnEnv, ...dlxOptions } = { @@ -1735,7 +1747,9 @@ async function spawnTrufflehogDlx( const resolution = resolveTrufflehog() if (resolution.type !== 'github-release') { - throw new Error('Unexpected resolution type for trufflehog') + throw new Error( + `internal: resolveTrufflehog returned resolution.type="${resolution.type}" (expected "github-release"); this is a resolver contract bug — re-run with --debug and report the output`, + ) } const { env: spawnEnv, ...dlxOptions } = { @@ -1797,7 +1811,9 @@ async function spawnOpengrepDlx( const resolution = resolveOpengrep() if (resolution.type !== 'github-release') { - throw new Error('Unexpected resolution type for opengrep') + throw new Error( + `internal: resolveOpengrep returned resolution.type="${resolution.type}" (expected "github-release"); this is a resolver contract bug — re-run with --debug and report the output`, + ) } const { env: spawnEnv, ...dlxOptions } = { diff --git a/packages/cli/src/utils/dlx/vfs-extract.mts b/packages/cli/src/utils/dlx/vfs-extract.mts index e6d235fc0..220160af3 100644 --- a/packages/cli/src/utils/dlx/vfs-extract.mts +++ b/packages/cli/src/utils/dlx/vfs-extract.mts @@ -273,7 +273,7 @@ async function extractTool(tool: ExternalTool): Promise { if (!processWithSmol.smol?.mount) { throw new Error( - 'process.smol.mount not available - not in node-smol SEA mode', + `process.smol.mount is undefined — extractTool("${tool}") requires a node-smol SEA build; this code path should only run inside the SEA. Check isSeaBinary() / areExternalToolsAvailable() upstream`, ) } @@ -340,12 +340,16 @@ async function extractTool(tool: ExternalTool): Promise { } if (!existsSync(extractedPath)) { - throw new Error(`Extracted tool not found at ${extractedPath}`) + throw new Error( + `process.smol.mount returned but ${extractedPath} does not exist; the VFS layout for ${tool} may have changed — check the SEA build config and the tool's expected path`, + ) } return extractedPath } catch (e) { - throw new Error(`Failed to extract ${tool} from VFS: ${e}`) + throw new Error( + `failed to extract ${tool} from the SEA VFS (${(e as Error).message}); the embedded tool archive may be corrupt — rebuild the SEA binary`, + ) } } @@ -550,7 +554,7 @@ export async function extractExternalTools( } } throw new Error( - 'Timeout waiting for another process to extract external tools', + `timed out waiting for another socket process to finish extracting external tools from the SEA VFS; if no other socket process is running, remove any stale lock files under the node-smol base dir and retry`, ) } throw e @@ -641,7 +645,7 @@ export async function extractExternalTools( if (Object.keys(toolPaths).length !== EXTERNAL_TOOLS.length) { const missingTools = EXTERNAL_TOOLS.filter(t => !toolPaths[t]) throw new Error( - `Failed to extract all external tools. Missing: ${missingTools.join(', ')}`, + `SEA VFS extraction returned ${Object.keys(toolPaths).length}/${EXTERNAL_TOOLS.length} tools (missing: ${missingTools.join(', ')}); the SEA bundle is incomplete — rebuild with all external tools included`, ) } diff --git a/packages/cli/test/unit/utils/dlx/resolve-binary.test.mts b/packages/cli/test/unit/utils/dlx/resolve-binary.test.mts index dcd1948fc..02090c5fc 100644 --- a/packages/cli/test/unit/utils/dlx/resolve-binary.test.mts +++ b/packages/cli/test/unit/utils/dlx/resolve-binary.test.mts @@ -353,7 +353,7 @@ describe('binary resolution utilities', () => { ) expect(() => resolveSocketPatch()).toThrow( - 'socket-patch is not available for platform freebsd-x64', + /socket-patch has no prebuilt binary for "freebsd-x64"/, ) }) From a766d3d52dca006e471902781bd38276855dd42b Mon Sep 17 00:00:00 2001 From: jdalton Date: Wed, 22 Apr 2026 11:15:09 -0400 Subject: [PATCH 2/4] fix(cli): hoist lockFile/pythonDir above retry check in ensurePythonDlx The previous commit referenced `${lockFile}` and `${pythonDir}` in the MAX_RETRIES error message, but those consts were declared AFTER the retry check. Hitting the retry path threw ReferenceError from the temporal dead zone instead of the intended InputError. Fix: move the three const declarations (pythonDir, pythonBin, lockFile) above the MAX_RETRIES guard. Caught by Cursor bugbot (https://github.com/SocketDev/socket-cli/pull/1256#discussion) and confirmed by the type-check job. --- packages/cli/src/utils/dlx/spawn.mts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/utils/dlx/spawn.mts b/packages/cli/src/utils/dlx/spawn.mts index 3f14fef5f..28ee6464a 100644 --- a/packages/cli/src/utils/dlx/spawn.mts +++ b/packages/cli/src/utils/dlx/spawn.mts @@ -1055,16 +1055,16 @@ export async function ensurePython(): Promise { export async function ensurePythonDlx(retryCount = 0): Promise { const MAX_RETRIES = 3 + const pythonDir = getPythonCachePath() + const pythonBin = getPythonBinPath(pythonDir) + const lockFile = path.join(pythonDir, '.downloading') + if (retryCount >= MAX_RETRIES) { throw new InputError( `could not acquire the Python install lock after ${MAX_RETRIES} retries at ${lockFile}; another socket process may be stuck, or the lock file is stale — remove it manually and retry, or check that ${pythonDir} is writable`, ) } - const pythonDir = getPythonCachePath() - const pythonBin = getPythonBinPath(pythonDir) - const lockFile = path.join(pythonDir, '.downloading') - if (!existsSync(pythonBin)) { await safeMkdir(pythonDir, { recursive: true }) From 73a4eed5f69f8337fd506a6eea6daa2c535c5ba9 Mon Sep 17 00:00:00 2001 From: jdalton Date: Wed, 22 Apr 2026 11:51:19 -0400 Subject: [PATCH 3/4] chore(cli): harden (e as Error) casts to safe stringify Switch `(e as Error).message` to `e instanceof Error ? e.message : String(e)` so that when a non-Error value is thrown (strings, objects, null) the error message stays informative instead of becoming 'undefined'. Same fix as applied to #1260 (iocraft.mts) after Cursor bugbot flagged the pattern on that PR. --- packages/cli/src/utils/dlx/vfs-extract.mts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/utils/dlx/vfs-extract.mts b/packages/cli/src/utils/dlx/vfs-extract.mts index 220160af3..9979e35a4 100644 --- a/packages/cli/src/utils/dlx/vfs-extract.mts +++ b/packages/cli/src/utils/dlx/vfs-extract.mts @@ -348,7 +348,7 @@ async function extractTool(tool: ExternalTool): Promise { return extractedPath } catch (e) { throw new Error( - `failed to extract ${tool} from the SEA VFS (${(e as Error).message}); the embedded tool archive may be corrupt — rebuild the SEA binary`, + `failed to extract ${tool} from the SEA VFS (${e instanceof Error ? e.message : String(e)}); the embedded tool archive may be corrupt — rebuild the SEA binary`, ) } } From 7c7268642c7623393a17015b32cb694e4cf206fe Mon Sep 17 00:00:00 2001 From: jdalton Date: Wed, 22 Apr 2026 12:11:14 -0400 Subject: [PATCH 4/4] chore(cli): use joinAnd + getErrorCause helpers in dlx error messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switch to shared fleet helpers so error lists render as human prose ('a, b, and c') and error-cause stringification works safely for non-Error throws (falls back to 'Unknown error' instead of crashing or producing 'undefined'). - resolve-binary.mts: SOCKET_PATCH_ASSETS + OPENGREP_ASSETS platform lists now use `joinAnd(Object.keys(...))`. - vfs-extract.mts: missingTools list uses joinAnd; extract-failure error now uses getErrorCause(e) — equivalent to the inline 'e instanceof Error ? e.message : String(e)' with the standard UNKNOWN_ERROR fallback. - spawn.mts: output-map listing uses joinAnd. No behavior change for Error throws; non-Error throws now produce 'Unknown error' instead of '[object Object]' or similar. --- packages/cli/src/utils/dlx/resolve-binary.mts | 6 ++++-- packages/cli/src/utils/dlx/spawn.mts | 3 ++- packages/cli/src/utils/dlx/vfs-extract.mts | 6 ++++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/utils/dlx/resolve-binary.mts b/packages/cli/src/utils/dlx/resolve-binary.mts index 4d31e229b..8c724b7f3 100644 --- a/packages/cli/src/utils/dlx/resolve-binary.mts +++ b/packages/cli/src/utils/dlx/resolve-binary.mts @@ -5,6 +5,8 @@ import os from 'node:os' +import { joinAnd } from '@socketsecurity/lib/arrays' + import { getCdxgenVersion } from '../../env/cdxgen-version.mts' import { getCoanaVersion } from '../../env/coana-version.mts' import { requireOpengrepChecksum } from '../../env/opengrep-checksums.mts' @@ -167,7 +169,7 @@ export function resolveSocketPatch(): BinaryResolution { if (!assetName) { throw new Error( - `socket-patch has no prebuilt binary for "${platformKey}" (supported: ${Object.keys(SOCKET_PATCH_ASSETS).join(', ')}); upgrade socket-cli, build socket-patch from source, or set SOCKET_CLI_SOCKET_PATCH_LOCAL_PATH to point at a local build`, + `socket-patch has no prebuilt binary for "${platformKey}" (supported: ${joinAnd(Object.keys(SOCKET_PATCH_ASSETS))}); upgrade socket-cli, build socket-patch from source, or set SOCKET_CLI_SOCKET_PATCH_LOCAL_PATH to point at a local build`, ) } @@ -360,7 +362,7 @@ export function resolveOpengrep(): BinaryResolution { if (!assetName) { throw new Error( - `OpenGrep has no prebuilt binary for "${platformKey}" (supported: ${Object.keys(OPENGREP_ASSETS).join(', ')}); run socket-cli on a supported platform or install OpenGrep manually and point \`opengrep\` at it on PATH`, + `OpenGrep has no prebuilt binary for "${platformKey}" (supported: ${joinAnd(Object.keys(OPENGREP_ASSETS))}); run socket-cli on a supported platform or install OpenGrep manually and point \`opengrep\` at it on PATH`, ) } diff --git a/packages/cli/src/utils/dlx/spawn.mts b/packages/cli/src/utils/dlx/spawn.mts index 28ee6464a..39b316bbd 100644 --- a/packages/cli/src/utils/dlx/spawn.mts +++ b/packages/cli/src/utils/dlx/spawn.mts @@ -22,6 +22,7 @@ import os from 'node:os' import path from 'node:path' import AdmZip from 'adm-zip' +import { joinAnd } from '@socketsecurity/lib/arrays' import { WIN32 } from '@socketsecurity/lib/constants/platform' import { downloadBinary, getDlxCachePath } from '@socketsecurity/lib/dlx/binary' import { detectExecutableType } from '@socketsecurity/lib/dlx/detect' @@ -697,7 +698,7 @@ async function spawnToolVfs( if (!toolPath) { throw new Error( - `VFS extraction succeeded but ${tool} was not in the output map (got: ${Object.keys(toolPaths).join(', ') || 'empty'}); the SEA bundle is missing ${tool} — rebuild with it included`, + `VFS extraction succeeded but ${tool} was not in the output map (got: ${joinAnd(Object.keys(toolPaths)) || 'empty'}); the SEA bundle is missing ${tool} — rebuild with it included`, ) } diff --git a/packages/cli/src/utils/dlx/vfs-extract.mts b/packages/cli/src/utils/dlx/vfs-extract.mts index 9979e35a4..db2e9fd6b 100644 --- a/packages/cli/src/utils/dlx/vfs-extract.mts +++ b/packages/cli/src/utils/dlx/vfs-extract.mts @@ -66,12 +66,14 @@ import { existsSync, promises as fs } from 'node:fs' import { homedir } from 'node:os' import path from 'node:path' +import { joinAnd } from '@socketsecurity/lib/arrays' import { debug } from '@socketsecurity/lib/debug' import { safeMkdir } from '@socketsecurity/lib/fs' import { getDefaultLogger } from '@socketsecurity/lib/logger' import { normalizePath } from '@socketsecurity/lib/paths/normalize' import { UPDATE_STORE_DIR } from '../../constants/paths.mts' +import { getErrorCause } from '../error/errors.mts' import { isSeaBinary } from '../sea/detect.mts' const logger = getDefaultLogger() @@ -348,7 +350,7 @@ async function extractTool(tool: ExternalTool): Promise { return extractedPath } catch (e) { throw new Error( - `failed to extract ${tool} from the SEA VFS (${e instanceof Error ? e.message : String(e)}); the embedded tool archive may be corrupt — rebuild the SEA binary`, + `failed to extract ${tool} from the SEA VFS (${getErrorCause(e)}); the embedded tool archive may be corrupt — rebuild the SEA binary`, ) } } @@ -645,7 +647,7 @@ export async function extractExternalTools( if (Object.keys(toolPaths).length !== EXTERNAL_TOOLS.length) { const missingTools = EXTERNAL_TOOLS.filter(t => !toolPaths[t]) throw new Error( - `SEA VFS extraction returned ${Object.keys(toolPaths).length}/${EXTERNAL_TOOLS.length} tools (missing: ${missingTools.join(', ')}); the SEA bundle is incomplete — rebuild with all external tools included`, + `SEA VFS extraction returned ${Object.keys(toolPaths).length}/${EXTERNAL_TOOLS.length} tools (missing: ${joinAnd(missingTools)}); the SEA bundle is incomplete — rebuild with all external tools included`, ) }