diff --git a/packages/cli/src/commands/ask/cmd-ask.mts b/packages/cli/src/commands/ask/cmd-ask.mts index 67194fbff..a5ede7672 100644 --- a/packages/cli/src/commands/ask/cmd-ask.mts +++ b/packages/cli/src/commands/ask/cmd-ask.mts @@ -83,7 +83,7 @@ async function run( if (!query) { throw new InputError( - 'Please provide a question.\n\nExample: socket ask "scan for vulnerabilities"', + 'socket ask requires a QUERY positional argument; pass a question like `socket ask "scan for vulnerabilities"`', ) } diff --git a/packages/cli/src/commands/audit-log/cmd-audit-log.mts b/packages/cli/src/commands/audit-log/cmd-audit-log.mts index 6d9d3cd7c..a5b1fea11 100644 --- a/packages/cli/src/commands/audit-log/cmd-audit-log.mts +++ b/packages/cli/src/commands/audit-log/cmd-audit-log.mts @@ -1,6 +1,7 @@ import { handleAuditLog } from './handle-audit-log.mts' import { FLAG_JSON, FLAG_MARKDOWN } from '../../constants/cli.mts' import { outputDryRunFetch } from '../../utils/dry-run/output.mts' +import { InputError } from '../../utils/error/errors.mts' import { V1_MIGRATION_GUIDE_URL } from '../../constants/socket.mjs' import { commonFlags, outputFlags } from '../../flags.mts' import { meowOrExit } from '../../utils/cli/with-subcommands.mjs' @@ -191,10 +192,14 @@ async function run( } if (Number.isNaN(validatedPage) || validatedPage < 0) { - throw new Error(`Invalid value for --page: ${page}`) + throw new InputError( + `--page must be a non-negative integer (saw: "${page}"); pass a number like --page=1`, + ) } if (Number.isNaN(validatedPerPage) || validatedPerPage < 0) { - throw new Error(`Invalid value for --per-page: ${perPage}`) + throw new InputError( + `--per-page must be a non-negative integer (saw: "${perPage}"); pass a number like --per-page=30`, + ) } await handleAuditLog({ diff --git a/packages/cli/src/commands/config/handle-config-set.mts b/packages/cli/src/commands/config/handle-config-set.mts index 3349cb016..2eded70a7 100644 --- a/packages/cli/src/commands/config/handle-config-set.mts +++ b/packages/cli/src/commands/config/handle-config-set.mts @@ -2,6 +2,7 @@ import { debug, debugDir } from '@socketsecurity/lib/debug' import { outputConfigSet } from './output-config-set.mts' import { updateConfigValue } from '../../utils/config.mts' +import { InputError } from '../../utils/error/errors.mts' import type { OutputKind } from '../../types.mts' import type { LocalConfig } from '../../utils/config.mts' @@ -16,7 +17,9 @@ export async function handleConfigSet({ outputKind: OutputKind }) { if (value === undefined) { - throw new Error('Value is required for config set') + throw new InputError( + `socket config set ${key} requires a VALUE argument; pass the value as the second positional (e.g. \`socket config set ${key} my-value\`)`, + ) } debug(`Setting config ${key} = ${value}`) diff --git a/packages/cli/src/commands/fix/cmd-fix.mts b/packages/cli/src/commands/fix/cmd-fix.mts index bd1ebb033..9d862a2e0 100644 --- a/packages/cli/src/commands/fix/cmd-fix.mts +++ b/packages/cli/src/commands/fix/cmd-fix.mts @@ -371,7 +371,7 @@ async function run( for (const ecosystem of ecosystemsRaw) { if (!validEcosystemChoices.includes(ecosystem)) { logger.fail( - `Invalid ecosystem: "${ecosystem}". Valid values are: ${joinAnd(validEcosystemChoices)}`, + `--ecosystems must be one of: ${joinAnd(validEcosystemChoices)} (saw: "${ecosystem}"); pass a supported ecosystem like --ecosystems=${validEcosystemChoices[0]}`, ) process.exitCode = 1 return diff --git a/packages/cli/src/commands/fix/coana-fix.mts b/packages/cli/src/commands/fix/coana-fix.mts index 2f28c4833..613b0a576 100644 --- a/packages/cli/src/commands/fix/coana-fix.mts +++ b/packages/cli/src/commands/fix/coana-fix.mts @@ -340,7 +340,9 @@ export async function coanaFix( if (ghsaIdsRaw && ghsaIdsRaw.trim()) { const parsed = JSON.parse(ghsaIdsRaw) if (!Array.isArray(parsed)) { - throw new Error('Expected array of GHSA IDs from coana output') + throw new Error( + `coana find-vulnerabilities returned non-array JSON on last line (got: ${typeof parsed}); expected an array of GHSA ID strings`, + ) } discoveredIds.push(...parsed) } diff --git a/packages/cli/src/commands/login/cmd-login.mts b/packages/cli/src/commands/login/cmd-login.mts index 3b13d2c36..6ddaaf934 100644 --- a/packages/cli/src/commands/login/cmd-login.mts +++ b/packages/cli/src/commands/login/cmd-login.mts @@ -97,7 +97,7 @@ async function run( if (!isInteractive()) { throw new InputError( - 'Cannot prompt for credentials in a non-interactive shell. Use SOCKET_CLI_API_TOKEN environment variable instead', + 'socket login needs an interactive TTY to prompt for credentials (stdin/stdout is not a TTY); set SOCKET_CLI_API_TOKEN in the environment instead', ) } diff --git a/packages/cli/src/commands/manifest/convert-gradle-to-maven.mts b/packages/cli/src/commands/manifest/convert-gradle-to-maven.mts index 319b41feb..2d4577a46 100644 --- a/packages/cli/src/commands/manifest/convert-gradle-to-maven.mts +++ b/packages/cli/src/commands/manifest/convert-gradle-to-maven.mts @@ -178,7 +178,9 @@ async function execGradleWithSpinner( }) if (!output) { - throw new Error(`Failed to execute gradle: ${bin}`) + throw new Error( + `spawn returned no output for gradle (bin: ${bin}); check that the gradlew wrapper is executable and re-run with --verbose`, + ) } pass = true diff --git a/packages/cli/src/commands/organization/cmd-organization-dependencies.mts b/packages/cli/src/commands/organization/cmd-organization-dependencies.mts index d75704baf..53568f58b 100644 --- a/packages/cli/src/commands/organization/cmd-organization-dependencies.mts +++ b/packages/cli/src/commands/organization/cmd-organization-dependencies.mts @@ -1,6 +1,7 @@ import { handleDependencies } from './handle-dependencies.mts' import { FLAG_JSON, FLAG_MARKDOWN } from '../../constants/cli.mts' import { outputDryRunFetch } from '../../utils/dry-run/output.mts' +import { InputError } from '../../utils/error/errors.mts' import { commonFlags, outputFlags } from '../../flags.mts' import { meowOrExit } from '../../utils/cli/with-subcommands.mjs' import { @@ -115,10 +116,14 @@ async function run( } if (Number.isNaN(validatedLimit) || validatedLimit < 0) { - throw new Error(`Invalid value for --limit: ${limit}`) + throw new InputError( + `--limit must be a non-negative integer (saw: "${limit}"); pass a number like --limit=50`, + ) } if (Number.isNaN(validatedOffset) || validatedOffset < 0) { - throw new Error(`Invalid value for --offset: ${offset}`) + throw new InputError( + `--offset must be a non-negative integer (saw: "${offset}"); pass a number like --offset=0`, + ) } await handleDependencies({ diff --git a/packages/cli/src/commands/scan/cmd-scan-create.mts b/packages/cli/src/commands/scan/cmd-scan-create.mts index fc33239e1..43fffeb85 100644 --- a/packages/cli/src/commands/scan/cmd-scan-create.mts +++ b/packages/cli/src/commands/scan/cmd-scan-create.mts @@ -13,6 +13,7 @@ import { suggestTarget } from './suggest_target.mts' import { validateReachabilityTarget } from './validate-reachability-target.mts' import constants, { REQUIREMENTS_TXT, SOCKET_JSON } from '../../constants.mts' import { outputDryRunUpload } from '../../utils/dry-run/output.mts' +import { InputError } from '../../utils/error/errors.mts' import { commonFlags, outputFlags } from '../../flags.mts' import { meowOrExit } from '../../utils/cli/with-subcommands.mts' import { getEcosystemChoicesForMeow } from '../../utils/ecosystem/types.mts' @@ -441,8 +442,8 @@ async function run( const validEcosystems = getEcosystemChoicesForMeow() for (const ecosystem of reachEcosystemsRaw) { if (!validEcosystems.includes(ecosystem)) { - throw new Error( - `Invalid ecosystem: "${ecosystem}". Valid values are: ${joinAnd(validEcosystems)}`, + throw new InputError( + `--reach-ecosystems must be one of: ${joinAnd(validEcosystems)} (saw: "${ecosystem}"); pass a supported ecosystem like --reach-ecosystems=${validEcosystems[0]}`, ) } reachEcosystems.push(ecosystem as PURL_Type) @@ -726,8 +727,15 @@ async function run( // Validate numeric flag conversions. const validatedPullRequest = Number(pullRequest) - if (pullRequest !== undefined && Number.isNaN(validatedPullRequest)) { - throw new Error(`Invalid number value for --pull-request: ${pullRequest}`) + if ( + pullRequest !== undefined && + (Number.isNaN(validatedPullRequest) || + !Number.isInteger(validatedPullRequest) || + validatedPullRequest < 0) + ) { + throw new InputError( + `--pull-request must be a non-negative integer (saw: "${pullRequest}"); pass a number like --pull-request=42`, + ) } const validatedReachAnalysisMemoryLimit = Number(reachAnalysisMemoryLimit) @@ -735,8 +743,8 @@ async function run( reachAnalysisMemoryLimit !== undefined && Number.isNaN(validatedReachAnalysisMemoryLimit) ) { - throw new Error( - `Invalid number value for --reach-analysis-memory-limit: ${reachAnalysisMemoryLimit}`, + throw new InputError( + `--reach-analysis-memory-limit must be a number of megabytes (saw: "${reachAnalysisMemoryLimit}"); pass an integer like --reach-analysis-memory-limit=4096`, ) } @@ -745,18 +753,20 @@ async function run( reachAnalysisTimeout !== undefined && Number.isNaN(validatedReachAnalysisTimeout) ) { - throw new Error( - `Invalid number value for --reach-analysis-timeout: ${reachAnalysisTimeout}`, + throw new InputError( + `--reach-analysis-timeout must be a number of seconds (saw: "${reachAnalysisTimeout}"); pass an integer like --reach-analysis-timeout=300`, ) } const validatedReachConcurrency = Number(reachConcurrency) if ( reachConcurrency !== undefined && - Number.isNaN(validatedReachConcurrency) + (Number.isNaN(validatedReachConcurrency) || + !Number.isInteger(validatedReachConcurrency) || + validatedReachConcurrency <= 0) ) { - throw new Error( - `Invalid number value for --reach-concurrency: ${reachConcurrency}`, + throw new InputError( + `--reach-concurrency must be a positive integer (saw: "${reachConcurrency}"); pass a number like --reach-concurrency=4`, ) } diff --git a/packages/cli/src/commands/scan/cmd-scan-list.mts b/packages/cli/src/commands/scan/cmd-scan-list.mts index 596bb51db..6147cc719 100644 --- a/packages/cli/src/commands/scan/cmd-scan-list.mts +++ b/packages/cli/src/commands/scan/cmd-scan-list.mts @@ -1,5 +1,6 @@ import { handleListScans } from './handle-list-scans.mts' import { outputDryRunFetch } from '../../utils/dry-run/output.mts' +import { InputError } from '../../utils/error/errors.mts' import { V1_MIGRATION_GUIDE_URL } from '../../constants/socket.mts' import { commonFlags, outputFlags } from '../../flags.mts' import { meowOrExit } from '../../utils/cli/with-subcommands.mjs' @@ -201,10 +202,14 @@ async function run( } if (Number.isNaN(validatedPage) || validatedPage < 1) { - throw new Error(`Invalid value for --page: ${cli.flags['page']}`) + throw new InputError( + `--page must be a positive integer (saw: "${cli.flags['page']}"); pass a number like --page=1`, + ) } if (Number.isNaN(validatedPerPage) || validatedPerPage < 1) { - throw new Error(`Invalid value for --per-page: ${cli.flags['perPage']}`) + throw new InputError( + `--per-page must be a positive integer (saw: "${cli.flags['perPage']}"); pass a number like --per-page=30`, + ) } await handleListScans({ diff --git a/packages/cli/src/commands/scan/cmd-scan-reach.mts b/packages/cli/src/commands/scan/cmd-scan-reach.mts index fc94b80e1..8722a7769 100644 --- a/packages/cli/src/commands/scan/cmd-scan-reach.mts +++ b/packages/cli/src/commands/scan/cmd-scan-reach.mts @@ -7,6 +7,7 @@ import { reachabilityFlags } from './reachability-flags.mts' import { suggestTarget } from './suggest_target.mts' import { validateReachabilityTarget } from './validate-reachability-target.mts' import { outputDryRunExecute } from '../../utils/dry-run/output.mts' +import { InputError } from '../../utils/error/errors.mts' import { commonFlags, outputFlags } from '../../flags.mts' import { meowOrExit } from '../../utils/cli/with-subcommands.mts' import { getEcosystemChoicesForMeow } from '../../utils/ecosystem/types.mts' @@ -171,8 +172,8 @@ async function run( const validEcosystems = getEcosystemChoicesForMeow() for (const ecosystem of reachEcosystemsRaw) { if (!validEcosystems.includes(ecosystem)) { - throw new Error( - `Invalid ecosystem: "${ecosystem}". Valid values are: ${joinAnd(validEcosystems)}`, + throw new InputError( + `--reach-ecosystems must be one of: ${joinAnd(validEcosystems)} (saw: "${ecosystem}"); pass a supported ecosystem like --reach-ecosystems=${validEcosystems[0]}`, ) } reachEcosystems.push(ecosystem as PURL_Type) @@ -277,8 +278,8 @@ async function run( reachAnalysisMemoryLimit !== undefined && Number.isNaN(validatedReachAnalysisMemoryLimit) ) { - throw new Error( - `Invalid number value for --reach-analysis-memory-limit: ${reachAnalysisMemoryLimit}`, + throw new InputError( + `--reach-analysis-memory-limit must be a number of megabytes (saw: "${reachAnalysisMemoryLimit}"); pass an integer like --reach-analysis-memory-limit=4096`, ) } @@ -287,18 +288,20 @@ async function run( reachAnalysisTimeout !== undefined && Number.isNaN(validatedReachAnalysisTimeout) ) { - throw new Error( - `Invalid number value for --reach-analysis-timeout: ${reachAnalysisTimeout}`, + throw new InputError( + `--reach-analysis-timeout must be a number of seconds (saw: "${reachAnalysisTimeout}"); pass an integer like --reach-analysis-timeout=300`, ) } const validatedReachConcurrency = Number(reachConcurrency) if ( reachConcurrency !== undefined && - Number.isNaN(validatedReachConcurrency) + (Number.isNaN(validatedReachConcurrency) || + !Number.isInteger(validatedReachConcurrency) || + validatedReachConcurrency <= 0) ) { - throw new Error( - `Invalid number value for --reach-concurrency: ${reachConcurrency}`, + throw new InputError( + `--reach-concurrency must be a positive integer (saw: "${reachConcurrency}"); pass a number like --reach-concurrency=4`, ) } diff --git a/packages/cli/src/commands/threat-feed/cmd-threat-feed.mts b/packages/cli/src/commands/threat-feed/cmd-threat-feed.mts index 92861924f..c743838f0 100644 --- a/packages/cli/src/commands/threat-feed/cmd-threat-feed.mts +++ b/packages/cli/src/commands/threat-feed/cmd-threat-feed.mts @@ -4,6 +4,7 @@ import { getDefaultLogger } from '@socketsecurity/lib/logger' import { handleThreatFeed } from './handle-threat-feed.mts' import { outputDryRunFetch } from '../../utils/dry-run/output.mts' +import { InputError } from '../../utils/error/errors.mts' import { commonFlags, outputFlags } from '../../flags.mts' import { meowOrExit } from '../../utils/cli/with-subcommands.mjs' import { @@ -288,7 +289,9 @@ async function run( return } if (Number.isNaN(validatedPerPage) || validatedPerPage < 1) { - throw new Error(`Invalid value for --per-page: ${cli.flags['perPage']}`) + throw new InputError( + `--per-page must be a positive integer (saw: "${cli.flags['perPage']}"); pass a number like --per-page=30`, + ) } await handleThreatFeed({ diff --git a/packages/cli/src/commands/wrapper/add-socket-wrapper.mts b/packages/cli/src/commands/wrapper/add-socket-wrapper.mts index fad9ce57b..adc744dd9 100644 --- a/packages/cli/src/commands/wrapper/add-socket-wrapper.mts +++ b/packages/cli/src/commands/wrapper/add-socket-wrapper.mts @@ -1,6 +1,9 @@ import { promises as fs } from 'node:fs' import { getDefaultLogger } from '@socketsecurity/lib/logger' + +import { FileSystemError, getErrorCause } from '../../utils/error/errors.mts' + const logger = getDefaultLogger() export async function addSocketWrapper(file: string): Promise { @@ -10,7 +13,14 @@ export async function addSocketWrapper(file: string): Promise { 'alias npm="socket npm"\nalias npx="socket npx"\n', ) } catch (e) { - throw new Error(`There was an error setting up the alias: ${e}`) + // Don't include `file` in the message: display.formatErrorForDisplay + // appends `(${error.path})` automatically when FileSystemError carries + // a path, so embedding it here would show the filename twice. + throw new FileSystemError( + `failed to append socket aliases (${getErrorCause(e)}); check that the file exists and is writable`, + file, + (e as NodeJS.ErrnoException)?.code, + ) } logger.success( `The alias was added to ${file}. Running 'npm install' will now be wrapped in Socket's "safe npm" 🎉`, diff --git a/packages/cli/src/commands/wrapper/postinstall-wrapper.mts b/packages/cli/src/commands/wrapper/postinstall-wrapper.mts index 48b3c77ad..6ee329fac 100644 --- a/packages/cli/src/commands/wrapper/postinstall-wrapper.mts +++ b/packages/cli/src/commands/wrapper/postinstall-wrapper.mts @@ -8,7 +8,7 @@ import { addSocketWrapper } from './add-socket-wrapper.mts' import { checkSocketWrapperSetup } from './check-socket-wrapper-setup.mts' import { getBashRcPath, getZshRcPath } from '../../constants/paths.mts' import { getBashrcDetails } from '../../utils/cli/completion.mts' -import { getErrorCause } from '../../utils/error/errors.mjs' +import { FileSystemError, getErrorCause } from '../../utils/error/errors.mjs' import { updateInstalledTabCompletionScript } from '../install/setup-tab-completion.mts' const logger = getDefaultLogger() @@ -85,8 +85,10 @@ async function setupSocketWrapper(query: string): Promise { await addSocketWrapper(zshRcPath) } } catch (e) { - throw new Error( - `There was an issue setting up the alias: ${getErrorCause(e)}`, + throw new FileSystemError( + `failed to add socket aliases to ${bashRcPath} / ${zshRcPath} (${getErrorCause(e)}); check that your shell rc files exist and are writable`, + undefined, + (e as NodeJS.ErrnoException)?.code, ) } } diff --git a/packages/cli/test/integration/cli/cmd-ask.test.mts b/packages/cli/test/integration/cli/cmd-ask.test.mts index d5b5dab58..9f94fca1a 100644 --- a/packages/cli/test/integration/cli/cmd-ask.test.mts +++ b/packages/cli/test/integration/cli/cmd-ask.test.mts @@ -62,7 +62,7 @@ describe('socket ask', async () => { 'should error when no query provided', async cmd => { const { code, stdout } = await spawnSocketCli(binCliPath, cmd) - expect(stdout).toContain('Please provide a question') + expect(stdout).toContain('requires a QUERY positional argument') expect(code, 'should exit with non-zero code').not.toBe(0) }, ) diff --git a/packages/cli/test/integration/cli/cmd-scan-create.test.mts b/packages/cli/test/integration/cli/cmd-scan-create.test.mts index ead3b40cb..4c069467a 100644 --- a/packages/cli/test/integration/cli/cmd-scan-create.test.mts +++ b/packages/cli/test/integration/cli/cmd-scan-create.test.mts @@ -541,7 +541,7 @@ describe('socket scan create', async () => { async cmd => { const { code, stderr, stdout } = await spawnSocketCli(binCliPath, cmd) const output = stdout + stderr - expect(output).toContain('Invalid ecosystem: "invalid-ecosystem"') + expect(output).toContain('(saw: "invalid-ecosystem")') expect( code, 'should exit with non-zero code when invalid ecosystem is provided', @@ -661,7 +661,7 @@ describe('socket scan create', async () => { async cmd => { const { code, stderr, stdout } = await spawnSocketCli(binCliPath, cmd) const output = stdout + stderr - expect(output).toContain('Invalid ecosystem: "invalid"') + expect(output).toContain('(saw: "invalid")') expect( code, 'should exit with non-zero code when invalid ecosystem provided', diff --git a/packages/cli/test/integration/cli/cmd-scan-reach-dry-run.test.mts b/packages/cli/test/integration/cli/cmd-scan-reach-dry-run.test.mts index 9e49b8719..f242f90a2 100644 --- a/packages/cli/test/integration/cli/cmd-scan-reach-dry-run.test.mts +++ b/packages/cli/test/integration/cli/cmd-scan-reach-dry-run.test.mts @@ -244,7 +244,7 @@ describe('socket scan reach - dry-run tests', async () => { async cmd => { const { code, stderr, stdout } = await spawnSocketCli(binCliPath, cmd) const output = stdout + stderr - expect(output).toContain('Invalid ecosystem: "invalid-ecosystem"') + expect(output).toContain('(saw: "invalid-ecosystem")') expect(code, 'should exit with non-zero code').not.toBe(0) }, ) @@ -445,7 +445,7 @@ describe('socket scan reach - dry-run tests', async () => { async cmd => { const { code, stderr, stdout } = await spawnSocketCli(binCliPath, cmd) const output = stdout + stderr - expect(output).toContain('Invalid ecosystem: "invalid1"') + expect(output).toContain('(saw: "invalid1")') expect(code, 'should exit with non-zero code').not.toBe(0) }, ) diff --git a/packages/cli/test/unit/commands/ask/cmd-ask.test.mts b/packages/cli/test/unit/commands/ask/cmd-ask.test.mts index 71f83c5c3..e1a74701f 100644 --- a/packages/cli/test/unit/commands/ask/cmd-ask.test.mts +++ b/packages/cli/test/unit/commands/ask/cmd-ask.test.mts @@ -50,7 +50,7 @@ describe('cmd-ask', () => { it('should throw InputError when no query provided', async () => { await expect(cmdAsk.run([], importMeta, context)).rejects.toThrow( - 'Please provide a question', + /socket ask requires a QUERY positional argument/, ) }) diff --git a/packages/cli/test/unit/commands/audit-log/cmd-audit-log.test.mts b/packages/cli/test/unit/commands/audit-log/cmd-audit-log.test.mts index 8e87c48f9..3b3f31845 100644 --- a/packages/cli/test/unit/commands/audit-log/cmd-audit-log.test.mts +++ b/packages/cli/test/unit/commands/audit-log/cmd-audit-log.test.mts @@ -296,7 +296,7 @@ describe('cmd-audit-log', () => { await expect( cmdAuditLog.run(['--page', 'invalid'], importMeta, context), - ).rejects.toThrow('Invalid value for --page') + ).rejects.toThrow(/--page must be a non-negative integer/) }) it('should validate per-page is numeric', async () => { @@ -304,7 +304,7 @@ describe('cmd-audit-log', () => { await expect( cmdAuditLog.run(['--per-page', 'invalid'], importMeta, context), - ).rejects.toThrow('Invalid value for --per-page') + ).rejects.toThrow(/--per-page must be a non-negative integer/) }) it('should reject negative page numbers', async () => { @@ -312,7 +312,7 @@ describe('cmd-audit-log', () => { await expect( cmdAuditLog.run(['--page', '-1'], importMeta, context), - ).rejects.toThrow('Invalid value for --page') + ).rejects.toThrow(/--page must be a non-negative integer/) }) it('should reject negative per-page numbers', async () => { @@ -320,7 +320,7 @@ describe('cmd-audit-log', () => { await expect( cmdAuditLog.run(['--per-page', '-1'], importMeta, context), - ).rejects.toThrow('Invalid value for --per-page') + ).rejects.toThrow(/--per-page must be a non-negative integer/) }) it('should accept zero as page number', async () => { diff --git a/packages/cli/test/unit/commands/fix/cmd-fix.test.mts b/packages/cli/test/unit/commands/fix/cmd-fix.test.mts index 53935f125..c50539e70 100644 --- a/packages/cli/test/unit/commands/fix/cmd-fix.test.mts +++ b/packages/cli/test/unit/commands/fix/cmd-fix.test.mts @@ -196,7 +196,7 @@ describe('cmd-fix', () => { expect(process.exitCode).toBe(1) expect(mockHandleFix).not.toHaveBeenCalled() expect(mockLogger.fail).toHaveBeenCalledWith( - expect.stringContaining('Invalid ecosystem'), + expect.stringContaining('--ecosystems must be one of'), ) }) diff --git a/packages/cli/test/unit/commands/login/cmd-login.test.mts b/packages/cli/test/unit/commands/login/cmd-login.test.mts index af68c2bba..106dfa71d 100644 --- a/packages/cli/test/unit/commands/login/cmd-login.test.mts +++ b/packages/cli/test/unit/commands/login/cmd-login.test.mts @@ -115,7 +115,7 @@ describe('cmd-login', () => { mockIsInteractive.mockReturnValue(false) await expect(cmdLogin.run([], importMeta, context)).rejects.toThrow( - 'Cannot prompt for credentials in a non-interactive shell', + /socket login needs an interactive TTY/, ) expect(mockAttemptLogin).not.toHaveBeenCalled() }) diff --git a/packages/cli/test/unit/commands/organization/cmd-organization-dependencies.test.mts b/packages/cli/test/unit/commands/organization/cmd-organization-dependencies.test.mts index 1e56f41f5..eff3a38b8 100644 --- a/packages/cli/test/unit/commands/organization/cmd-organization-dependencies.test.mts +++ b/packages/cli/test/unit/commands/organization/cmd-organization-dependencies.test.mts @@ -197,7 +197,9 @@ describe('cmd-organization-dependencies', () => { await expect( cmdOrganizationDependencies.run(['--limit', '-1'], importMeta, context), - ).rejects.toThrow('Invalid value for --limit: -1') + ).rejects.toThrow( + /--limit must be a non-negative integer \(saw: "-1"\)/, + ) expect(mockHandleDependencies).not.toHaveBeenCalled() }) @@ -211,7 +213,9 @@ describe('cmd-organization-dependencies', () => { importMeta, context, ), - ).rejects.toThrow('Invalid value for --offset: -1') + ).rejects.toThrow( + /--offset must be a non-negative integer \(saw: "-1"\)/, + ) expect(mockHandleDependencies).not.toHaveBeenCalled() }) @@ -225,7 +229,9 @@ describe('cmd-organization-dependencies', () => { importMeta, context, ), - ).rejects.toThrow('Invalid value for --limit: invalid') + ).rejects.toThrow( + /--limit must be a non-negative integer \(saw: "invalid"\)/, + ) expect(mockHandleDependencies).not.toHaveBeenCalled() }) @@ -239,7 +245,9 @@ describe('cmd-organization-dependencies', () => { importMeta, context, ), - ).rejects.toThrow('Invalid value for --offset: invalid') + ).rejects.toThrow( + /--offset must be a non-negative integer \(saw: "invalid"\)/, + ) expect(mockHandleDependencies).not.toHaveBeenCalled() }) diff --git a/packages/cli/test/unit/commands/scan/cmd-scan-create.test.mts b/packages/cli/test/unit/commands/scan/cmd-scan-create.test.mts index f3a7e7ec2..7fe41f8ff 100644 --- a/packages/cli/test/unit/commands/scan/cmd-scan-create.test.mts +++ b/packages/cli/test/unit/commands/scan/cmd-scan-create.test.mts @@ -763,7 +763,7 @@ describe('cmd-scan-create', () => { importMeta, context, ), - ).rejects.toThrow(/Invalid ecosystem/) + ).rejects.toThrow(/--reach-ecosystems must be one of/) }) it('should pass --commit-hash flag to handleCreateNewScan', async () => { @@ -878,7 +878,7 @@ describe('cmd-scan-create', () => { importMeta, context, ), - ).rejects.toThrow(/Invalid number value for --reach-analysis-memory-limit/) + ).rejects.toThrow(/--reach-analysis-memory-limit must be a number of megabytes/) }) it('should validate --reach-analysis-timeout is a number', async () => { @@ -898,7 +898,7 @@ describe('cmd-scan-create', () => { importMeta, context, ), - ).rejects.toThrow(/Invalid number value for --reach-analysis-timeout/) + ).rejects.toThrow(/--reach-analysis-timeout must be a number of seconds/) }) it('should validate --reach-concurrency is a number', async () => { @@ -918,7 +918,7 @@ describe('cmd-scan-create', () => { importMeta, context, ), - ).rejects.toThrow(/Invalid number value for --reach-concurrency/) + ).rejects.toThrow(/--reach-concurrency must be a positive integer/) }) }) diff --git a/packages/cli/test/unit/commands/scan/cmd-scan-reach.test.mts b/packages/cli/test/unit/commands/scan/cmd-scan-reach.test.mts index 0a496c74b..a9ee81b4d 100644 --- a/packages/cli/test/unit/commands/scan/cmd-scan-reach.test.mts +++ b/packages/cli/test/unit/commands/scan/cmd-scan-reach.test.mts @@ -288,7 +288,7 @@ describe('cmd-scan-reach', () => { importMeta, context, ), - ).rejects.toThrow(/Invalid ecosystem/) + ).rejects.toThrow(/--reach-ecosystems must be one of/) }) it('should support --json output mode', async () => { @@ -544,7 +544,7 @@ describe('cmd-scan-reach', () => { context, ), ).rejects.toThrow( - /Invalid number value for --reach-analysis-memory-limit/, + /--reach-analysis-memory-limit must be a number of megabytes/, ) }) @@ -557,7 +557,7 @@ describe('cmd-scan-reach', () => { importMeta, context, ), - ).rejects.toThrow(/Invalid number value for --reach-analysis-timeout/) + ).rejects.toThrow(/--reach-analysis-timeout must be a number of seconds/) }) it('should validate invalid numeric values for concurrency', async () => { @@ -569,7 +569,7 @@ describe('cmd-scan-reach', () => { importMeta, context, ), - ).rejects.toThrow(/Invalid number value for --reach-concurrency/) + ).rejects.toThrow(/--reach-concurrency must be a positive integer/) }) it('should default to current directory if no target specified', async () => { diff --git a/packages/cli/test/unit/commands/wrapper/add-socket-wrapper.test.mts b/packages/cli/test/unit/commands/wrapper/add-socket-wrapper.test.mts index 051bc12fa..edd6de3ee 100644 --- a/packages/cli/test/unit/commands/wrapper/add-socket-wrapper.test.mts +++ b/packages/cli/test/unit/commands/wrapper/add-socket-wrapper.test.mts @@ -77,9 +77,19 @@ describe('addSocketWrapper', () => { mockAppendFile.mockRejectedValue(error) + // The FileSystemError wraps the cause in the message; the path is + // stored on the `.path` property (not embedded in the message) to + // avoid display.formatErrorForDisplay double-printing it. Assert on + // the message shape + the path property separately. await expect(addSocketWrapper('/etc/protected-file')).rejects.toThrow( - 'There was an error setting up the alias', + /failed to append socket aliases \(Permission denied\)/, ) + await expect( + addSocketWrapper('/etc/protected-file'), + ).rejects.toMatchObject({ + name: 'FileSystemError', + path: '/etc/protected-file', + }) expect(fs.promises.appendFile).toHaveBeenCalledWith( '/etc/protected-file', diff --git a/packages/cli/test/unit/commands/wrapper/postinstall-wrapper.test.mts b/packages/cli/test/unit/commands/wrapper/postinstall-wrapper.test.mts index 2b5b89f4b..ccc7865fb 100644 --- a/packages/cli/test/unit/commands/wrapper/postinstall-wrapper.test.mts +++ b/packages/cli/test/unit/commands/wrapper/postinstall-wrapper.test.mts @@ -61,6 +61,21 @@ vi.mock('../../../../src/commands/install/setup-tab-completion.mts', () => ({ })) vi.mock('../../../../src/utils/error/errors.mts', () => ({ getErrorCause: vi.fn(e => e?.message || String(e)), + FileSystemError: class FileSystemError extends Error { + public readonly path?: string | undefined + public readonly code?: string | undefined + public readonly recovery: string[] = [] + constructor( + message: string, + path?: string | undefined, + code?: string | undefined, + ) { + super(message) + this.name = 'FileSystemError' + this.path = path + this.code = code + } + }, })) describe('postinstallWrapper', () => { @@ -196,7 +211,7 @@ describe('postinstallWrapper', () => { }) await expect(postinstallWrapper()).rejects.toThrow( - 'There was an issue setting up the alias: Permission denied', + /failed to add socket aliases to .* \(Permission denied\)/, ) }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index adc83df24..158a84812 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2151,6 +2151,7 @@ packages: '@socketaddon/iocraft@file:packages/package-builder/build/dev/out/socketaddon-iocraft': resolution: {directory: packages/package-builder/build/dev/out/socketaddon-iocraft, type: directory} + engines: {node: '>=18'} '@socketregistry/es-set-tostringtag@1.0.10': resolution: {integrity: sha512-btXmvw1JpA8WtSoXx9mTapo9NAyIDKRRzK84i48d8zc0X09M6ORfobVnHbgwhXf7CFhkRzhYrHG9dqbI9vpELQ==}