diff --git a/CHANGELOG.md b/CHANGELOG.md index f9fd87eb..6e40545d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,30 @@ +# [11.2.0](https://github.com/salesforcecli/sf-plugins-core/compare/11.1.9...11.2.0) (2024-07-19) + + +### Features + +* parse error messages to suggest correct flag value usage ([05c25ba](https://github.com/salesforcecli/sf-plugins-core/commit/05c25ba9fe3d8eff16207fc36a94e6e5d996b9d1)) + + + +## [11.1.9](https://github.com/salesforcecli/sf-plugins-core/compare/11.1.8...11.1.9) (2024-07-14) + + +### Bug Fixes + +* **deps:** bump @oclif/core from 4.0.8 to 4.0.12 ([26004af](https://github.com/salesforcecli/sf-plugins-core/commit/26004af618653526b9bb3cf1f41b672965df076e)) + + + +## [11.1.8](https://github.com/salesforcecli/sf-plugins-core/compare/11.1.7...11.1.8) (2024-07-14) + + +### Bug Fixes + +* **deps:** bump ansis from 3.2.0 to 3.2.1 ([7240349](https://github.com/salesforcecli/sf-plugins-core/commit/724034987eb101cd66eb83074f574e44b1a96fd9)) + + + ## [11.1.7](https://github.com/salesforcecli/sf-plugins-core/compare/11.1.6...11.1.7) (2024-07-14) diff --git a/package.json b/package.json index 7a0d41f8..efb10523 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@salesforce/sf-plugins-core", - "version": "11.1.7", + "version": "11.2.0", "description": "Utils for writing Salesforce CLI plugins", "main": "lib/exported", "types": "lib/exported.d.ts", @@ -46,11 +46,11 @@ "dependencies": { "@inquirer/confirm": "^3.1.14", "@inquirer/password": "^2.1.14", - "@oclif/core": "^4.0.8", + "@oclif/core": "^4.0.12", "@salesforce/core": "^8.1.1", "@salesforce/kit": "^3.1.6", "@salesforce/ts-types": "^2.0.10", - "ansis": "^3.2.0", + "ansis": "^3.2.1", "cli-progress": "^3.12.0", "natural-orderby": "^3.0.2", "slice-ansi": "^7.1.0", 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 { diff --git a/yarn.lock b/yarn.lock index 668e8744..3e3f5e99 100644 --- a/yarn.lock +++ b/yarn.lock @@ -588,13 +588,13 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@oclif/core@^4.0.8": - version "4.0.8" - resolved "https://registry.yarnpkg.com/@oclif/core/-/core-4.0.8.tgz#667caa6481682e34594a4df6faeb3137879652d8" - integrity sha512-9AzNoRlKfIeuqOin+HK9cyouELeup7sX+MGIFc5dR+bnG0sSzFnV1A/Z57E7KWrY5NdWULHYT5NhiL1YpEhG2w== +"@oclif/core@^4.0.12": + version "4.0.12" + resolved "https://registry.yarnpkg.com/@oclif/core/-/core-4.0.12.tgz#b47089631827e2c909bfffc8c14d7b1781e502ce" + integrity sha512-o2dfPtmi2uBGHgpvHr+GNfoRKysKgQGiffONoMN3R+qBVNeIkJIZhVk31HreDkAI9LAzS92BWNgp/l7lXDxdvg== dependencies: ansi-escapes "^4.3.2" - ansis "^3.1.1" + ansis "^3.2.1" clean-stack "^3.0.1" cli-spinners "^2.9.2" debug "^4.3.5" @@ -1048,10 +1048,10 @@ ansi-styles@^6.1.0, ansi-styles@^6.2.1: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== -ansis@^3.1.1, ansis@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/ansis/-/ansis-3.2.0.tgz#0e050c5be94784f32ffdac4b84fccba064aeae4b" - integrity sha512-Yk3BkHH9U7oPyCN3gL5Tc7CpahG/+UFv/6UG03C311Vy9lzRmA5uoxDTpU9CO3rGHL6KzJz/pdDeXZCZ5Mu/Sg== +ansis@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansis/-/ansis-3.2.1.tgz#abf2de690eb7a74fa4292edf82c7887bda5ee549" + integrity sha512-SgzY+k2aa9UqJe3jzrPZhSVzLc2XrE4/h7rk0dMCDwhCq7ipmpPZvyODoxPCms4OpMLTiBTS+Mpl4VZQ6FDitw== anymatch@~3.1.2: version "3.1.3"