diff --git a/packages/cli/src/commands/scan/cmd-scan-create.mts b/packages/cli/src/commands/scan/cmd-scan-create.mts index c1de899a2..8b02c0556 100644 --- a/packages/cli/src/commands/scan/cmd-scan-create.mts +++ b/packages/cli/src/commands/scan/cmd-scan-create.mts @@ -202,10 +202,28 @@ const generalFlags: MeowFlags = { }, } -export const cmdScanCreate = { - description, - hidden, - run, +// Scan argv for `--default-branch=` (both kebab-case +// and yargs-parser's camelCase expansion). The flag is declared boolean, +// so meow coerces `--default-branch=main` to `true` and discards "main" +// — silently leaving the scan without a branch tag. +const DEFAULT_BRANCH_PREFIXES = ['--default-branch=', '--defaultBranch='] + +function findDefaultBranchValueMisuse( + argv: readonly string[], +): { prefix: string; value: string } | undefined { + for (const arg of argv) { + const prefix = DEFAULT_BRANCH_PREFIXES.find(p => arg.startsWith(p)) + if (!prefix) { + continue + } + const value = arg.slice(prefix.length) + const normalized = value.toLowerCase() + if (normalized === 'true' || normalized === 'false' || value === '') { + continue + } + return { prefix, value } + } + return undefined } async function run( @@ -272,6 +290,25 @@ async function run( `, } + // Detect the common `--default-branch=main` misuse before meow parses. + // `--default-branch` is a boolean — meow/yargs-parser treats + // `--default-branch=main` as `defaultBranch=true` and silently drops + // the "main" portion, so the user's scan gets tagged without the + // intended branch name and doesn't appear in the Main/PR dashboard + // tabs. Fail fast with a suggestion toward the correct form. + const defaultBranchMisuse = findDefaultBranchValueMisuse(argv) + if (defaultBranchMisuse) { + const { prefix, value } = defaultBranchMisuse + // Strip the trailing `=` from the matched prefix when naming the + // canonical flag in the suggestion — users should always be told + // to use the kebab-case form. + logger.fail( + `"${prefix}${value}" looks like you meant the branch name "${value}".\n--default-branch is a boolean flag; pass the branch name with --branch instead:\n socket scan create --branch ${value} --default-branch`, + ) + process.exitCode = 2 + return + } + const cli = meowOrExit({ argv, config, @@ -680,3 +717,9 @@ async function run( workspace: (workspace && String(workspace)) || '', }) } + +export const cmdScanCreate = { + description, + hidden, + run, +} 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 be57cd47c..28a0ae9fc 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 @@ -1366,5 +1366,100 @@ describe('cmd-scan-create', () => { expect(mockHandleCreateNewScan).not.toHaveBeenCalled() }) }) + + describe('--default-branch misuse detection', () => { + // --default-branch is a boolean flag; meow silently discards the + // `=` portion on `--default-branch=`. Catch that + // pattern before meow parses so users get a clear error pointing + // at the right shape (`--branch --default-branch`). + it('fails when --default-branch= is passed with a branch name', async () => { + await cmdScanCreate.run( + ['--org', 'test-org', '--default-branch=main', '.'], + importMeta, + context, + ) + + expect(process.exitCode).toBe(2) + expect(mockHandleCreateNewScan).not.toHaveBeenCalled() + expect(mockLogger.fail).toHaveBeenCalledWith( + expect.stringContaining( + '"--default-branch=main" looks like you meant the branch name "main"', + ), + ) + expect(mockLogger.fail).toHaveBeenCalledWith( + expect.stringContaining('--branch main --default-branch'), + ) + }) + + it('also catches the camelCase --defaultBranch= variant', async () => { + // yargs-parser expands camelCase, so users can type either + // form from the shell. See Cursor bugbot feedback on PR #1230. + await cmdScanCreate.run( + ['--org', 'test-org', '--defaultBranch=main', '.'], + importMeta, + context, + ) + + expect(process.exitCode).toBe(2) + expect(mockHandleCreateNewScan).not.toHaveBeenCalled() + expect(mockLogger.fail).toHaveBeenCalledWith( + expect.stringContaining('looks like you meant the branch name "main"'), + ) + // Error quotes the exact form the user typed so there's no + // confusion about whether the error applies to their input. + expect(mockLogger.fail).toHaveBeenCalledWith( + expect.stringContaining('"--defaultBranch=main"'), + ) + }) + + it.each([ + '--default-branch=true', + '--default-branch=false', + '--default-branch=TRUE', + ])('allows %s (explicit boolean form)', async arg => { + mockHasDefaultApiToken.mockReturnValueOnce(true) + + await cmdScanCreate.run( + [ + '--org', + 'test-org', + '--branch', + 'main', + arg, + '.', + '--no-interactive', + ], + importMeta, + context, + ) + + // meow parses the flag normally and flows through to handleCreateNewScan. + expect(mockLogger.fail).not.toHaveBeenCalledWith( + expect.stringContaining('looks like you meant the branch name'), + ) + }) + + it('allows bare --default-branch (default truthy form)', async () => { + mockHasDefaultApiToken.mockReturnValueOnce(true) + + await cmdScanCreate.run( + [ + '--org', + 'test-org', + '--branch', + 'main', + '--default-branch', + '.', + '--no-interactive', + ], + importMeta, + context, + ) + + expect(mockLogger.fail).not.toHaveBeenCalledWith( + expect.stringContaining('looks like you meant the branch name'), + ) + }) + }) }) })