From af07c242b57b3265dc686e19e421715f4ac84684 Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Tue, 3 Mar 2026 09:11:06 -0700 Subject: [PATCH 1/8] fix: add --version flag to agent activate/deactivate --- src/commands/agent/activate.ts | 15 +++++++++------ src/commands/agent/deactivate.ts | 12 +++++++----- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/commands/agent/activate.ts b/src/commands/agent/activate.ts index 30d68706..d0b949d6 100644 --- a/src/commands/agent/activate.ts +++ b/src/commands/agent/activate.ts @@ -17,13 +17,16 @@ import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; import { Messages } from '@salesforce/core'; import { getAgentForActivation } from '../../agentActivation.js'; +export type AgentActivateResult = { success: boolean; version: number }; + Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-agent', 'agent.activate'); -export default class AgentActivate extends SfCommand { +export default class AgentActivate extends SfCommand { public static readonly summary = messages.getMessage('summary'); public static readonly description = messages.getMessage('description'); public static readonly examples = messages.getMessages('examples'); + public static readonly enableJsonFlag = true; public static readonly flags = { 'target-org': Flags.requiredOrg(), @@ -32,9 +35,10 @@ export default class AgentActivate extends SfCommand { summary: messages.getMessage('flags.api-name.summary'), char: 'n', }), + version: Flags.integer({ summary: messages.getMessage('flags.version.summary') }), }; - public async run(): Promise { + public async run(): Promise { const { flags } = await this.parse(AgentActivate); const apiNameFlag = flags['api-name']; @@ -43,11 +47,10 @@ export default class AgentActivate extends SfCommand { if (!apiNameFlag && this.jsonEnabled()) { throw messages.createError('error.missingRequiredFlags', ['api-name']); } - const agent = await getAgentForActivation({ targetOrg, status: 'Active', apiNameFlag }); - await agent.activate(); - const agentName = (await agent.getBotMetadata()).DeveloperName; + const result = await agent.activate(flags.version); - this.log(`Agent ${agentName} activated.`); + this.log(`Agent ${result.DeveloperName} activated.`); + return { success: true, version: result.VersionNumber }; } } diff --git a/src/commands/agent/deactivate.ts b/src/commands/agent/deactivate.ts index 733764ee..6d55b540 100644 --- a/src/commands/agent/deactivate.ts +++ b/src/commands/agent/deactivate.ts @@ -16,11 +16,12 @@ import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; import { Messages } from '@salesforce/core'; import { getAgentForActivation } from '../../agentActivation.js'; +import { AgentActivateResult } from './activate.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-agent', 'agent.deactivate'); -export default class AgentDeactivate extends SfCommand { +export default class AgentDeactivate extends SfCommand { public static readonly summary = messages.getMessage('summary'); public static readonly description = messages.getMessage('description'); public static readonly examples = messages.getMessages('examples'); @@ -32,9 +33,10 @@ export default class AgentDeactivate extends SfCommand { summary: messages.getMessage('flags.api-name.summary'), char: 'n', }), + version: Flags.integer({ summary: messages.getMessage('flags.version.summary') }), }; - public async run(): Promise { + public async run(): Promise { const { flags } = await this.parse(AgentDeactivate); const apiNameFlag = flags['api-name']; @@ -45,9 +47,9 @@ export default class AgentDeactivate extends SfCommand { } const agent = await getAgentForActivation({ targetOrg, status: 'Inactive', apiNameFlag }); - await agent.deactivate(); - const agentName = (await agent.getBotMetadata()).DeveloperName; + const result = await agent.deactivate(flags.version); - this.log(`Agent ${agentName} deactivated.`); + this.log(`Agent ${result.DeveloperName} deactivated.`); + return { success: true, version: result.VersionNumber }; } } From 4fe0098c24a9bce63ff10b37b740cd6a846936c5 Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Tue, 3 Mar 2026 10:10:05 -0700 Subject: [PATCH 2/8] fix: deactivate only one version anywyays --- messages/agent.activate.md | 4 ++ src/agentActivation.ts | 68 ++++++++++++++++++++++++++++++-- src/commands/agent/activate.ts | 7 ++-- src/commands/agent/deactivate.ts | 5 +-- test/nuts/agent.activate.nut.ts | 40 +++++++++++++++++++ 5 files changed, 115 insertions(+), 9 deletions(-) diff --git a/messages/agent.activate.md b/messages/agent.activate.md index d370cff6..be4a68f7 100644 --- a/messages/agent.activate.md +++ b/messages/agent.activate.md @@ -22,6 +22,10 @@ You must know the agent's API name to activate it; you can either be prompted fo API name of the agent to activate. +# flags.version.summary + +Version number of the agent to activate. + # error.missingRequiredFlags Missing required flags: %s. diff --git a/src/agentActivation.ts b/src/agentActivation.ts index 07d9c110..24c9ecb7 100644 --- a/src/agentActivation.ts +++ b/src/agentActivation.ts @@ -15,7 +15,7 @@ */ import { Messages, Org, SfError, SfProject } from '@salesforce/core'; -import { Agent, type BotMetadata, ProductionAgent } from '@salesforce/agents'; +import { Agent, type BotMetadata, type BotVersionMetadata, ProductionAgent } from '@salesforce/agents'; import { select } from '@inquirer/prompts'; type Choice = { @@ -28,6 +28,11 @@ type AgentValue = { DeveloperName: string; }; +type VersionChoice = { + version: number; + status: string; +}; + Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-agent', 'agent.activation'); @@ -48,8 +53,10 @@ export const getAgentChoices = (agents: BotMetadata[], status: 'Active' | 'Inact agents.map((agent) => { let disabled: string | boolean = false; - const lastBotVersion = agent.BotVersions.records[agent.BotVersions.records.length - 1]; - if (lastBotVersion.Status === status) { + // For deactivate (status='Inactive'), check if any version is Active (can be deactivated) + // For activate (status='Active'), check if any version is Inactive (can be activated) + const hasAvailableVersion = agent.BotVersions.records.some((version) => version.Status !== status); + if (!hasAvailableVersion) { disabled = `(Already ${status})`; } if (agentIsUnsupported(agent.DeveloperName)) { @@ -66,6 +73,22 @@ export const getAgentChoices = (agents: BotMetadata[], status: 'Active' | 'Inact }; }); +export const getVersionChoices = ( + versions: BotVersionMetadata[], + status: 'Active' | 'Inactive' +): Array> => + versions.map((version) => { + const isTargetStatus = version.Status === status; + return { + name: `Version ${version.VersionNumber}`, + value: { + version: version.VersionNumber, + status: version.Status, + }, + disabled: isTargetStatus ? `(Already ${status})` : false, + }; + }); + export const getAgentForActivation = async (config: { targetOrg: Org; status: 'Active' | 'Inactive'; @@ -110,3 +133,42 @@ export const getAgentForActivation = async (config: { project: SfProject.getInstance(), }); }; + +export const getVersionForActivation = async (config: { + agent: ProductionAgent; + status: 'Active' | 'Inactive'; + versionFlag?: number; +}): Promise => { + const { agent, status, versionFlag } = config; + + // If version flag is provided, return it + if (versionFlag !== undefined) { + return versionFlag; + } + + // Get bot metadata to access versions + const botMetadata = await agent.getBotMetadata(); + const versions = botMetadata.BotVersions.records; + + // If there's only one version, return it + if (versions.length === 1) { + return versions[0].VersionNumber; + } + + // Get version choices and filter out disabled ones + const choices = getVersionChoices(versions, status); + const availableChoices = choices.filter((choice) => !choice.disabled); + + // If there's only one available choice, return it automatically + if (availableChoices.length === 1) { + return availableChoices[0].value.version; + } + + // Prompt user to select a version + const versionChoice = await select({ + message: 'Select a version', + choices, + }); + + return versionChoice.version; +}; diff --git a/src/commands/agent/activate.ts b/src/commands/agent/activate.ts index d0b949d6..4054e267 100644 --- a/src/commands/agent/activate.ts +++ b/src/commands/agent/activate.ts @@ -15,7 +15,7 @@ */ import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; import { Messages } from '@salesforce/core'; -import { getAgentForActivation } from '../../agentActivation.js'; +import { getAgentForActivation, getVersionForActivation } from '../../agentActivation.js'; export type AgentActivateResult = { success: boolean; version: number }; @@ -48,9 +48,10 @@ export default class AgentActivate extends SfCommand { throw messages.createError('error.missingRequiredFlags', ['api-name']); } const agent = await getAgentForActivation({ targetOrg, status: 'Active', apiNameFlag }); - const result = await agent.activate(flags.version); + const version = await getVersionForActivation({ agent, status: 'Active', versionFlag: flags.version }); + const result = await agent.activate(version); - this.log(`Agent ${result.DeveloperName} activated.`); + this.log(`Agent v${result.VersionNumber} activated.`); return { success: true, version: result.VersionNumber }; } } diff --git a/src/commands/agent/deactivate.ts b/src/commands/agent/deactivate.ts index 6d55b540..b66dbb0c 100644 --- a/src/commands/agent/deactivate.ts +++ b/src/commands/agent/deactivate.ts @@ -33,7 +33,6 @@ export default class AgentDeactivate extends SfCommand { summary: messages.getMessage('flags.api-name.summary'), char: 'n', }), - version: Flags.integer({ summary: messages.getMessage('flags.version.summary') }), }; public async run(): Promise { @@ -47,9 +46,9 @@ export default class AgentDeactivate extends SfCommand { } const agent = await getAgentForActivation({ targetOrg, status: 'Inactive', apiNameFlag }); - const result = await agent.deactivate(flags.version); + const result = await agent.deactivate(); - this.log(`Agent ${result.DeveloperName} deactivated.`); + this.log(`Agent v${result.VersionNumber} deactivated.`); return { success: true, version: result.VersionNumber }; } } diff --git a/test/nuts/agent.activate.nut.ts b/test/nuts/agent.activate.nut.ts index 8ed91121..d817ba7e 100644 --- a/test/nuts/agent.activate.nut.ts +++ b/test/nuts/agent.activate.nut.ts @@ -85,6 +85,26 @@ describe('agent activate/deactivate NUTs', function () { expect(finalStatus).to.equal('Active'); }); + it('should activate the agent with version flag', async () => { + // Ensure agent is inactive first + const initialStatus = await getBotStatus(); + if (initialStatus === 'Active') { + execCmd(`agent deactivate --api-name ${botApiName} --target-org ${username} --json`, { + ensureExitCode: 0, + }); + await sleep(5000); + } + + // Activate with version 1 + execCmd(`agent activate --api-name ${botApiName} --target-org ${username} --version 1 --json`, { + ensureExitCode: 0, + }); + + // Verify the BotVersion status is now 'Active' + const finalStatus = await getBotStatus(); + expect(finalStatus).to.equal('Active'); + }); + it('should deactivate the agent', async () => { // Verify the BotVersion status has 'Active' initial state const initialStatus = await getBotStatus(); @@ -98,4 +118,24 @@ describe('agent activate/deactivate NUTs', function () { const finalStatus = await getBotStatus(); expect(finalStatus).to.equal('Inactive'); }); + + it('should deactivate the agent (version automatically detected)', async () => { + // Ensure agent is active first + const initialStatus = await getBotStatus(); + if (initialStatus === 'Inactive') { + execCmd(`agent activate --api-name ${botApiName} --target-org ${username} --version 1 --json`, { + ensureExitCode: 0, + }); + await sleep(5000); + } + + // Deactivate (version is automatically detected) + execCmd(`agent deactivate --api-name ${botApiName} --target-org ${username} --json`, { + ensureExitCode: 0, + }); + + // Verify the BotVersion status is now 'Inactive' + const finalStatus = await getBotStatus(); + expect(finalStatus).to.equal('Inactive'); + }); }); From 240c90e93a723fe621cec34a3feb0bba0ad2e863 Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Tue, 3 Mar 2026 10:18:03 -0700 Subject: [PATCH 3/8] chore: fix output --- src/commands/agent/activate.ts | 3 ++- src/commands/agent/deactivate.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/commands/agent/activate.ts b/src/commands/agent/activate.ts index 4054e267..cfd9b780 100644 --- a/src/commands/agent/activate.ts +++ b/src/commands/agent/activate.ts @@ -50,8 +50,9 @@ export default class AgentActivate extends SfCommand { const agent = await getAgentForActivation({ targetOrg, status: 'Active', apiNameFlag }); const version = await getVersionForActivation({ agent, status: 'Active', versionFlag: flags.version }); const result = await agent.activate(version); + const metadata = await agent.getBotMetadata(); - this.log(`Agent v${result.VersionNumber} activated.`); + this.log(`${metadata.DeveloperName} v${result.VersionNumber} activated.`); return { success: true, version: result.VersionNumber }; } } diff --git a/src/commands/agent/deactivate.ts b/src/commands/agent/deactivate.ts index b66dbb0c..9d230f6a 100644 --- a/src/commands/agent/deactivate.ts +++ b/src/commands/agent/deactivate.ts @@ -47,8 +47,9 @@ export default class AgentDeactivate extends SfCommand { const agent = await getAgentForActivation({ targetOrg, status: 'Inactive', apiNameFlag }); const result = await agent.deactivate(); + const metadata = await agent.getBotMetadata(); - this.log(`Agent v${result.VersionNumber} deactivated.`); + this.log(`${metadata.DeveloperName} v${result.VersionNumber} deactivated.`); return { success: true, version: result.VersionNumber }; } } From a6dd2a7109ffbecd46d618c77f70014c59fc4dc3 Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Tue, 3 Mar 2026 10:48:33 -0700 Subject: [PATCH 4/8] fix: usability, errors, tests --- command-snapshot.json | 2 +- schemas/agent-activate.json | 19 +++ schemas/agent-deactivate.json | 19 +++ src/agentActivation.ts | 25 +++- src/commands/agent/activate.ts | 10 +- test/agentActivation.test.ts | 228 ++++++++++++++++++++++++++++++++ test/nuts/agent.activate.nut.ts | 33 +++++ 7 files changed, 328 insertions(+), 8 deletions(-) create mode 100644 schemas/agent-activate.json create mode 100644 schemas/agent-deactivate.json create mode 100644 test/agentActivation.test.ts diff --git a/command-snapshot.json b/command-snapshot.json index 547b53b6..76eda4ce 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -4,7 +4,7 @@ "command": "agent:activate", "flagAliases": [], "flagChars": ["n", "o"], - "flags": ["api-name", "api-version", "flags-dir", "json", "target-org"], + "flags": ["api-name", "api-version", "flags-dir", "json", "target-org", "version"], "plugin": "@salesforce/plugin-agent" }, { diff --git a/schemas/agent-activate.json b/schemas/agent-activate.json new file mode 100644 index 00000000..5827721b --- /dev/null +++ b/schemas/agent-activate.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/AgentActivateResult", + "definitions": { + "AgentActivateResult": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "version": { + "type": "number" + } + }, + "required": ["success", "version"], + "additionalProperties": false + } + } +} diff --git a/schemas/agent-deactivate.json b/schemas/agent-deactivate.json new file mode 100644 index 00000000..5827721b --- /dev/null +++ b/schemas/agent-deactivate.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/AgentActivateResult", + "definitions": { + "AgentActivateResult": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "version": { + "type": "number" + } + }, + "required": ["success", "version"], + "additionalProperties": false + } + } +} diff --git a/src/agentActivation.ts b/src/agentActivation.ts index 24c9ecb7..c824e23e 100644 --- a/src/agentActivation.ts +++ b/src/agentActivation.ts @@ -138,12 +138,13 @@ export const getVersionForActivation = async (config: { agent: ProductionAgent; status: 'Active' | 'Inactive'; versionFlag?: number; -}): Promise => { - const { agent, status, versionFlag } = config; + jsonEnabled?: boolean; +}): Promise<{ version: number | undefined; warning?: string }> => { + const { agent, status, versionFlag, jsonEnabled } = config; // If version flag is provided, return it if (versionFlag !== undefined) { - return versionFlag; + return { version: versionFlag }; } // Get bot metadata to access versions @@ -152,7 +153,7 @@ export const getVersionForActivation = async (config: { // If there's only one version, return it if (versions.length === 1) { - return versions[0].VersionNumber; + return { version: versions[0].VersionNumber }; } // Get version choices and filter out disabled ones @@ -161,7 +162,19 @@ export const getVersionForActivation = async (config: { // If there's only one available choice, return it automatically if (availableChoices.length === 1) { - return availableChoices[0].value.version; + return { version: availableChoices[0].value.version }; + } + + // If JSON mode is enabled, automatically select the latest available version + if (jsonEnabled) { + // Find the latest (highest version number) available version + const latestVersion = availableChoices.reduce((latest, choice) => + choice.value.version > latest.value.version ? choice : latest + ); + return { + version: latestVersion.value.version, + warning: `No version specified, automatically selected latest available version: ${latestVersion.value.version}`, + }; } // Prompt user to select a version @@ -170,5 +183,5 @@ export const getVersionForActivation = async (config: { choices, }); - return versionChoice.version; + return { version: versionChoice.version }; }; diff --git a/src/commands/agent/activate.ts b/src/commands/agent/activate.ts index cfd9b780..3d1b7035 100644 --- a/src/commands/agent/activate.ts +++ b/src/commands/agent/activate.ts @@ -48,11 +48,19 @@ export default class AgentActivate extends SfCommand { throw messages.createError('error.missingRequiredFlags', ['api-name']); } const agent = await getAgentForActivation({ targetOrg, status: 'Active', apiNameFlag }); - const version = await getVersionForActivation({ agent, status: 'Active', versionFlag: flags.version }); + const { version, warning } = await getVersionForActivation({ + agent, + status: 'Active', + versionFlag: flags.version, + jsonEnabled: this.jsonEnabled(), + }); const result = await agent.activate(version); const metadata = await agent.getBotMetadata(); this.log(`${metadata.DeveloperName} v${result.VersionNumber} activated.`); + if (warning) { + this.warn(warning); + } return { success: true, version: result.VersionNumber }; } } diff --git a/test/agentActivation.test.ts b/test/agentActivation.test.ts new file mode 100644 index 00000000..05b470cf --- /dev/null +++ b/test/agentActivation.test.ts @@ -0,0 +1,228 @@ +/* + * Copyright 2026, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import { type BotMetadata, type BotVersionMetadata } from '@salesforce/agents'; +import { getAgentChoices, getVersionChoices } from '../src/agentActivation.js'; + +describe('agentActivation', () => { + describe('getVersionChoices', () => { + it('should mark versions with target status as disabled', () => { + const versions: BotVersionMetadata[] = [ + { + Id: 'v1', + Status: 'Active', + VersionNumber: 1, + DeveloperName: 'Test_v1', + } as BotVersionMetadata, + { + Id: 'v2', + Status: 'Inactive', + VersionNumber: 2, + DeveloperName: 'Test_v2', + } as BotVersionMetadata, + { + Id: 'v3', + Status: 'Inactive', + VersionNumber: 3, + DeveloperName: 'Test_v3', + } as BotVersionMetadata, + ]; + + const choices = getVersionChoices(versions, 'Inactive'); + + expect(choices).to.have.lengthOf(3); + expect(choices[0].disabled).to.equal(false); // Version 1 is Active, can be deactivated + expect(choices[1].disabled).to.equal('(Already Inactive)'); // Version 2 is already Inactive + expect(choices[2].disabled).to.equal('(Already Inactive)'); // Version 3 is already Inactive + }); + + it('should include version numbers in choices', () => { + const versions: BotVersionMetadata[] = [ + { + Id: 'v1', + Status: 'Active', + VersionNumber: 5, + DeveloperName: 'Test_v5', + } as BotVersionMetadata, + ]; + + const choices = getVersionChoices(versions, 'Active'); + + expect(choices[0].name).to.equal('Version 5'); + expect(choices[0].value.version).to.equal(5); + expect(choices[0].value.status).to.equal('Active'); + }); + + it('should mark active versions as available for deactivation', () => { + const versions: BotVersionMetadata[] = [ + { + Id: 'v1', + Status: 'Active', + VersionNumber: 1, + DeveloperName: 'Test_v1', + } as BotVersionMetadata, + { + Id: 'v2', + Status: 'Active', + VersionNumber: 2, + DeveloperName: 'Test_v2', + } as BotVersionMetadata, + ]; + + const choices = getVersionChoices(versions, 'Inactive'); + + expect(choices[0].disabled).to.equal(false); + expect(choices[1].disabled).to.equal(false); + }); + }); + + describe('getAgentChoices', () => { + it('should enable agent when ANY version is available for activation', () => { + const agents: BotMetadata[] = [ + { + Id: 'agent1', + DeveloperName: 'Test_Agent', + BotVersions: { + records: [ + { Status: 'Active', VersionNumber: 1 } as BotVersionMetadata, + { Status: 'Inactive', VersionNumber: 2 } as BotVersionMetadata, // Can be activated + { Status: 'Inactive', VersionNumber: 3 } as BotVersionMetadata, + ], + }, + } as BotMetadata, + ]; + + const choices = getAgentChoices(agents, 'Active'); + + expect(choices).to.have.lengthOf(1); + expect(choices[0].disabled).to.equal(false); // Has inactive versions that can be activated + expect(choices[0].value.DeveloperName).to.equal('Test_Agent'); + }); + + it('should enable agent when ANY version is available for deactivation', () => { + const agents: BotMetadata[] = [ + { + Id: 'agent1', + DeveloperName: 'Test_Agent', + BotVersions: { + records: [ + { Status: 'Inactive', VersionNumber: 1 } as BotVersionMetadata, + { Status: 'Active', VersionNumber: 2 } as BotVersionMetadata, // Can be deactivated + { Status: 'Inactive', VersionNumber: 3 } as BotVersionMetadata, + ], + }, + } as BotMetadata, + ]; + + const choices = getAgentChoices(agents, 'Inactive'); + + expect(choices).to.have.lengthOf(1); + expect(choices[0].disabled).to.equal(false); // Has active version that can be deactivated + }); + + it('should disable agent when ALL versions are already in target state', () => { + const agents: BotMetadata[] = [ + { + Id: 'agent1', + DeveloperName: 'Test_Agent', + BotVersions: { + records: [ + { Status: 'Active', VersionNumber: 1 } as BotVersionMetadata, + { Status: 'Active', VersionNumber: 2 } as BotVersionMetadata, + { Status: 'Active', VersionNumber: 3 } as BotVersionMetadata, + ], + }, + } as BotMetadata, + ]; + + const choices = getAgentChoices(agents, 'Active'); + + expect(choices).to.have.lengthOf(1); + expect(choices[0].disabled).to.equal('(Already Active)'); // All versions are already active + }); + + it('should disable agent when ALL versions are inactive for deactivation', () => { + const agents: BotMetadata[] = [ + { + Id: 'agent1', + DeveloperName: 'Test_Agent', + BotVersions: { + records: [ + { Status: 'Inactive', VersionNumber: 1 } as BotVersionMetadata, + { Status: 'Inactive', VersionNumber: 2 } as BotVersionMetadata, + ], + }, + } as BotMetadata, + ]; + + const choices = getAgentChoices(agents, 'Inactive'); + + expect(choices).to.have.lengthOf(1); + expect(choices[0].disabled).to.equal('(Already Inactive)'); // All versions are already inactive + }); + + it('should disable unsupported agents', () => { + const agents: BotMetadata[] = [ + { + Id: 'agent1', + DeveloperName: 'Copilot_for_Salesforce', + BotVersions: { + records: [{ Status: 'Inactive', VersionNumber: 1 } as BotVersionMetadata], + }, + } as BotMetadata, + ]; + + const choices = getAgentChoices(agents, 'Active'); + + expect(choices).to.have.lengthOf(1); + expect(choices[0].disabled).to.equal('(Not Supported)'); + }); + + it('should handle multiple agents with mixed states', () => { + const agents: BotMetadata[] = [ + { + Id: 'agent1', + DeveloperName: 'Agent_All_Active', + BotVersions: { + records: [ + { Status: 'Active', VersionNumber: 1 } as BotVersionMetadata, + { Status: 'Active', VersionNumber: 2 } as BotVersionMetadata, + ], + }, + } as BotMetadata, + { + Id: 'agent2', + DeveloperName: 'Agent_Mixed', + BotVersions: { + records: [ + { Status: 'Active', VersionNumber: 1 } as BotVersionMetadata, + { Status: 'Inactive', VersionNumber: 2 } as BotVersionMetadata, + ], + }, + } as BotMetadata, + ]; + + const choices = getAgentChoices(agents, 'Active'); + + expect(choices).to.have.lengthOf(2); + expect(choices[0].value.DeveloperName).to.equal('Agent_All_Active'); + expect(choices[0].disabled).to.equal('(Already Active)'); + expect(choices[1].value.DeveloperName).to.equal('Agent_Mixed'); + expect(choices[1].disabled).to.equal(false); + }); + }); +}); diff --git a/test/nuts/agent.activate.nut.ts b/test/nuts/agent.activate.nut.ts index d817ba7e..6d18a86d 100644 --- a/test/nuts/agent.activate.nut.ts +++ b/test/nuts/agent.activate.nut.ts @@ -105,6 +105,39 @@ describe('agent activate/deactivate NUTs', function () { expect(finalStatus).to.equal('Active'); }); + it('should auto-select latest version in JSON mode without version flag', async () => { + // Ensure agent is inactive first + const initialStatus = await getBotStatus(); + if (initialStatus === 'Active') { + execCmd(`agent deactivate --api-name ${botApiName} --target-org ${username} --json`, { + ensureExitCode: 0, + }); + await sleep(5000); + } + + // Activate with --json but no --version flag + const result = execCmd<{ version: number; success: boolean }>( + `agent activate --api-name ${botApiName} --target-org ${username} --json`, + { + ensureExitCode: 0, + } + ); + + // Parse the JSON result + const jsonResult = result.jsonOutput!.result; + expect(jsonResult?.success).to.equal(true); + expect(jsonResult?.version).to.be.a('number'); + + // Verify the warning was included in the output + expect(result.shellOutput.stderr).to.include( + 'No version specified, automatically selected latest available version' + ); + + // Verify the BotVersion status is now 'Active' + const finalStatus = await getBotStatus(); + expect(finalStatus).to.equal('Active'); + }); + it('should deactivate the agent', async () => { // Verify the BotVersion status has 'Active' initial state const initialStatus = await getBotStatus(); From 6b057c0a785a82570098c103fc48bd4b38c1e153 Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Tue, 3 Mar 2026 11:26:48 -0700 Subject: [PATCH 5/8] chore: filter deleted versions --- src/agentActivation.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/agentActivation.ts b/src/agentActivation.ts index c824e23e..f45d9d98 100644 --- a/src/agentActivation.ts +++ b/src/agentActivation.ts @@ -149,7 +149,8 @@ export const getVersionForActivation = async (config: { // Get bot metadata to access versions const botMetadata = await agent.getBotMetadata(); - const versions = botMetadata.BotVersions.records; + // Filter out deleted versions as a defensive measure + const versions = botMetadata.BotVersions.records.filter((v) => !v.IsDeleted); // If there's only one version, return it if (versions.length === 1) { From c165ee9f9bbf72c737ee9949bc93983d5e28e86c Mon Sep 17 00:00:00 2001 From: Juliet Shackell <63259011+jshackell-sfdc@users.noreply.github.com> Date: Tue, 3 Mar 2026 10:49:21 -0800 Subject: [PATCH 6/8] fix: edit --- messages/agent.activate.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/messages/agent.activate.md b/messages/agent.activate.md index be4a68f7..d03814e8 100644 --- a/messages/agent.activate.md +++ b/messages/agent.activate.md @@ -4,27 +4,29 @@ Activate an agent in an org. # description -Activating an agent makes it immediately available to your users. An agent must be active before you can preview it with the "agent preview" CLI command or VS Code. +Activating an agent makes it immediately available to your users. A published agent must be active before you can preview it with the "agent preview" CLI command or VS Code. Agents can have multiple versions; only one version can be active at a time. -You must know the agent's API name to activate it; you can either be prompted for it or you can specify it with the --api-name flag. Find the agent's API name in its Agent Details page of your org's Agentforce Studio UI in Setup. +If you run the command without the --api-name or --version flags, the command provides a list of agent API names and versions for you to choose from. Use the flags to specify the exact agent and version without being prompted. If you use the --json flag and not --version, then the latest agent version is automatically activated. + +The value of the --version flag is always a number, corresponding to the "vX" part of the "BotVersion" metadata in your project. For example, if you have a force-app/main/default/bots/My_Agent/v4.botVersion-meta.xml file in your project, then you activate this version with the "--version 4" flag. # examples -- Activate an agent in your default target org by being prompted: +- Activate an agent in your default target org by being prompted for both its API name and version: <%= config.bin %> <%= command.id %> -- Activate an agent with API name Resort_Manager in the org with alias "my-org": +- Activate version 2 of an agent with API name Resort_Manager in the org with alias "my-org": - <%= config.bin %> <%= command.id %> --api-name Resort_Manager --target-org my-org + <%= config.bin %> <%= command.id %> --api-name Resort_Manager --version 2 --target-org my-org # flags.api-name.summary -API name of the agent to activate. +API name of the agent to activate; if not specified, the command provides a list that you choose from. # flags.version.summary -Version number of the agent to activate. +Version number of the agent to activate; if not specified, the command provides a list that you choose from. # error.missingRequiredFlags From 8edf77fded4442d6e66a9d423c480779a2f0c893 Mon Sep 17 00:00:00 2001 From: Juliet Shackell <63259011+jshackell-sfdc@users.noreply.github.com> Date: Tue, 3 Mar 2026 10:52:29 -0800 Subject: [PATCH 7/8] Update agent.deactivate.md --- messages/agent.deactivate.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/messages/agent.deactivate.md b/messages/agent.deactivate.md index 7303405e..06f39755 100644 --- a/messages/agent.deactivate.md +++ b/messages/agent.deactivate.md @@ -6,7 +6,7 @@ Deactivate an agent in an org. Deactivating an agent makes it unavailable to your users. To make changes to an agent, such as adding or removing topics or actions, you must deactivate it. You can't preview an agent with the "agent preview" CLI command or VS Code if it's deactivated. -You must know the agent's API name to deactivate it; you can either be prompted for it or you can specify it with the --api-name flag. Find the agent's API name in its Agent Details page of your org's Agentforce Studio UI in Setup. +If you run the command without the --api-name flag, the command provides a list of agent API names for you to choose from. Use the flag to specify the exact agent without being prompted. # examples @@ -20,7 +20,7 @@ You must know the agent's API name to deactivate it; you can either be prompted # flags.api-name.summary -API name of the agent to deactivate. +API name of the agent to deactivate; if not specified, the command provides a list that you choose from. # error.missingRequiredFlags From 3d42f669e773aa2f8bee1080d6a8b7363ebe775a Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Mon, 9 Mar 2026 08:24:18 -0600 Subject: [PATCH 8/8] chore: enable json flag on deactivate --- src/commands/agent/deactivate.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/commands/agent/deactivate.ts b/src/commands/agent/deactivate.ts index 9d230f6a..c39019d8 100644 --- a/src/commands/agent/deactivate.ts +++ b/src/commands/agent/deactivate.ts @@ -25,6 +25,7 @@ export default class AgentDeactivate extends SfCommand { public static readonly summary = messages.getMessage('summary'); public static readonly description = messages.getMessage('description'); public static readonly examples = messages.getMessages('examples'); + public static enableJsonFlag = true; public static readonly flags = { 'target-org': Flags.requiredOrg(),