diff --git a/src/SfCommandError.ts b/src/SfCommandError.ts index c3e995be..0f94e7fc 100644 --- a/src/SfCommandError.ts +++ b/src/SfCommandError.ts @@ -95,4 +95,35 @@ export class SfCommandError extends SfError { result: this.result, }; } + + public appendErrorSuggestions(): void { + const output = + // @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 + (this.cause?.parse?.output?.raw as Array<{ flag: string; input: string; type: 'flag' | 'arg' }>) ?? []; + + /* + if there's a group of args, and additional args separated, we could have multiple suggestions + --first my first --second my second => + try this: + --first "my first" + --second "my second" + */ + + const aggregator: Array<{ flag: string; args: string[] }> = []; + output.forEach((k, i) => { + let argCounter = i + 1; + if (k.type === 'flag' && output[argCounter]?.type === 'arg') { + const args: string[] = []; + while (output[argCounter]?.type === 'arg') { + args.push(output[argCounter].input); + argCounter++; + } + aggregator.push({ flag: k.flag, args: [k.input, ...args] }); + } + }); + + this.actions ??= []; + this.actions.push(...aggregator.map((cause) => `--${cause.flag} "${cause.args.join(' ')}"`)); + } } diff --git a/src/sfCommand.ts b/src/sfCommand.ts index 6257e336..72a6a09b 100644 --- a/src/sfCommand.ts +++ b/src/sfCommand.ts @@ -376,6 +376,15 @@ 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') + ) { + sfCommandError.appendErrorSuggestions(); + } + if (this.jsonEnabled()) { this.logJson(sfCommandError.toJson()); } else { diff --git a/test/unit/sfCommand.test.ts b/test/unit/sfCommand.test.ts index 8369c00f..944e9dca 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"', '--second "my second value"']); + 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 {