diff --git a/src/sfCommand.ts b/src/sfCommand.ts index 6257e336a..1f5c5b6bd 100644 --- a/src/sfCommand.ts +++ b/src/sfCommand.ts @@ -376,6 +376,30 @@ export abstract class SfCommand extends Command { const sfCommandError = SfCommandError.from(error, this.statics.name, this.warnings); process.exitCode = sfCommandError.exitCode; + // no var args (strict = true || undefined), and unexpected arguments when parsing + if ( + this.statics.strict !== false && + sfCommandError.exitCode === 2 && + error.message.includes('Unexpected argument') + ) { + // @ts-expect-error error's causes aren't typed, this is what's returned from flag parsing errors + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const output = + (sfCommandError.cause?.parse?.output?.raw as Array<{ flag: string; input: string; type: 'flag' | 'arg' }>) ?? + []; + + // find the extra arguments causing issues + const extras = output + .filter((f) => f.type === 'arg') + .flatMap((f) => f.input) + .join(' '); + // find the flag before the 'args' block that's valid, to append the args with its value as a suggestion + const target = output.find((flag, index) => flag.type === 'flag' && output[index + 1]?.type === 'arg'); + + sfCommandError.actions ??= []; + sfCommandError.actions.push(`--${target?.flag} "${target?.input} ${extras}"`); + } + if (this.jsonEnabled()) { this.logJson(sfCommandError.toJson()); } else { diff --git a/test/unit/sfCommand.test.ts b/test/unit/sfCommand.test.ts index 8369c00fc..19aa49e61 100644 --- a/test/unit/sfCommand.test.ts +++ b/test/unit/sfCommand.test.ts @@ -110,6 +110,23 @@ class NonJsonCommand extends SfCommand { } } +class SuggestionCommand extends SfCommand { + public static enableJsonFlag = false; + public static readonly flags = { + first: Flags.string({ + default: 'My first flag', + required: true, + }), + second: Flags.string({ + default: 'My second', + required: true, + }), + }; + public async run(): Promise { + await this.parse(SuggestionCommand); + } +} + describe('jsonEnabled', () => { afterEach(() => { delete process.env.SF_CONTENT_TYPE; @@ -375,6 +392,69 @@ describe('error standardization', () => { } }); + it('should log correct suggestion when user doesnt wrap with quotes', async () => { + const logToStderrStub = $$.SANDBOX.stub(SfCommand.prototype, 'logToStderr'); + try { + await SuggestionCommand.run(['--first', 'my', 'alias', 'with', 'spaces', '--second', 'my second value']); + expect(false, 'error should have been thrown').to.be.true; + } catch (e: unknown) { + expect(e).to.be.instanceOf(SfCommandError); + const err = e as SfCommand.Error; + + // Ensure the error was logged to the console + expect(logToStderrStub.callCount).to.equal(1); + expect(logToStderrStub.firstCall.firstArg).to.contain(err.message); + + // Ensure the error has expected properties + expect(err).to.have.property('actions'); + expect(err.actions).to.deep.equal(['--first "my alias with spaces"']); + expect(err).to.have.property('exitCode', 2); + expect(err).to.have.property('context', 'SuggestionCommand'); + expect(err).to.have.property('data', undefined); + expect(err).to.have.property('cause'); + expect(err).to.have.property('code', '2'); + expect(err).to.have.property('status', 2); + expect(err).to.have.property('stack').and.be.ok; + expect(err).to.have.property('skipOclifErrorHandling', true); + expect(err).to.have.deep.property('oclif', { exit: 2 }); + + // Ensure a sfCommandError event was emitted with the expected data + expect(sfCommandErrorData[0]).to.equal(err); + expect(sfCommandErrorData[1]).to.equal('suggestioncommand'); + } + }); + it('should log correct suggestion when user doesnt wrap with quotes without flag order', async () => { + const logToStderrStub = $$.SANDBOX.stub(SfCommand.prototype, 'logToStderr'); + try { + await SuggestionCommand.run(['--second', 'my second value', '--first', 'my', 'alias', 'with', 'spaces']); + expect(false, 'error should have been thrown').to.be.true; + } catch (e: unknown) { + expect(e).to.be.instanceOf(SfCommandError); + const err = e as SfCommand.Error; + + // Ensure the error was logged to the console + expect(logToStderrStub.callCount).to.equal(1); + expect(logToStderrStub.firstCall.firstArg).to.contain(err.message); + + // Ensure the error has expected properties + expect(err).to.have.property('actions'); + expect(err.actions).to.deep.equal(['--first "my alias with spaces"']); + expect(err).to.have.property('exitCode', 2); + expect(err).to.have.property('context', 'SuggestionCommand'); + expect(err).to.have.property('data', undefined); + expect(err).to.have.property('cause'); + expect(err).to.have.property('code', '2'); + expect(err).to.have.property('status', 2); + expect(err).to.have.property('stack').and.be.ok; + expect(err).to.have.property('skipOclifErrorHandling', true); + expect(err).to.have.deep.property('oclif', { exit: 2 }); + + // Ensure a sfCommandError event was emitted with the expected data + expect(sfCommandErrorData[0]).to.equal(err); + expect(sfCommandErrorData[1]).to.equal('suggestioncommand'); + } + }); + it('should log correct error when command throws an SfError --json', async () => { const logJsonStub = $$.SANDBOX.stub(SfCommand.prototype, 'logJson'); try {