diff --git a/package.json b/package.json index 15010e3f4..0c1fe344f 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "author": "Salesforce", "bugs": "https://github.com/forcedotcom/cli/issues", "dependencies": { + "@inquirer/prompts": "^7.3.3", "@oclif/core": "^4.2.10", "@oclif/multi-stage-output": "^0.8.12", "@salesforce/apex-node": "^8.1.19", @@ -12,7 +13,7 @@ "@salesforce/kit": "^3.2.3", "@salesforce/plugin-info": "^3.4.47", "@salesforce/sf-plugins-core": "^12.2.1", - "@salesforce/source-deploy-retrieve": "12.16.9", + "@salesforce/source-deploy-retrieve": "^12.16.10", "@salesforce/source-tracking": "^7.3.19", "@salesforce/ts-types": "^2.0.12", "ansis": "^3.17.0", diff --git a/src/commands/project/delete/source.ts b/src/commands/project/delete/source.ts index de70dad79..1aa3dac46 100644 --- a/src/commands/project/delete/source.ts +++ b/src/commands/project/delete/source.ts @@ -9,6 +9,7 @@ import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; import { Interfaces } from '@oclif/core'; +import * as inquirerPrompts from '@inquirer/prompts'; import { Lifecycle, Messages, Org, SfError } from '@salesforce/core'; import { ComponentSet, @@ -136,6 +137,7 @@ export class Source extends SfCommand { private org!: Org; private componentSet!: ComponentSet; private deployResult!: DeployResult; + private inquirer = inquirerPrompts; public async run(): Promise { this.flags = (await this.parse(Source)).flags; @@ -189,6 +191,29 @@ export class Source extends SfCommand { fsPaths: await getPackageDirs(), include: this.componentSet, }); + // Confirm if the user wants to delete GenAiPlugins as part of an agent + if (!this.flags['no-prompt']) { + const genAiPlugins = this.componentSet.toArray().filter((comp) => comp.type.name === 'GenAiPlugin'); + if (genAiPlugins?.length) { + const funcsToDelete = await this.inquirer.checkbox({ + message: 'Select related topics to delete', + choices: genAiPlugins.map((plugin) => ({ name: plugin.fullName, value: plugin.fullName })), + }); + if (funcsToDelete?.length !== genAiPlugins?.length) { + // Create a new ComponentSet with selected GenAiPlugins and all non-GenAiPlugins + const compSetNoPlugins = new ComponentSet(); + for (const comp of this.componentSet) { + if ( + comp.type.name !== 'GenAiPlugin' || + (comp.type.name === 'GenAiPlugin' && funcsToDelete.includes(comp.fullName)) + ) { + compSetNoPlugins.add(comp); + } + } + this.componentSet = compSetNoPlugins; + } + } + } } if (this.flags['track-source'] && !this.flags['force-overwrite']) { @@ -441,7 +466,7 @@ Update the .forceignore file and try again.`); : messages.getMessage('areYouSure'), ]; - return this.confirm({ message: message.join('\n') }); + return this.confirm({ message: message.join('\n'), ms: 30_000 }); } return true; } diff --git a/test/commands/delete/source.test.ts b/test/commands/delete/source.test.ts index 8eb9667a2..3454b7c08 100644 --- a/test/commands/delete/source.test.ts +++ b/test/commands/delete/source.test.ts @@ -12,6 +12,7 @@ import { ComponentSet, ComponentSetBuilder, ComponentSetOptions, + RegistryAccess, SourceComponent, } from '@salesforce/source-deploy-retrieve'; import { Lifecycle, SfProject } from '@salesforce/core'; @@ -40,6 +41,30 @@ export const exampleSourceComponent: ComponentProperties = { content: '/dreamhouse-lwc/force-app/main/default/classes/GeocodingService.cls', }; +const registry = new RegistryAccess(); +const agentComponents: SourceComponent[] = [ + new SourceComponent({ + name: 'My_Agent', + type: registry.getTypeByName('Bot'), + xml: '/dreamhouse-lwc/force-app/main/default/bots/My_Agent.bot-meta.xml', + }), + new SourceComponent({ + name: 'Test_Planner', + type: registry.getTypeByName('GenAiPlanner'), + xml: '/dreamhouse-lwc/force-app/main/default/genAiPlanners/Test_Planner.genAiPlanner-meta.xml', + }), + new SourceComponent({ + name: 'Test_Plugin1', + type: registry.getTypeByName('GenAiPlugin'), + xml: '/dreamhouse-lwc/force-app/main/default/genAiPlugins/Test_Plugin1.genAiPlugin-meta.xml', + }), + new SourceComponent({ + name: 'Test_Plugin2', + type: registry.getTypeByName('GenAiPlugin'), + xml: '/dreamhouse-lwc/force-app/main/default/genAiPlugins/Test_Plugin2.genAiPlugin-meta.xml', + }), +]; + export const exampleDeleteResponse = { // required but ignored by the delete UT getFileResponses: (): void => {}, @@ -118,6 +143,7 @@ describe('project delete source', () => { let resolveProjectConfigStub: sinon.SinonStub; let rmStub: sinon.SinonStub; let compSetFromSourceStub: sinon.SinonStub; + let handlePromptStub: sinon.SinonStub; class TestDelete extends Source { public async runIt() { @@ -128,7 +154,13 @@ describe('project delete source', () => { } } - const runDeleteCmd = async (params: string[], options?: { sourceApiVersion?: string }) => { + const runDeleteCmd = async ( + params: string[], + options?: { + sourceApiVersion?: string; + inquirerMock?: { checkbox: sinon.SinonStub }; + } + ) => { const cmd = new TestDelete(params, oclifConfigStub); cmd.project = SfProject.getInstance(); $$.SANDBOX.stub(cmd.project, 'getDefaultPackage').returns({ name: '', path: '', fullPath: defaultPackagePath }); @@ -151,7 +183,14 @@ describe('project delete source', () => { onCancel: () => {}, onError: () => {}, }); - stubMethod($$.SANDBOX, cmd, 'handlePrompt').returns(confirm); + handlePromptStub = stubMethod($$.SANDBOX, cmd, 'handlePrompt').returns(confirm); + if (options?.inquirerMock) { + // @ts-expect-error stubbing private member of the command + cmd.inquirer = options.inquirerMock; + } else { + // @ts-expect-error stubbing private member of the command + cmd.inquirer = { checkbox: $$.SANDBOX.stub().resolves([]) }; + } rmStub = stubMethod($$.SANDBOX, fs.promises, 'rm').resolves(); stubMethod($$.SANDBOX, DeployCache, 'update').resolves(); @@ -233,9 +272,45 @@ describe('project delete source', () => { ensureHookArgs(); }); - it('should pass along metadata and org for pseudo-type matching', async () => { + it('should pass along metadata and org for pseudo-type matching with plugins', async () => { + const agentCompSet = new ComponentSet(); + const pluginNames = [agentComponents[2].name, agentComponents[3].name]; + agentComponents.map((comp) => agentCompSet.add(comp)); + compSetFromSourceStub = compSetFromSourceStub.returns(agentCompSet); + const inquirerCheckboxStub = $$.SANDBOX.stub().resolves(pluginNames); + const inquirerMock = { checkbox: inquirerCheckboxStub }; + const metadata = ['Agent:My_Agent']; + await runDeleteCmd(['--metadata', metadata[0], '--json'], { inquirerMock }); + ensureCreateComponentSetArgs({ + metadata: { + metadataEntries: metadata, + directoryPaths: [defaultPackagePath], + }, + org: { + username: testOrg.username, + exclude: [], + }, + }); + ensureHookArgs(); + expect(compSetFromSourceStub.calledOnce).to.be.true; + expect(inquirerCheckboxStub.calledOnce).to.be.true; + expect(inquirerCheckboxStub.firstCall.firstArg).has.property('message', 'Select related topics to delete'); + expect(inquirerCheckboxStub.firstCall.firstArg).has.deep.property('choices', [ + { name: 'Test_Plugin1', value: 'Test_Plugin1' }, + { name: 'Test_Plugin2', value: 'Test_Plugin2' }, + ]); + expect(handlePromptStub.calledOnce).to.be.true; + expect(lifecycleEmitStub.firstCall.args[1]).to.deep.equal(agentComponents); + }); + + it('should pass along metadata and org for pseudo-type matching without plugins', async () => { + const agentCompSet = new ComponentSet(); + agentComponents.map((comp) => agentCompSet.add(comp)); + compSetFromSourceStub = compSetFromSourceStub.returns(agentCompSet); + const inquirerCheckboxStub = $$.SANDBOX.stub().resolves([]); + const inquirerMock = { checkbox: inquirerCheckboxStub }; const metadata = ['Agent:My_Agent']; - await runDeleteCmd(['--metadata', metadata[0], '--json']); + await runDeleteCmd(['--metadata', metadata[0], '--json'], { inquirerMock }); ensureCreateComponentSetArgs({ metadata: { metadataEntries: metadata, @@ -248,6 +323,13 @@ describe('project delete source', () => { }); ensureHookArgs(); expect(compSetFromSourceStub.calledOnce).to.be.true; + expect(inquirerCheckboxStub.calledOnce).to.be.true; + expect(inquirerCheckboxStub.firstCall.firstArg).has.deep.property('choices', [ + { name: 'Test_Plugin1', value: 'Test_Plugin1' }, + { name: 'Test_Plugin2', value: 'Test_Plugin2' }, + ]); + expect(handlePromptStub.calledOnce).to.be.true; + expect(lifecycleEmitStub.firstCall.args[1]).to.deep.equal([agentComponents[0], agentComponents[1]]); }); it('should pass along apiversion', async () => { diff --git a/yarn.lock b/yarn.lock index a83fdcf4a..33bb6e678 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1294,7 +1294,7 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@oclif/core@^4", "@oclif/core@^4.0.27", "@oclif/core@^4.2.10", "@oclif/core@^4.2.4", "@oclif/core@^4.2.8", "@oclif/core@^4.2.9": +"@oclif/core@^4", "@oclif/core@^4.0.27", "@oclif/core@^4.2.10", "@oclif/core@^4.2.8", "@oclif/core@^4.2.9": version "4.2.10" resolved "https://registry.yarnpkg.com/@oclif/core/-/core-4.2.10.tgz#31dfb7481c79887c3e672e10c981fcc01fcbaeb3" integrity sha512-fAqcXgqkUm4v5FYy7qWP4w1HaOlVSVJveah+yVTo5Nm5kTiXhmD5mQQ7+knGeBaStyrtQy6WardoC2xSic9rlQ== @@ -1581,23 +1581,7 @@ string-width "^7.2.0" terminal-link "^3.0.0" -"@salesforce/sf-plugins-core@^12": - version "12.2.0" - resolved "https://registry.yarnpkg.com/@salesforce/sf-plugins-core/-/sf-plugins-core-12.2.0.tgz#c53f5342841cc490752b78f2707e84d8946dd740" - integrity sha512-aGNk74rMt8I+HTP7hRsX6kxiGTuun9ONrWkX7JvWDdtIoO9TsEbNVZENH8GFxHFalWPFCj31IMUQD/bGbxMFbg== - dependencies: - "@inquirer/confirm" "^3.1.22" - "@inquirer/password" "^2.2.0" - "@oclif/core" "^4.2.4" - "@oclif/table" "^0.4.6" - "@salesforce/core" "^8.5.1" - "@salesforce/kit" "^3.2.3" - "@salesforce/ts-types" "^2.0.12" - ansis "^3.3.2" - cli-progress "^3.12.0" - terminal-link "^3.0.0" - -"@salesforce/sf-plugins-core@^12.2.1": +"@salesforce/sf-plugins-core@^12", "@salesforce/sf-plugins-core@^12.2.1": version "12.2.1" resolved "https://registry.yarnpkg.com/@salesforce/sf-plugins-core/-/sf-plugins-core-12.2.1.tgz#1048a5d1245f07f0e864f0f76e8818fd21a84fe6" integrity sha512-b3eRSzGO0weBLL1clHaJNgNP1aKkD1Qy2DQEc0ieteEm+fh1FfPA0QpJ9rh/hdmkJRip2x2R2zz9tflJ0wflbg== @@ -1613,10 +1597,10 @@ cli-progress "^3.12.0" terminal-link "^3.0.0" -"@salesforce/source-deploy-retrieve@12.16.9", "@salesforce/source-deploy-retrieve@^12.16.6", "@salesforce/source-deploy-retrieve@^12.16.9": - version "12.16.9" - resolved "https://registry.yarnpkg.com/@salesforce/source-deploy-retrieve/-/source-deploy-retrieve-12.16.9.tgz#f86f7dd8de10efe533f1b5737b2a4047d21fdc5a" - integrity sha512-1Ms7ULjaSnzJ+KJu7jFmEaYN4krLXFUvZQfB1UuUoCvcnNrMuP5zj7TqyfnsVN8CiTlIn3Xap9vT2yYC3CE2uw== +"@salesforce/source-deploy-retrieve@^12.16.10", "@salesforce/source-deploy-retrieve@^12.16.6", "@salesforce/source-deploy-retrieve@^12.16.9": + version "12.16.10" + resolved "https://registry.yarnpkg.com/@salesforce/source-deploy-retrieve/-/source-deploy-retrieve-12.16.10.tgz#b38ee71f9e5691212beae6575078ec8459b59a00" + integrity sha512-O1K5I5ZmMTfCZ4SV+/w5iWMjGsHSU+PtHCx7gJVcgH8emY25OUhxrQPqb7BS2jupKQZy6U6hhVhkcD9yYqClag== dependencies: "@salesforce/core" "^8.8.5" "@salesforce/kit" "^3.2.3"