You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
* 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/).
* 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
(#1256 (comment))
and confirmed by the type-check job.
* 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.
* chore(cli): use joinAnd + getErrorCause helpers in dlx error messages
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.
`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`,
172
173
)
173
174
}
174
175
@@ -246,8 +247,7 @@ export function resolveTrivy(): BinaryResolution {
246
247
constplatform=os.platform()
247
248
constarch=os.arch()
248
249
thrownewError(
249
-
`Trivy is not available for platform ${platform}-${arch}. `+
`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`,
251
251
)
252
252
}
253
253
@@ -310,8 +310,7 @@ export function resolveTrufflehog(): BinaryResolution {
310
310
constplatform=os.platform()
311
311
constarch=os.arch()
312
312
thrownewError(
313
-
`TruffleHog is not available for platform ${platform}-${arch}. `+
`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`,
315
314
)
316
315
}
317
316
@@ -363,8 +362,7 @@ export function resolveOpengrep(): BinaryResolution {
363
362
364
363
if(!assetName){
365
364
thrownewError(
366
-
`OpenGrep is not available for platform ${platformKey}. `+
`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`,
@@ -116,14 +117,14 @@ function validatePackageName(name: string): void {
116
117
117
118
if(!validNamePattern.test(name)){
118
119
thrownewInputError(
119
-
`Invalid package name "${name}". Package names must contain only lowercase letters, numbers, hyphens, underscores, dots, and optionally a scope (@org/package).`,
120
+
`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`,
`package name "${name}" contains path traversal characters (".." or a "/" outside of @scope/); pass a plain name like "lodash" or "@org/pkg"`,
127
128
)
128
129
}
129
130
}
@@ -232,7 +233,7 @@ async function downloadGitHubReleaseBinary(
232
233
}
233
234
}
234
235
thrownewInputError(
235
-
'Timeout waiting for another process to download GitHub release',
236
+
`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`,
236
237
)
237
238
}
238
239
throwe
@@ -267,8 +268,7 @@ async function downloadGitHubReleaseBinary(
`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}`,
272
272
)
273
273
}
274
274
}
@@ -286,8 +286,7 @@ async function downloadGitHubReleaseBinary(
`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}`,
291
290
)
292
291
}
293
292
}
@@ -298,19 +297,20 @@ async function downloadGitHubReleaseBinary(
`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`,
307
+
)
307
308
}
308
309
309
310
// Verify binary was extracted.
310
311
if(!existsSync(binaryPath)){
311
312
thrownewInputError(
312
-
`Binary ${binaryFileName} not found after extracting ${assetName}. `+
313
-
`Expected at: ${binaryPath}`,
313
+
`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`,
314
314
)
315
315
}
316
316
@@ -408,7 +408,9 @@ export async function spawnCoanaDlx(
408
408
409
409
// Use dlx version (resolveCoana only returns 'local' or 'dlx' types).
410
410
if(resolution.type!=='dlx'){
411
-
thrownewError('Unexpected resolution type for coana')
411
+
thrownewError(
412
+
`internal: resolveCoana returned resolution.type="${resolution.type}" (expected "dlx"); this is a resolver contract bug — re-run with --debug and report the output`,
413
+
)
412
414
}
413
415
constresult=awaitspawnDlx(
414
416
{
@@ -484,7 +486,9 @@ export async function spawnCdxgenDlx(
484
486
485
487
// Use dlx version (resolveCdxgen only returns 'local' or 'dlx' types).
486
488
if(resolution.type!=='dlx'){
487
-
thrownewError('Unexpected resolution type for cdxgen')
489
+
thrownewError(
490
+
`internal: resolveCdxgen returned resolution.type="${resolution.type}" (expected "dlx"); this is a resolver contract bug — re-run with --debug and report the output`,
491
+
)
488
492
}
489
493
returnawaitspawnDlx(
490
494
resolution.details,
@@ -554,7 +558,9 @@ export async function spawnSfwDlx(
554
558
555
559
// Use dlx version (resolveSfw only returns 'local' or 'dlx' types).
556
560
if(resolution.type!=='dlx'){
557
-
thrownewError('Unexpected resolution type for sfw')
561
+
thrownewError(
562
+
`internal: resolveSfw returned resolution.type="${resolution.type}" (expected "dlx"); this is a resolver contract bug — re-run with --debug and report the output`,
563
+
)
558
564
}
559
565
returnawaitspawnDlx(
560
566
resolution.details,
@@ -675,21 +681,25 @@ async function spawnToolVfs(
675
681
): Promise<DlxSpawnResult>{
676
682
if(!areExternalToolsAvailable()){
677
683
thrownewError(
678
-
`Cannot spawn ${tool} from VFS - tools not available in SEA mode`,
684
+
`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`,
679
685
)
680
686
}
681
687
682
688
// Extract tools from VFS (returns paths directly).
683
689
consttoolPaths=awaitextractExternalTools()
684
690
if(!toolPaths){
685
-
thrownewError(`Failed to extract ${tool} from VFS`)
691
+
thrownewError(
692
+
`failed to extract ${tool} from VFS (extractExternalTools returned null); the embedded tool archive may be corrupt — rebuild the SEA binary`,
693
+
)
686
694
}
687
695
688
696
// Get tool path.
689
697
consttoolPath=toolPaths[tool]
690
698
691
699
if(!toolPath){
692
-
thrownewError(`Tool path not found for ${tool}`)
700
+
thrownewError(
701
+
`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`,
`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`,
953
+
)
942
954
}
943
955
944
956
// Asset name format matches checksums in bundle-tools.json.
@@ -1000,7 +1012,7 @@ async function downloadPython(pythonDir: string): Promise<void> {
'tar is required to extract Python. Please install tar for your system.',
1015
+
`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`,
`Failed to acquire Python installation lock after ${MAX_RETRIES} retries. `+
1050
-
'Please check for filesystem issues or competing processes.',
1065
+
`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`,
'Timeout waiting for Python download by another process',
1121
+
`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`,
`Python binary not found after extraction: ${pythonBin}`,
1132
+
`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`,
1122
1133
)
1123
1134
}
1124
1135
@@ -1218,7 +1229,9 @@ async function downloadPyPiWheel(
1218
1229
try{
1219
1230
constresponse=awaitsocketHttpRequest(pypiUrl)
1220
1231
if(!response.ok){
1221
-
thrownewError(`PyPI API returned ${response.status}`)
1232
+
thrownewError(
1233
+
`PyPI returned HTTP ${response.status} for ${pypiUrl} (expected 200); check the package name and version, or retry if the registry is rate-limiting`,
1234
+
)
1222
1235
}
1223
1236
constdata=response.json()as{
1224
1237
urls?: Array<{filename: string;url: string}>
@@ -1235,14 +1248,13 @@ async function downloadPyPiWheel(
1235
1248
// If we can't fetch from API, construct URL directly (may not work for all packages).
1236
1249
// This is a fallback; the API approach is more reliable.
1237
1250
thrownewInputError(
1238
-
`Failed to fetch PyPI package info for ${packageName}@${version}: ${getErrorCause(e)}`,
1251
+
`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`,
1239
1252
)
1240
1253
}
1241
1254
1242
1255
if(!wheelUrl){
1243
1256
thrownewInputError(
1244
-
`No wheel found for ${packageName}@${version} on PyPI. `+
1245
-
'This package may only be available as a source distribution.',
1257
+
`${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`,
1246
1258
)
1247
1259
}
1248
1260
@@ -1275,8 +1287,7 @@ export async function ensureSocketPyCli(
1275
1287
1276
1288
if(retryCount>=MAX_RETRIES){
1277
1289
thrownewInputError(
1278
-
`Failed to acquire Socket Python CLI installation lock after ${MAX_RETRIES} retries. `+
1279
-
'Please check for filesystem issues or competing processes.',
1290
+
`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`,
1280
1291
)
1281
1292
}
1282
1293
@@ -1386,7 +1397,7 @@ export async function ensureSocketPyCli(
1386
1397
})
1387
1398
}else{
1388
1399
thrownewInputError(
1389
-
`Failed to download verified socketsecurity wheel for version ${pyCliVersion}`,
1400
+
`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`,
1390
1401
)
1391
1402
}
1392
1403
}else{
@@ -1454,7 +1465,7 @@ export async function spawnSocketPyCliVfs(
1454
1465
})
1455
1466
}else{
1456
1467
thrownewError(
1457
-
`Failed to download verified socketsecurity wheel for version ${pyCliVersion}`,
1468
+
`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`,
1458
1469
)
1459
1470
}
1460
1471
}else{
@@ -1673,7 +1684,9 @@ async function spawnTrivyDlx(
1673
1684
constresolution=resolveTrivy()
1674
1685
1675
1686
if(resolution.type!=='github-release'){
1676
-
thrownewError('Unexpected resolution type for trivy')
1687
+
thrownewError(
1688
+
`internal: resolveTrivy returned resolution.type="${resolution.type}" (expected "github-release"); this is a resolver contract bug — re-run with --debug and report the output`,
1689
+
)
1677
1690
}
1678
1691
1679
1692
const{env: spawnEnv, ...dlxOptions}={
@@ -1735,7 +1748,9 @@ async function spawnTrufflehogDlx(
1735
1748
constresolution=resolveTrufflehog()
1736
1749
1737
1750
if(resolution.type!=='github-release'){
1738
-
thrownewError('Unexpected resolution type for trufflehog')
1751
+
thrownewError(
1752
+
`internal: resolveTrufflehog returned resolution.type="${resolution.type}" (expected "github-release"); this is a resolver contract bug — re-run with --debug and report the output`,
1753
+
)
1739
1754
}
1740
1755
1741
1756
const{env: spawnEnv, ...dlxOptions}={
@@ -1797,7 +1812,9 @@ async function spawnOpengrepDlx(
1797
1812
constresolution=resolveOpengrep()
1798
1813
1799
1814
if(resolution.type!=='github-release'){
1800
-
thrownewError('Unexpected resolution type for opengrep')
1815
+
thrownewError(
1816
+
`internal: resolveOpengrep returned resolution.type="${resolution.type}" (expected "github-release"); this is a resolver contract bug — re-run with --debug and report the output`,
0 commit comments