Skip to content

Commit d018b35

Browse files
committed
fix(cli): align utils/dlx/ error messages with 4-ingredient strategy
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 <tool>' — 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/).
1 parent fc5591f commit d018b35

4 files changed

Lines changed: 64 additions & 48 deletions

File tree

packages/cli/src/utils/dlx/resolve-binary.mts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -167,8 +167,7 @@ export function resolveSocketPatch(): BinaryResolution {
167167

168168
if (!assetName) {
169169
throw new Error(
170-
`socket-patch is not available for platform ${platformKey}. ` +
171-
`Supported platforms: ${Object.keys(SOCKET_PATCH_ASSETS).join(', ')}`,
170+
`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`,
172171
)
173172
}
174173

@@ -246,8 +245,7 @@ export function resolveTrivy(): BinaryResolution {
246245
const platform = os.platform()
247246
const arch = os.arch()
248247
throw new Error(
249-
`Trivy is not available for platform ${platform}-${arch}. ` +
250-
'Supported platforms: darwin-arm64, darwin-x64, linux-arm64, linux-x64, win32-x64',
248+
`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`,
251249
)
252250
}
253251

@@ -310,8 +308,7 @@ export function resolveTrufflehog(): BinaryResolution {
310308
const platform = os.platform()
311309
const arch = os.arch()
312310
throw new Error(
313-
`TruffleHog is not available for platform ${platform}-${arch}. ` +
314-
'Supported platforms: darwin-arm64, darwin-x64, linux-arm64, linux-x64, win32-arm64, win32-x64',
311+
`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`,
315312
)
316313
}
317314

@@ -363,8 +360,7 @@ export function resolveOpengrep(): BinaryResolution {
363360

364361
if (!assetName) {
365362
throw new Error(
366-
`OpenGrep is not available for platform ${platformKey}. ` +
367-
`Supported platforms: ${Object.keys(OPENGREP_ASSETS).join(', ')}`,
363+
`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`,
368364
)
369365
}
370366

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

Lines changed: 50 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -116,14 +116,14 @@ function validatePackageName(name: string): void {
116116

117117
if (!validNamePattern.test(name)) {
118118
throw new InputError(
119-
`Invalid package name "${name}". Package names must contain only lowercase letters, numbers, hyphens, underscores, dots, and optionally a scope (@org/package).`,
119+
`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`,
120120
)
121121
}
122122

123123
// Check for path traversal attempts.
124124
if (name.includes('..') || (name.includes('/') && !name.startsWith('@'))) {
125125
throw new InputError(
126-
`Invalid package name "${name}". Package names cannot contain path traversal sequences.`,
126+
`package name "${name}" contains path traversal characters (".." or a "/" outside of @scope/); pass a plain name like "lodash" or "@org/pkg"`,
127127
)
128128
}
129129
}
@@ -232,7 +232,7 @@ async function downloadGitHubReleaseBinary(
232232
}
233233
}
234234
throw new InputError(
235-
'Timeout waiting for another process to download GitHub release',
235+
`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`,
236236
)
237237
}
238238
throw e
@@ -267,8 +267,7 @@ async function downloadGitHubReleaseBinary(
267267
const entryPath = path.resolve(path.join(cacheDir, entry.entryName))
268268
if (!entryPath.startsWith(normalizedCacheDir)) {
269269
throw new InputError(
270-
`Archive contains path traversal: ${entry.entryName}. ` +
271-
`This may indicate a compromised release asset.`,
270+
`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}`,
272271
)
273272
}
274273
}
@@ -286,8 +285,7 @@ async function downloadGitHubReleaseBinary(
286285
if (!resolvedTarget.startsWith(normalizedCacheDir)) {
287286
await fs.unlink(fullPath)
288287
throw new InputError(
289-
`Archive contains unsafe symbolic link: ${file}. ` +
290-
`This may indicate a compromised release asset.`,
288+
`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}`,
291289
)
292290
}
293291
}
@@ -298,19 +296,20 @@ async function downloadGitHubReleaseBinary(
298296
const tarPath = await whichReal('tar', { nothrow: true })
299297
if (!tarPath || Array.isArray(tarPath)) {
300298
throw new InputError(
301-
'tar is required to extract GitHub release archives. Please install tar for your system.',
299+
`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`,
302300
)
303301
}
304302
await spawn(tarPath, ['-xzf', result.binaryPath, '-C', cacheDir], {})
305303
} else {
306-
throw new InputError(`Unsupported archive format: ${assetName}`)
304+
throw new InputError(
305+
`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`,
306+
)
307307
}
308308

309309
// Verify binary was extracted.
310310
if (!existsSync(binaryPath)) {
311311
throw new InputError(
312-
`Binary ${binaryFileName} not found after extracting ${assetName}. ` +
313-
`Expected at: ${binaryPath}`,
312+
`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`,
314313
)
315314
}
316315

@@ -408,7 +407,9 @@ export async function spawnCoanaDlx(
408407

409408
// Use dlx version (resolveCoana only returns 'local' or 'dlx' types).
410409
if (resolution.type !== 'dlx') {
411-
throw new Error('Unexpected resolution type for coana')
410+
throw new Error(
411+
`internal: resolveCoana returned resolution.type="${resolution.type}" (expected "dlx"); this is a resolver contract bug — re-run with --debug and report the output`,
412+
)
412413
}
413414
const result = await spawnDlx(
414415
{
@@ -484,7 +485,9 @@ export async function spawnCdxgenDlx(
484485

485486
// Use dlx version (resolveCdxgen only returns 'local' or 'dlx' types).
486487
if (resolution.type !== 'dlx') {
487-
throw new Error('Unexpected resolution type for cdxgen')
488+
throw new Error(
489+
`internal: resolveCdxgen returned resolution.type="${resolution.type}" (expected "dlx"); this is a resolver contract bug — re-run with --debug and report the output`,
490+
)
488491
}
489492
return await spawnDlx(
490493
resolution.details,
@@ -554,7 +557,9 @@ export async function spawnSfwDlx(
554557

555558
// Use dlx version (resolveSfw only returns 'local' or 'dlx' types).
556559
if (resolution.type !== 'dlx') {
557-
throw new Error('Unexpected resolution type for sfw')
560+
throw new Error(
561+
`internal: resolveSfw returned resolution.type="${resolution.type}" (expected "dlx"); this is a resolver contract bug — re-run with --debug and report the output`,
562+
)
558563
}
559564
return await spawnDlx(
560565
resolution.details,
@@ -675,21 +680,25 @@ async function spawnToolVfs(
675680
): Promise<DlxSpawnResult> {
676681
if (!areExternalToolsAvailable()) {
677682
throw new Error(
678-
`Cannot spawn ${tool} from VFS - tools not available in SEA mode`,
683+
`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`,
679684
)
680685
}
681686

682687
// Extract tools from VFS (returns paths directly).
683688
const toolPaths = await extractExternalTools()
684689
if (!toolPaths) {
685-
throw new Error(`Failed to extract ${tool} from VFS`)
690+
throw new Error(
691+
`failed to extract ${tool} from VFS (extractExternalTools returned null); the embedded tool archive may be corrupt — rebuild the SEA binary`,
692+
)
686693
}
687694

688695
// Get tool path.
689696
const toolPath = toolPaths[tool]
690697

691698
if (!toolPath) {
692-
throw new Error(`Tool path not found for ${tool}`)
699+
throw new Error(
700+
`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`,
701+
)
693702
}
694703

695704
const { env: spawnEnv, ...dlxOptions } = {
@@ -938,7 +947,9 @@ function getPythonStandaloneInfo(): { assetName: string; url: string } {
938947
platformTriple =
939948
arch === 'arm64' ? 'aarch64-pc-windows-msvc' : 'x86_64-pc-windows-msvc'
940949
} else {
941-
throw new InputError(`Unsupported platform: ${platform}`)
950+
throw new InputError(
951+
`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`,
952+
)
942953
}
943954

944955
// Asset name format matches checksums in bundle-tools.json.
@@ -1000,7 +1011,7 @@ async function downloadPython(pythonDir: string): Promise<void> {
10001011
const tarPath = await whichReal('tar', { nothrow: true })
10011012
if (!tarPath || Array.isArray(tarPath)) {
10021013
throw new InputError(
1003-
'tar is required to extract Python. Please install tar for your system.',
1014+
`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`,
10041015
)
10051016
}
10061017
await spawn(tarPath, ['-xzf', result.binaryPath, '-C', pythonDir], {})
@@ -1046,8 +1057,7 @@ export async function ensurePythonDlx(retryCount = 0): Promise<string> {
10461057

10471058
if (retryCount >= MAX_RETRIES) {
10481059
throw new InputError(
1049-
`Failed to acquire Python installation lock after ${MAX_RETRIES} retries. ` +
1050-
'Please check for filesystem issues or competing processes.',
1060+
`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`,
10511061
)
10521062
}
10531063

@@ -1107,7 +1117,7 @@ export async function ensurePythonDlx(retryCount = 0): Promise<string> {
11071117
}
11081118
}
11091119
throw new InputError(
1110-
'Timeout waiting for Python download by another process',
1120+
`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`,
11111121
)
11121122
}
11131123
throw e
@@ -1118,7 +1128,7 @@ export async function ensurePythonDlx(retryCount = 0): Promise<string> {
11181128

11191129
if (!existsSync(pythonBin)) {
11201130
throw new InputError(
1121-
`Python binary not found after extraction: ${pythonBin}`,
1131+
`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`,
11221132
)
11231133
}
11241134

@@ -1218,7 +1228,9 @@ async function downloadPyPiWheel(
12181228
try {
12191229
const response = await socketHttpRequest(pypiUrl)
12201230
if (!response.ok) {
1221-
throw new Error(`PyPI API returned ${response.status}`)
1231+
throw new Error(
1232+
`PyPI returned HTTP ${response.status} for ${pypiUrl} (expected 200); check the package name and version, or retry if the registry is rate-limiting`,
1233+
)
12221234
}
12231235
const data = response.json() as {
12241236
urls?: Array<{ filename: string; url: string }>
@@ -1235,14 +1247,13 @@ async function downloadPyPiWheel(
12351247
// If we can't fetch from API, construct URL directly (may not work for all packages).
12361248
// This is a fallback; the API approach is more reliable.
12371249
throw new InputError(
1238-
`Failed to fetch PyPI package info for ${packageName}@${version}: ${getErrorCause(e)}`,
1250+
`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`,
12391251
)
12401252
}
12411253

12421254
if (!wheelUrl) {
12431255
throw new InputError(
1244-
`No wheel found for ${packageName}@${version} on PyPI. ` +
1245-
'This package may only be available as a source distribution.',
1256+
`${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`,
12461257
)
12471258
}
12481259

@@ -1275,8 +1286,7 @@ export async function ensureSocketPyCli(
12751286

12761287
if (retryCount >= MAX_RETRIES) {
12771288
throw new InputError(
1278-
`Failed to acquire Socket Python CLI installation lock after ${MAX_RETRIES} retries. ` +
1279-
'Please check for filesystem issues or competing processes.',
1289+
`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`,
12801290
)
12811291
}
12821292

@@ -1386,7 +1396,7 @@ export async function ensureSocketPyCli(
13861396
})
13871397
} else {
13881398
throw new InputError(
1389-
`Failed to download verified socketsecurity wheel for version ${pyCliVersion}`,
1399+
`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`,
13901400
)
13911401
}
13921402
} else {
@@ -1454,7 +1464,7 @@ export async function spawnSocketPyCliVfs(
14541464
})
14551465
} else {
14561466
throw new Error(
1457-
`Failed to download verified socketsecurity wheel for version ${pyCliVersion}`,
1467+
`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`,
14581468
)
14591469
}
14601470
} else {
@@ -1673,7 +1683,9 @@ async function spawnTrivyDlx(
16731683
const resolution = resolveTrivy()
16741684

16751685
if (resolution.type !== 'github-release') {
1676-
throw new Error('Unexpected resolution type for trivy')
1686+
throw new Error(
1687+
`internal: resolveTrivy returned resolution.type="${resolution.type}" (expected "github-release"); this is a resolver contract bug — re-run with --debug and report the output`,
1688+
)
16771689
}
16781690

16791691
const { env: spawnEnv, ...dlxOptions } = {
@@ -1735,7 +1747,9 @@ async function spawnTrufflehogDlx(
17351747
const resolution = resolveTrufflehog()
17361748

17371749
if (resolution.type !== 'github-release') {
1738-
throw new Error('Unexpected resolution type for trufflehog')
1750+
throw new Error(
1751+
`internal: resolveTrufflehog returned resolution.type="${resolution.type}" (expected "github-release"); this is a resolver contract bug — re-run with --debug and report the output`,
1752+
)
17391753
}
17401754

17411755
const { env: spawnEnv, ...dlxOptions } = {
@@ -1797,7 +1811,9 @@ async function spawnOpengrepDlx(
17971811
const resolution = resolveOpengrep()
17981812

17991813
if (resolution.type !== 'github-release') {
1800-
throw new Error('Unexpected resolution type for opengrep')
1814+
throw new Error(
1815+
`internal: resolveOpengrep returned resolution.type="${resolution.type}" (expected "github-release"); this is a resolver contract bug — re-run with --debug and report the output`,
1816+
)
18011817
}
18021818

18031819
const { env: spawnEnv, ...dlxOptions } = {

packages/cli/src/utils/dlx/vfs-extract.mts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ async function extractTool(tool: ExternalTool): Promise<string> {
273273

274274
if (!processWithSmol.smol?.mount) {
275275
throw new Error(
276-
'process.smol.mount not available - not in node-smol SEA mode',
276+
`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`,
277277
)
278278
}
279279

@@ -340,12 +340,16 @@ async function extractTool(tool: ExternalTool): Promise<string> {
340340
}
341341

342342
if (!existsSync(extractedPath)) {
343-
throw new Error(`Extracted tool not found at ${extractedPath}`)
343+
throw new Error(
344+
`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`,
345+
)
344346
}
345347

346348
return extractedPath
347349
} catch (e) {
348-
throw new Error(`Failed to extract ${tool} from VFS: ${e}`)
350+
throw new Error(
351+
`failed to extract ${tool} from the SEA VFS (${(e as Error).message}); the embedded tool archive may be corrupt — rebuild the SEA binary`,
352+
)
349353
}
350354
}
351355

@@ -550,7 +554,7 @@ export async function extractExternalTools(
550554
}
551555
}
552556
throw new Error(
553-
'Timeout waiting for another process to extract external tools',
557+
`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`,
554558
)
555559
}
556560
throw e
@@ -641,7 +645,7 @@ export async function extractExternalTools(
641645
if (Object.keys(toolPaths).length !== EXTERNAL_TOOLS.length) {
642646
const missingTools = EXTERNAL_TOOLS.filter(t => !toolPaths[t])
643647
throw new Error(
644-
`Failed to extract all external tools. Missing: ${missingTools.join(', ')}`,
648+
`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`,
645649
)
646650
}
647651

packages/cli/test/unit/utils/dlx/resolve-binary.test.mts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ describe('binary resolution utilities', () => {
353353
)
354354

355355
expect(() => resolveSocketPatch()).toThrow(
356-
'socket-patch is not available for platform freebsd-x64',
356+
/socket-patch has no prebuilt binary for "freebsd-x64"/,
357357
)
358358
})
359359

0 commit comments

Comments
 (0)