From 0d390f1cec8bbf1d9cfd796177871a9d7bf6dac4 Mon Sep 17 00:00:00 2001 From: Marcelino Llano Date: Tue, 10 Feb 2026 20:04:53 +0100 Subject: [PATCH 1/5] feat: rewrite authoring bundle creation as wizard-style flow Replace autocomplete spec selector with two-step type picker and file picker from specs/ directory. Remove interactive API name prompt in favor of auto-generation. Add MultiStageOutput progress reporting. --- messages/agent.generate.authoring-bundle.md | 64 +++++++- .../agent/generate/authoring-bundle.ts | 148 ++++++++++-------- 2 files changed, 149 insertions(+), 63 deletions(-) diff --git a/messages/agent.generate.authoring-bundle.md b/messages/agent.generate.authoring-bundle.md index c0da1de..80bbe41 100644 --- a/messages/agent.generate.authoring-bundle.md +++ b/messages/agent.generate.authoring-bundle.md @@ -16,7 +16,7 @@ This command requires an org because it uses it to access an LLM for generating # flags.spec.summary -Path to the agent spec YAML file. If you don't specify the flag, the command provides a list that you can choose from. Use the --no-spec flag to skip using an agent spec entirely. +Path to the agent spec YAML file. If you don't specify the flag, the command provides a list that you can choose from. Use the --no-spec flag to skip using an agent spec entirely. # flags.spec.prompt @@ -78,4 +78,64 @@ The specified file is not a valid agent spec YAML file. # error.failed-to-create-agent -Failed to create an authoring bundle from the agent spec YAML file. +Failed to generate authoring bundle: %s + +# wizard.specType.prompt + +Select the type of agent spec to use + +# wizard.specType.option.default.name + +Default Template (Recommended) + +# wizard.specType.option.default.description + +Start with a ready-to-use Agent Script + +# wizard.specType.option.fromSpec.name + +From Spec File (Advanced) + +# wizard.specType.option.fromSpec.description + +Generate Agent Script from an existing YAML spec + +# wizard.specFile.prompt + +Choose a spec to generate your Agent Script + +# wizard.name.prompt + +Enter a name for the new agent + +# wizard.name.validation.required + +Bundle name is required + +# wizard.name.validation.empty + +Bundle name can't be empty. + +# progress.title + +Generating authoring bundle: %s... + +# progress.stage.creating + +Creating authoring bundle structure... + +# progress.stage.generating + +Generating Agent Script file... + +# progress.stage.complete + +Complete! + +# success.message + +Authoring bundle "%s" was generated successfully. + +# warning.noSpecDir + +No agent spec directory found at %s diff --git a/src/commands/agent/generate/authoring-bundle.ts b/src/commands/agent/generate/authoring-bundle.ts index 88d0eb4..218724f 100644 --- a/src/commands/agent/generate/authoring-bundle.ts +++ b/src/commands/agent/generate/authoring-bundle.ts @@ -15,14 +15,14 @@ */ import { join, resolve } from 'node:path'; -import { readFileSync, existsSync } from 'node:fs'; +import { readFileSync, existsSync, readdirSync } from 'node:fs'; import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; import { generateApiName, Messages, SfError } from '@salesforce/core'; import { AgentJobSpec, ScriptAgent } from '@salesforce/agents'; +import { MultiStageOutput } from '@oclif/multi-stage-output'; import YAML from 'yaml'; -import { input as inquirerInput } from '@inquirer/prompts'; +import { select, input as inquirerInput } from '@inquirer/prompts'; import { theme } from '../../../inquirer-theme.js'; -import { FlaggablePrompt, promptForFlag, promptForSpecYaml } from '../../../flags.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-agent', 'agent.generate.authoring-bundle'); @@ -62,45 +62,6 @@ export default class AgentGenerateAuthoringBundle extends SfCommand - d.trim().length > 0 || 'Name cannot be empty or contain only whitespace', - required: true, - }, - 'api-name': { - message: messages.getMessage('flags.api-name.summary'), - promptMessage: messages.getMessage('flags.api-name.prompt'), - validate: (d: string): boolean | string => { - if (d.length === 0) { - return true; - } - if (d.length > 80) { - return 'API name cannot be over 80 characters.'; - } - const regex = /^[A-Za-z][A-Za-z0-9_]*[A-Za-z0-9]+$/; - if (!regex.test(d)) { - return 'Invalid API name.'; - } - return true; - }, - }, - spec: { - message: messages.getMessage('flags.spec.summary'), - promptMessage: messages.getMessage('flags.spec.prompt'), - validate: (d: string): boolean | string => { - const specPath = resolve(d); - if (!existsSync(specPath)) { - return 'Please enter an existing agent spec (yaml) file'; - } - return true; - }, - required: true, - }, - } satisfies Record; - public async run(): Promise { const { flags } = await this.parse(AgentGenerateAuthoringBundle); const { 'output-dir': outputDir } = flags; @@ -109,7 +70,7 @@ export default class AgentGenerateAuthoringBundle extends SfCommand undefined (default spec), --spec => path, missing => prompt + // Resolve spec: --no-spec => undefined, --spec => path, missing => wizard prompts let spec: string | undefined; if (flags['no-spec']) { spec = undefined; @@ -120,27 +81,84 @@ export default class AgentGenerateAuthoringBundle extends SfCommand (f.endsWith('.yaml') || f.endsWith('.yml')) && !f.includes('-testSpec') + ); + } else { + this.warn(messages.getMessage('warning.noSpecDir', [specsDir])); + } - // If we don't have a name yet, prompt for it - const name = flags['name'] ?? (await promptForFlag(AgentGenerateAuthoringBundle.FLAGGABLE_PROMPTS['name'])); - - // If we don't have an api name yet, prompt for it - let bundleApiName = flags['api-name']; - if (!bundleApiName) { - bundleApiName = generateApiName(name); - const promptedValue = await inquirerInput({ - message: messages.getMessage('flags.api-name.prompt'), - validate: AgentGenerateAuthoringBundle.FLAGGABLE_PROMPTS['api-name'].validate, - default: bundleApiName, + // Build spec type choices + const specTypeChoices: Array<{ name: string; value: 'default' | 'fromSpec'; description: string }> = [ + { + name: messages.getMessage('wizard.specType.option.default.name'), + value: 'default', + description: messages.getMessage('wizard.specType.option.default.description'), + }, + ]; + + if (specFiles.length > 0) { + specTypeChoices.push({ + name: messages.getMessage('wizard.specType.option.fromSpec.name'), + value: 'fromSpec', + description: messages.getMessage('wizard.specType.option.fromSpec.description'), + }); + } + + const specType = await select({ + message: messages.getMessage('wizard.specType.prompt'), + choices: specTypeChoices, theme, }); - if (promptedValue?.length) { - bundleApiName = promptedValue; + + if (specType === 'fromSpec') { + const selectedFile = await select({ + message: messages.getMessage('wizard.specFile.prompt'), + choices: specFiles.map((f) => ({ name: f, value: join(specsDir, f) })), + theme, + }); + spec = selectedFile; + } else { + spec = undefined; } } + // Resolve name: --name flag or prompt + const name = + flags['name'] ?? + (await inquirerInput({ + message: messages.getMessage('wizard.name.prompt'), + validate: (d: string): boolean | string => { + if (d.length === 0) { + return messages.getMessage('wizard.name.validation.required'); + } + if (d.trim().length === 0) { + return messages.getMessage('wizard.name.validation.empty'); + } + return true; + }, + theme, + })); + + // Resolve API name: --api-name flag or auto-generate from name + const bundleApiName = flags['api-name'] ?? generateApiName(name); + + const mso = new MultiStageOutput<{ apiName: string }>({ + stages: [ + messages.getMessage('progress.stage.creating'), + messages.getMessage('progress.stage.generating'), + messages.getMessage('progress.stage.complete'), + ], + title: messages.getMessage('progress.title', [bundleApiName]), + jsonEnabled: this.jsonEnabled(), + data: { apiName: bundleApiName }, + }); + try { // Get default output directory if not specified const defaultOutputDir = join(this.project!.getDefaultPackage().fullPath, 'main', 'default'); @@ -150,8 +168,12 @@ export default class AgentGenerateAuthoringBundle extends SfCommand Date: Tue, 10 Feb 2026 20:41:43 +0100 Subject: [PATCH 2/5] test: add unit tests for authoring bundle wizard flow Add 18 unit tests covering flag-based usage, interactive wizard prompts, name/API name validation, error handling, and spec file filtering. Also restore the interactive API name prompt and refine wizard prompt text. --- messages/agent.generate.authoring-bundle.md | 16 +- .../agent/generate/authoring-bundle.ts | 28 +- .../agent/generate/authoring-bundle.test.ts | 363 ++++++++++++++++++ 3 files changed, 397 insertions(+), 10 deletions(-) create mode 100644 test/commands/agent/generate/authoring-bundle.test.ts diff --git a/messages/agent.generate.authoring-bundle.md b/messages/agent.generate.authoring-bundle.md index 80bbe41..35c8093 100644 --- a/messages/agent.generate.authoring-bundle.md +++ b/messages/agent.generate.authoring-bundle.md @@ -44,7 +44,7 @@ API name of the new authoring bundle; if not specified, the API name is derived # flags.api-name.prompt -API name of the new authoring bundle +Enter authoring bundle API name # examples @@ -82,19 +82,19 @@ Failed to generate authoring bundle: %s # wizard.specType.prompt -Select the type of agent spec to use +Select authoring bundle template # wizard.specType.option.default.name -Default Template (Recommended) +Default template (recommended) # wizard.specType.option.default.description -Start with a ready-to-use Agent Script +Start with a ready-to-use Agent Script template # wizard.specType.option.fromSpec.name -From Spec File (Advanced) +From YAML spec (advanced) # wizard.specType.option.fromSpec.description @@ -102,11 +102,11 @@ Generate Agent Script from an existing YAML spec # wizard.specFile.prompt -Choose a spec to generate your Agent Script +Select authoring bundle YAML spec # wizard.name.prompt -Enter a name for the new agent +Enter authoring bundle name # wizard.name.validation.required @@ -118,7 +118,7 @@ Bundle name can't be empty. # progress.title -Generating authoring bundle: %s... +Generating authoring bundle: %s # progress.stage.creating diff --git a/src/commands/agent/generate/authoring-bundle.ts b/src/commands/agent/generate/authoring-bundle.ts index 218724f..7190dda 100644 --- a/src/commands/agent/generate/authoring-bundle.ts +++ b/src/commands/agent/generate/authoring-bundle.ts @@ -145,8 +145,32 @@ export default class AgentGenerateAuthoringBundle extends SfCommand { + if (d.length === 0) { + return true; + } + if (d.length > 80) { + return 'API name cannot be over 80 characters.'; + } + const regex = /^[A-Za-z][A-Za-z0-9_]*[A-Za-z0-9]+$/; + if (!regex.test(d)) { + return 'Invalid API name.'; + } + return true; + }, + default: bundleApiName, + theme, + }); + if (promptedValue?.length) { + bundleApiName = promptedValue; + } + } const mso = new MultiStageOutput<{ apiName: string }>({ stages: [ diff --git a/test/commands/agent/generate/authoring-bundle.test.ts b/test/commands/agent/generate/authoring-bundle.test.ts new file mode 100644 index 0000000..31069c1 --- /dev/null +++ b/test/commands/agent/generate/authoring-bundle.test.ts @@ -0,0 +1,363 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any, class-methods-use-this */ + +import { join } from 'node:path'; +import { expect } from 'chai'; +import sinon from 'sinon'; +import esmock from 'esmock'; +import { TestContext } from '@salesforce/core/testSetup'; +import { SfProject, generateApiName } from '@salesforce/core'; +import type { AgentJobSpec } from '@salesforce/agents'; + +const MOCK_PROJECT_DIR = join(process.cwd(), 'test', 'mock-projects', 'agent-generate-template'); + +type CreateAuthoringBundleArgs = { + bundleApiName: string; + agentSpec: AgentJobSpec & { name: string; developerName: string }; + project: SfProject; +}; + +type PromptConfig = { + message: string; + choices?: Array<{ name: string; value: string; description?: string }>; + validate?: (input: string) => boolean | string; + default?: string; +}; + +describe('agent generate authoring-bundle', () => { + const $$ = new TestContext(); + let selectStub: sinon.SinonStub; + let inputStub: sinon.SinonStub; + let createAuthoringBundleStub: sinon.SinonStub; + let AgentGenerateAuthoringBundle: any; + + beforeEach(async () => { + selectStub = $$.SANDBOX.stub(); + inputStub = $$.SANDBOX.stub(); + createAuthoringBundleStub = $$.SANDBOX.stub().resolves(); + + // Use esmock to replace ESM module imports + const mod = await esmock('../../../../src/commands/agent/generate/authoring-bundle.js', { + '@inquirer/prompts': { + select: selectStub, + input: inputStub, + }, + '@salesforce/agents': { + ScriptAgent: { + createAuthoringBundle: createAuthoringBundleStub, + }, + }, + '@oclif/multi-stage-output': { + MultiStageOutput: class MockMSO { + public goto(): void {} + public stop(): void {} + public error(): void {} + }, + }, + }); + + AgentGenerateAuthoringBundle = mod.default; + + // Tell TestContext we're in a project context + $$.inProject(true); + + const mockProject = { + getPath: () => MOCK_PROJECT_DIR, + getDefaultPackage: () => ({ + fullPath: join(MOCK_PROJECT_DIR, 'force-app'), + }), + } as unknown as SfProject; + + // Stub both resolve (used by framework) and getInstance (used by command code) + $$.SANDBOX.stub(SfProject, 'resolve').resolves(mockProject); + $$.SANDBOX.stub(SfProject, 'getInstance').returns(mockProject); + }); + + afterEach(() => { + $$.restore(); + }); + + describe('flag-based (non-interactive) usage', () => { + it('should generate with --no-spec and --name and --api-name', async () => { + const result = await AgentGenerateAuthoringBundle.run([ + '--no-spec', + '--name', + 'My Agent', + '--api-name', + 'MyAgent', + '--target-org', + 'test@org.com', + ]); + + expect(result.agentPath).to.include('MyAgent.agent'); + expect(result.metaXmlPath).to.include('MyAgent.bundle-meta.xml'); + expect(result.outputDir).to.include(join('aiAuthoringBundles', 'MyAgent')); + expect(createAuthoringBundleStub.calledOnce).to.be.true; + + const callArgs = createAuthoringBundleStub.firstCall.args[0] as CreateAuthoringBundleArgs; + expect(callArgs.bundleApiName).to.equal('MyAgent'); + expect(callArgs.agentSpec.name).to.equal('My Agent'); + expect(callArgs.agentSpec.developerName).to.equal('MyAgent'); + expect(callArgs.agentSpec.role).to.equal('My Agent description'); + }); + + it('should generate with --spec pointing to a file', async () => { + const specPath = join(MOCK_PROJECT_DIR, 'specs', 'agentSpec.yaml'); + + const result = await AgentGenerateAuthoringBundle.run([ + '--spec', + specPath, + '--name', + 'Spec Agent', + '--api-name', + 'SpecAgent', + '--target-org', + 'test@org.com', + ]); + + expect(result.agentPath).to.include('SpecAgent.agent'); + expect(createAuthoringBundleStub.calledOnce).to.be.true; + + const callArgs = createAuthoringBundleStub.firstCall.args[0] as CreateAuthoringBundleArgs; + expect(callArgs.agentSpec.role).to.equal('test agent role'); + expect(callArgs.agentSpec.companyName).to.equal('Test Company Name'); + }); + + it('should throw when --spec and --no-spec are both provided', async () => { + try { + await AgentGenerateAuthoringBundle.run([ + '--spec', + 'some/path.yaml', + '--no-spec', + '--name', + 'Agent', + '--api-name', + 'Agent', + '--target-org', + 'test@org.com', + ]); + expect.fail('Expected error'); + } catch (error) { + expect((error as Error).message).to.include("can't specify both"); + } + }); + + it('should throw when --spec points to nonexistent file', async () => { + try { + await AgentGenerateAuthoringBundle.run([ + '--spec', + '/nonexistent/path.yaml', + '--name', + 'Agent', + '--api-name', + 'Agent', + '--target-org', + 'test@org.com', + ]); + expect.fail('Expected error'); + } catch (error) { + expect((error as Error).message).to.include('No agent spec YAML file found'); + } + }); + + it('should auto-generate API name default from --name when --api-name is not provided', async () => { + inputStub.resolves('MyCustomApiName'); + + const result = await AgentGenerateAuthoringBundle.run([ + '--no-spec', + '--name', + 'My Custom Agent', + '--target-org', + 'test@org.com', + ]); + + expect(inputStub.calledOnce).to.be.true; + const inputCall = inputStub.firstCall.args[0] as PromptConfig; + expect(inputCall.default).to.equal(generateApiName('My Custom Agent')); + expect(result.outputDir).to.include('MyCustomApiName'); + }); + }); + + describe('wizard (interactive) usage', () => { + it('should prompt for spec type, name, and api name when no flags provided', async () => { + selectStub.resolves('default'); + inputStub.onFirstCall().resolves('Interactive Agent'); + inputStub.onSecondCall().resolves('InteractiveAgent'); + + const result = await AgentGenerateAuthoringBundle.run(['--target-org', 'test@org.com']); + + expect(selectStub.calledOnce).to.be.true; + expect(inputStub.calledTwice).to.be.true; + expect(result.agentPath).to.include('InteractiveAgent.agent'); + + const selectCall = selectStub.firstCall.args[0] as PromptConfig; + expect(selectCall.message).to.equal('Select authoring bundle template'); + + const nameInputCall = inputStub.firstCall.args[0] as PromptConfig; + expect(nameInputCall.message).to.equal('Enter authoring bundle name'); + }); + + it('should show spec file selection when "fromSpec" is chosen', async () => { + selectStub.onFirstCall().resolves('fromSpec'); + selectStub.onSecondCall().resolves(join(MOCK_PROJECT_DIR, 'specs', 'agentSpec.yaml')); + inputStub.onFirstCall().resolves('From Spec Agent'); + inputStub.onSecondCall().resolves('FromSpecAgent'); + + const result = await AgentGenerateAuthoringBundle.run(['--target-org', 'test@org.com']); + + expect(selectStub.calledTwice).to.be.true; + expect(result.agentPath).to.include('FromSpecAgent.agent'); + + const specFileCall = selectStub.secondCall.args[0] as PromptConfig; + expect(specFileCall.message).to.equal('Select authoring bundle YAML spec'); + + const callArgs = createAuthoringBundleStub.firstCall.args[0] as CreateAuthoringBundleArgs; + expect(callArgs.agentSpec.role).to.equal('test agent role'); + }); + + it('should only show default template when no spec files exist', async () => { + // Override project path to a dir without specs/ + const noSpecsProject = { + getPath: () => '/tmp/no-specs-here', + getDefaultPackage: () => ({ + fullPath: join(MOCK_PROJECT_DIR, 'force-app'), + }), + } as unknown as SfProject; + (SfProject.resolve as sinon.SinonStub).resolves(noSpecsProject); + (SfProject.getInstance as sinon.SinonStub).returns(noSpecsProject); + + selectStub.resolves('default'); + inputStub.onFirstCall().resolves('No Spec Agent'); + inputStub.onSecondCall().resolves('NoSpecAgent'); + + await AgentGenerateAuthoringBundle.run(['--target-org', 'test@org.com']); + + const selectCall = selectStub.firstCall.args[0] as PromptConfig; + expect(selectCall.choices).to.have.length(1); + expect(selectCall.choices![0].value).to.equal('default'); + }); + + it('should show both template options when spec files exist', async () => { + selectStub.resolves('default'); + inputStub.onFirstCall().resolves('Agent'); + inputStub.onSecondCall().resolves('Agent'); + + await AgentGenerateAuthoringBundle.run(['--target-org', 'test@org.com']); + + const selectCall = selectStub.firstCall.args[0] as PromptConfig; + expect(selectCall.choices).to.have.length(2); + expect(selectCall.choices![0].value).to.equal('default'); + expect(selectCall.choices![0].name).to.equal('Default template (recommended)'); + expect(selectCall.choices![1].value).to.equal('fromSpec'); + expect(selectCall.choices![1].name).to.equal('From YAML spec (advanced)'); + }); + }); + + describe('name validation', () => { + let validate: (input: string) => boolean | string; + + beforeEach(async () => { + selectStub.resolves('default'); + inputStub.onFirstCall().resolves('Valid Name'); + inputStub.onSecondCall().resolves('ValidName'); + await AgentGenerateAuthoringBundle.run(['--target-org', 'test@org.com']); + validate = (inputStub.firstCall.args[0] as PromptConfig).validate!; + }); + + it('should reject empty name', () => { + expect(validate('')).to.equal('Bundle name is required'); + }); + + it('should reject whitespace-only name', () => { + expect(validate(' ')).to.equal("Bundle name can't be empty."); + }); + + it('should accept valid name', () => { + expect(validate('My Agent')).to.be.true; + }); + }); + + describe('API name validation', () => { + let validate: (input: string) => boolean | string; + + beforeEach(async () => { + selectStub.resolves('default'); + inputStub.onFirstCall().resolves('Agent'); + inputStub.onSecondCall().resolves('Agent'); + await AgentGenerateAuthoringBundle.run(['--target-org', 'test@org.com']); + validate = (inputStub.secondCall.args[0] as PromptConfig).validate!; + }); + + it('should reject API names over 80 characters', () => { + expect(validate('A'.repeat(81))).to.equal('API name cannot be over 80 characters.'); + }); + + it('should reject invalid API name characters', () => { + expect(validate('invalid-name!')).to.equal('Invalid API name.'); + }); + + it('should accept valid API names', () => { + expect(validate('MyAgent01')).to.be.true; + expect(validate('My_Agent_Name')).to.be.true; + }); + + it('should accept empty API name (uses default)', () => { + expect(validate('')).to.be.true; + }); + }); + + describe('error handling', () => { + it('should wrap errors from ScriptAgent.createAuthoringBundle', async () => { + createAuthoringBundleStub.rejects(new Error('Generation failed')); + + try { + await AgentGenerateAuthoringBundle.run([ + '--no-spec', + '--name', + 'Agent', + '--api-name', + 'Agent', + '--target-org', + 'test@org.com', + ]); + expect.fail('Expected error'); + } catch (error) { + expect((error as Error).message).to.include('Failed to generate authoring bundle'); + expect((error as Error).message).to.include('Generation failed'); + expect((error as Error).name).to.equal('AgentGenerationError'); + } + }); + }); + + describe('spec file filtering', () => { + it('should filter out test spec files from the list', async () => { + selectStub.onFirstCall().resolves('fromSpec'); + selectStub.onSecondCall().resolves(join(MOCK_PROJECT_DIR, 'specs', 'agentSpec.yaml')); + inputStub.onFirstCall().resolves('Agent'); + inputStub.onSecondCall().resolves('Agent'); + + await AgentGenerateAuthoringBundle.run(['--target-org', 'test@org.com']); + + const specFileCall = selectStub.secondCall.args[0] as PromptConfig; + const choiceNames = specFileCall.choices!.map((c) => c.name); + for (const name of choiceNames) { + expect(name).to.not.include('-testSpec'); + } + }); + }); +}); From c07243d4d8c78b265dd8118f3342dc9995688a70 Mon Sep 17 00:00:00 2001 From: Marcelino Llano Date: Tue, 10 Feb 2026 21:46:50 +0100 Subject: [PATCH 3/5] feat: apply PR copy suggestions from code review --- messages/agent.generate.authoring-bundle.md | 22 +++++++++---------- .../agent/generate/authoring-bundle.test.ts | 14 ++++++------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/messages/agent.generate.authoring-bundle.md b/messages/agent.generate.authoring-bundle.md index 35c8093..ee802f0 100644 --- a/messages/agent.generate.authoring-bundle.md +++ b/messages/agent.generate.authoring-bundle.md @@ -78,43 +78,43 @@ The specified file is not a valid agent spec YAML file. # error.failed-to-create-agent -Failed to generate authoring bundle: %s +Failed to generate authoring bundle: %s. # wizard.specType.prompt -Select authoring bundle template +Select an authoring bundle template # wizard.specType.option.default.name -Default template (recommended) +Default template (Recommended) # wizard.specType.option.default.description -Start with a ready-to-use Agent Script template +Start with a ready-to-use Agent Script template. # wizard.specType.option.fromSpec.name -From YAML spec (advanced) +From an agent spec YAML file (Advanced) # wizard.specType.option.fromSpec.description -Generate Agent Script from an existing YAML spec +Generate an Agent Script file from an existing agent spec YAML file. # wizard.specFile.prompt -Select authoring bundle YAML spec +Select the agent spec YAML file. # wizard.name.prompt -Enter authoring bundle name +Enter the authoring bundle name. # wizard.name.validation.required -Bundle name is required +Authoring bundle name is required. # wizard.name.validation.empty -Bundle name can't be empty. +Authroring bundle name can't be empty. # progress.title @@ -138,4 +138,4 @@ Authoring bundle "%s" was generated successfully. # warning.noSpecDir -No agent spec directory found at %s +No agent spec directory found at %s. diff --git a/test/commands/agent/generate/authoring-bundle.test.ts b/test/commands/agent/generate/authoring-bundle.test.ts index 31069c1..8e58c14 100644 --- a/test/commands/agent/generate/authoring-bundle.test.ts +++ b/test/commands/agent/generate/authoring-bundle.test.ts @@ -206,10 +206,10 @@ describe('agent generate authoring-bundle', () => { expect(result.agentPath).to.include('InteractiveAgent.agent'); const selectCall = selectStub.firstCall.args[0] as PromptConfig; - expect(selectCall.message).to.equal('Select authoring bundle template'); + expect(selectCall.message).to.equal('Select an authoring bundle template'); const nameInputCall = inputStub.firstCall.args[0] as PromptConfig; - expect(nameInputCall.message).to.equal('Enter authoring bundle name'); + expect(nameInputCall.message).to.equal('Enter the authoring bundle name.'); }); it('should show spec file selection when "fromSpec" is chosen', async () => { @@ -224,7 +224,7 @@ describe('agent generate authoring-bundle', () => { expect(result.agentPath).to.include('FromSpecAgent.agent'); const specFileCall = selectStub.secondCall.args[0] as PromptConfig; - expect(specFileCall.message).to.equal('Select authoring bundle YAML spec'); + expect(specFileCall.message).to.equal('Select the agent spec YAML file.'); const callArgs = createAuthoringBundleStub.firstCall.args[0] as CreateAuthoringBundleArgs; expect(callArgs.agentSpec.role).to.equal('test agent role'); @@ -262,9 +262,9 @@ describe('agent generate authoring-bundle', () => { const selectCall = selectStub.firstCall.args[0] as PromptConfig; expect(selectCall.choices).to.have.length(2); expect(selectCall.choices![0].value).to.equal('default'); - expect(selectCall.choices![0].name).to.equal('Default template (recommended)'); + expect(selectCall.choices![0].name).to.equal('Default template (Recommended)'); expect(selectCall.choices![1].value).to.equal('fromSpec'); - expect(selectCall.choices![1].name).to.equal('From YAML spec (advanced)'); + expect(selectCall.choices![1].name).to.equal('From an agent spec YAML file (Advanced)'); }); }); @@ -280,11 +280,11 @@ describe('agent generate authoring-bundle', () => { }); it('should reject empty name', () => { - expect(validate('')).to.equal('Bundle name is required'); + expect(validate('')).to.equal('Authoring bundle name is required.'); }); it('should reject whitespace-only name', () => { - expect(validate(' ')).to.equal("Bundle name can't be empty."); + expect(validate(' ')).to.equal("Authroring bundle name can't be empty."); }); it('should accept valid name', () => { From 9789067b564fc3d7f79b829dc8e75dae21269c46 Mon Sep 17 00:00:00 2001 From: Marcelino Llano Date: Tue, 10 Feb 2026 21:50:11 +0100 Subject: [PATCH 4/5] refactor: replace multi-stage output with simple spinner for bundle generation --- messages/agent.generate.authoring-bundle.md | 16 ++------------ .../agent/generate/authoring-bundle.ts | 21 +++---------------- .../agent/generate/authoring-bundle.test.ts | 11 ++-------- 3 files changed, 7 insertions(+), 41 deletions(-) diff --git a/messages/agent.generate.authoring-bundle.md b/messages/agent.generate.authoring-bundle.md index ee802f0..90f6317 100644 --- a/messages/agent.generate.authoring-bundle.md +++ b/messages/agent.generate.authoring-bundle.md @@ -102,11 +102,11 @@ Generate an Agent Script file from an existing agent spec YAML file. # wizard.specFile.prompt -Select the agent spec YAML file. +Select the agent spec YAML file # wizard.name.prompt -Enter the authoring bundle name. +Enter the authoring bundle name # wizard.name.validation.required @@ -120,18 +120,6 @@ Authroring bundle name can't be empty. Generating authoring bundle: %s -# progress.stage.creating - -Creating authoring bundle structure... - -# progress.stage.generating - -Generating Agent Script file... - -# progress.stage.complete - -Complete! - # success.message Authoring bundle "%s" was generated successfully. diff --git a/src/commands/agent/generate/authoring-bundle.ts b/src/commands/agent/generate/authoring-bundle.ts index 7190dda..8d2ef1f 100644 --- a/src/commands/agent/generate/authoring-bundle.ts +++ b/src/commands/agent/generate/authoring-bundle.ts @@ -19,7 +19,6 @@ import { readFileSync, existsSync, readdirSync } from 'node:fs'; import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; import { generateApiName, Messages, SfError } from '@salesforce/core'; import { AgentJobSpec, ScriptAgent } from '@salesforce/agents'; -import { MultiStageOutput } from '@oclif/multi-stage-output'; import YAML from 'yaml'; import { select, input as inquirerInput } from '@inquirer/prompts'; import { theme } from '../../../inquirer-theme.js'; @@ -172,17 +171,6 @@ export default class AgentGenerateAuthoringBundle extends SfCommand({ - stages: [ - messages.getMessage('progress.stage.creating'), - messages.getMessage('progress.stage.generating'), - messages.getMessage('progress.stage.complete'), - ], - title: messages.getMessage('progress.title', [bundleApiName]), - jsonEnabled: this.jsonEnabled(), - data: { apiName: bundleApiName }, - }); - try { // Get default output directory if not specified const defaultOutputDir = join(this.project!.getDefaultPackage().fullPath, 'main', 'default'); @@ -192,12 +180,10 @@ export default class AgentGenerateAuthoringBundle extends SfCommand { createAuthoringBundle: createAuthoringBundleStub, }, }, - '@oclif/multi-stage-output': { - MultiStageOutput: class MockMSO { - public goto(): void {} - public stop(): void {} - public error(): void {} - }, - }, }); AgentGenerateAuthoringBundle = mod.default; @@ -209,7 +202,7 @@ describe('agent generate authoring-bundle', () => { expect(selectCall.message).to.equal('Select an authoring bundle template'); const nameInputCall = inputStub.firstCall.args[0] as PromptConfig; - expect(nameInputCall.message).to.equal('Enter the authoring bundle name.'); + expect(nameInputCall.message).to.equal('Enter the authoring bundle name'); }); it('should show spec file selection when "fromSpec" is chosen', async () => { @@ -224,7 +217,7 @@ describe('agent generate authoring-bundle', () => { expect(result.agentPath).to.include('FromSpecAgent.agent'); const specFileCall = selectStub.secondCall.args[0] as PromptConfig; - expect(specFileCall.message).to.equal('Select the agent spec YAML file.'); + expect(specFileCall.message).to.equal('Select the agent spec YAML file'); const callArgs = createAuthoringBundleStub.firstCall.args[0] as CreateAuthoringBundleArgs; expect(callArgs.agentSpec.role).to.equal('test agent role'); From 7601628153baf17d2fff87eecd16c5b30a4a38e8 Mon Sep 17 00:00:00 2001 From: Marcelino Llano Date: Tue, 10 Feb 2026 21:53:39 +0100 Subject: [PATCH 5/5] fix: correct typo in validation message (Authroring -> Authoring) --- messages/agent.generate.authoring-bundle.md | 2 +- test/commands/agent/generate/authoring-bundle.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/messages/agent.generate.authoring-bundle.md b/messages/agent.generate.authoring-bundle.md index 90f6317..55e89f6 100644 --- a/messages/agent.generate.authoring-bundle.md +++ b/messages/agent.generate.authoring-bundle.md @@ -114,7 +114,7 @@ Authoring bundle name is required. # wizard.name.validation.empty -Authroring bundle name can't be empty. +Authoring bundle name can't be empty. # progress.title diff --git a/test/commands/agent/generate/authoring-bundle.test.ts b/test/commands/agent/generate/authoring-bundle.test.ts index e451d6d..7ad27c4 100644 --- a/test/commands/agent/generate/authoring-bundle.test.ts +++ b/test/commands/agent/generate/authoring-bundle.test.ts @@ -277,7 +277,7 @@ describe('agent generate authoring-bundle', () => { }); it('should reject whitespace-only name', () => { - expect(validate(' ')).to.equal("Authroring bundle name can't be empty."); + expect(validate(' ')).to.equal("Authoring bundle name can't be empty."); }); it('should accept valid name', () => {