Skip to content

Commit 12288ed

Browse files
authored
fix(cli): error messages in utils/ misc (flags, fs, git, npm, promise, terminal) (#1260)
* fix(cli): align utils/ miscellaneous error messages with 4-ingredient strategy 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. * fix(cli): address Cursor bugbot findings on error messages 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 #1260 bugbot review. * 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.
1 parent e272f33 commit 12288ed

12 files changed

Lines changed: 38 additions & 22 deletions

File tree

packages/cli/src/flags.mts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,12 @@ function getRawSpaceSizeFlags(): RawSpaceSizeFlags {
6464
// Validate numeric flags (should be guaranteed by meow type='number', but defensive).
6565
if (Number.isNaN(maxOldSpaceSize) || maxOldSpaceSize < 0) {
6666
throw new Error(
67-
`Invalid value for --max-old-space-size: ${cli.flags['maxOldSpaceSize']}`,
67+
`--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`,
6868
)
6969
}
7070
if (Number.isNaN(maxSemiSpaceSize) || maxSemiSpaceSize < 0) {
7171
throw new Error(
72-
`Invalid value for --max-semi-space-size: ${cli.flags['maxSemiSpaceSize']}`,
72+
`--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`,
7373
)
7474
}
7575

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { createHash } from 'node:crypto'
1616
import { homedir } from 'node:os'
1717
import path from 'node:path'
1818

19+
import { joinAnd } from '@socketsecurity/lib/arrays'
1920
import { getDefaultLogger } from '@socketsecurity/lib/logger'
2021
import { normalizePath } from '@socketsecurity/lib/paths/normalize'
2122
import { spawn } from '@socketsecurity/lib/spawn'
@@ -176,7 +177,7 @@ export async function extractBasicsTools(
176177
const missingTools = tools.filter(t => !extractedPaths[t])
177178
if (missingTools.length) {
178179
throw new Error(
179-
`Failed to extract all basics tools. Missing: ${missingTools.join(', ')}`,
180+
`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`,
180181
)
181182
}
182183

@@ -186,7 +187,9 @@ export async function extractBasicsTools(
186187
const pythonExe = isPlatWin ? 'python3.exe' : 'python3'
187188
const pythonDir = extractedPaths['python']
188189
if (!pythonDir) {
189-
throw new Error('Python extraction path not found')
190+
throw new Error(
191+
`extractedPaths.python is undefined after VFS extraction (expected a directory path); the basics SEA bundle is missing Python — rebuild the SEA binary`,
192+
)
190193
}
191194
const pythonPath = normalizePath(path.join(pythonDir, 'bin', pythonExe))
192195

@@ -197,7 +200,7 @@ export async function extractBasicsTools(
197200

198201
if (!validateResult || validateResult.code !== 0) {
199202
throw new Error(
200-
`Python validation failed: ${validateResult?.stderr || 'Unable to execute Python'}`,
203+
`extracted Python at ${pythonPath} failed to run with exit code ${validateResult?.code ?? 'null'} (stderr: ${validateResult?.stderr || '<none>'}); the extracted binary may be corrupt or missing a shared lib — rebuild the SEA binary`,
201204
)
202205
}
203206

@@ -218,7 +221,7 @@ export async function extractBasicsTools(
218221

219222
if (!toolValidateResult || toolValidateResult.code !== 0) {
220223
throw new Error(
221-
`${tool} validation failed: ${toolValidateResult?.stderr || `Unable to execute ${tool}`}`,
224+
`extracted ${tool} at ${toolPath} failed to run with exit code ${toolValidateResult?.code ?? 'null'} (stderr: ${toolValidateResult?.stderr || '<none>'}); the extracted binary may be corrupt or missing a shared lib — rebuild the SEA binary`,
222225
)
223226
}
224227

packages/cli/src/utils/config.mts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,9 @@ function getConfigValues(retryCount = 0): LocalConfig {
140140
const decoded = Buffer.from(rawString, 'base64').toString('utf8')
141141
// Check for invalid UTF-8 sequences (replacement character).
142142
if (decoded.includes('\ufffd')) {
143-
throw new Error('Invalid UTF-8 in base64-encoded config')
143+
throw new Error(
144+
`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\``,
145+
)
144146
}
145147
const parsed = JSON.parse(decoded)
146148
// Only copy supported config keys to prevent prototype pollution.

packages/cli/src/utils/fs/path-resolve.mts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export function findNpmDirPathSync(npmBinPath: string): string | undefined {
5555
while (true) {
5656
if (iterations >= MAX_ITERATIONS) {
5757
throw new Error(
58-
`path traversal exceeded maximum iterations of ${MAX_ITERATIONS}`,
58+
`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`,
5959
)
6060
}
6161
iterations += 1

packages/cli/src/utils/git/gitlab-provider.mts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ export class GitLabProvider implements PrProvider {
9898
}
9999

100100
throw new Error(
101-
`Failed to create merge request after ${retries} attempts: ${owner}/${repo}#${head}`,
101+
`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`,
102102
)
103103
}
104104

@@ -327,6 +327,6 @@ function getGitLabToken(): string {
327327
}
328328

329329
throw new Error(
330-
'GitLab token not found. Set GITLAB_TOKEN environment variable.',
330+
`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=<token>`,
331331
)
332332
}

packages/cli/src/utils/git/operations.mts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@ async function getGitPath(): Promise<string> {
5555
if (!_gitPath) {
5656
const result = await whichReal('git', { nothrow: true })
5757
if (!result || Array.isArray(result)) {
58-
throw new Error('git not found in PATH')
58+
throw new Error(
59+
`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`,
60+
)
5961
}
6062
_gitPath = result
6163
}

packages/cli/src/utils/npm/spec.mts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,9 @@ export function safeNpmSpecToPurl(pkgSpec: string): string | undefined {
184184
export function npmSpecToPurl(pkgSpec: string): string {
185185
const purl = safeNpmSpecToPurl(pkgSpec)
186186
if (!purl) {
187-
throw new Error(`Failed to convert ${NPM} spec to PURL: ${pkgSpec}`)
187+
throw new Error(
188+
`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`,
189+
)
188190
}
189191
return purl
190192
}

packages/cli/src/utils/promise/queue.mts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ export class PromiseQueue {
2727
this.maxQueueLength = maxQueueLength
2828
}
2929
if (maxConcurrency < 1) {
30-
throw new Error('maxConcurrency must be at least 1')
30+
throw new Error(
31+
`PromiseQueue maxConcurrency must be >= 1 (saw: ${maxConcurrency}); pass a positive integer like 4 or 8`,
32+
)
3133
}
3234
}
3335

packages/cli/src/utils/terminal/iocraft.mts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
import { createRequire } from 'node:module'
99

10+
import { getErrorCause } from '../error/errors.mts'
11+
1012
import type iocraft from '@socketaddon/iocraft'
1113

1214
// Re-export iocraft types for direct access when needed.
@@ -27,8 +29,7 @@ function getIocraft(): typeof iocraft {
2729
iocraftInstance = loaded.default || loaded
2830
} catch (e) {
2931
throw new Error(
30-
`Failed to load iocraft native module: ${e}\n` +
31-
`Make sure @socketaddon/iocraft is installed and your platform is supported.`,
32+
`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`,
3233
)
3334
}
3435
}

packages/cli/test/unit/utils/git/gitlab-provider.test.mts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ describe('git/gitlab-provider', () => {
7272
it('throws error when no token available', () => {
7373
delete process.env['GITLAB_TOKEN']
7474
expect(() => new GitLabProvider()).toThrow(
75-
'GitLab token not found. Set GITLAB_TOKEN environment variable.',
75+
/GitLab access requires a token but process\.env\.GITLAB_TOKEN is not set/,
7676
)
7777
})
7878
})
@@ -193,7 +193,9 @@ describe('git/gitlab-provider', () => {
193193
retries: 2,
194194
title: 'Test',
195195
}),
196-
).rejects.toThrow('Failed to create merge request after 2 attempts')
196+
).rejects.toThrow(
197+
/GitLab API rejected createMergeRequest for owner\/repo .*after 2 attempts/,
198+
)
197199
})
198200

199201
it('does not retry on 400 errors', async () => {
@@ -212,7 +214,9 @@ describe('git/gitlab-provider', () => {
212214
retries: 3,
213215
title: 'Test',
214216
}),
215-
).rejects.toThrow('Failed to create merge request after 3 attempts')
217+
).rejects.toThrow(
218+
/GitLab API rejected createMergeRequest for owner\/repo .*after 3 attempts/,
219+
)
216220

217221
expect(mockCreate).toHaveBeenCalledTimes(1)
218222
})

0 commit comments

Comments
 (0)