44 * Licensed under the BSD 3-Clause license.
55 * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66 */
7- import { join } from 'node:path' ;
8- import { mkdirSync , writeFileSync } from 'node:fs' ;
7+ import { join , resolve , dirname } from 'node:path' ;
8+ import { mkdirSync , readFileSync , writeFileSync } from 'node:fs' ;
99import { SfCommand , Flags } from '@salesforce/sf-plugins-core' ;
1010import { Messages , SfProject } from '@salesforce/core' ;
1111import { Interfaces } from '@oclif/core' ;
12- import ansis from 'ansis' ;
1312import YAML from 'yaml' ;
1413import select from '@inquirer/select' ;
1514import inquirerInput from '@inquirer/input' ;
16- import figures from '@inquirer/figures' ;
1715import { Agent , AgentJobSpecCreateConfigV2 , AgentJobSpecV2 } from '@salesforce/agents' ;
1816import { theme } from '../../../inquirer-theme.js' ;
1917
@@ -113,15 +111,13 @@ export default class AgentCreateSpecV2 extends SfCommand<AgentCreateSpecResult>
113111 'target-org' : Flags . requiredOrg ( ) ,
114112 'api-version' : Flags . orgApiVersion ( ) ,
115113 ...makeFlags ( FLAGGABLE_PROMPTS ) ,
116- 'output-dir' : Flags . directory ( {
117- char : 'd' ,
118- summary : messages . getMessage ( 'flags.output-dir.summary' ) ,
119- default : 'config' ,
114+ spec : Flags . file ( {
115+ summary : messages . getMessage ( 'flags.spec.summary' ) ,
116+ exists : true ,
120117 } ) ,
121- 'file-name' : Flags . string ( {
122- char : 'f' ,
123- summary : messages . getMessage ( 'flags.file-name.summary' ) ,
124- default : 'agentSpec.yaml' ,
118+ 'output-file' : Flags . file ( {
119+ summary : messages . getMessage ( 'flags.output-file.summary' ) ,
120+ default : join ( 'config' , 'agentSpec.yaml' ) ,
125121 } ) ,
126122 'max-topics' : Flags . integer ( {
127123 summary : messages . getMessage ( 'flags.max-topics.summary' ) ,
@@ -136,6 +132,7 @@ export default class AgentCreateSpecV2 extends SfCommand<AgentCreateSpecResult>
136132 } ) ,
137133 } ;
138134
135+ // eslint-disable-next-line complexity
139136 public async run ( ) : Promise < AgentCreateSpecResult > {
140137 const { flags } = await this . parse ( AgentCreateSpecV2 ) ;
141138
@@ -146,88 +143,117 @@ export default class AgentCreateSpecV2 extends SfCommand<AgentCreateSpecResult>
146143 . map ( ( [ key ] ) => key ) ;
147144
148145 if ( missingFlags . length ) {
149- throw new Error ( `Missing required flags: ${ missingFlags . join ( ', ' ) } ` ) ;
146+ throw messages . createError ( 'error.missingRequiredFlags' , [ missingFlags . join ( ', ' ) ] ) ;
150147 }
151148 }
152149
153150 this . log ( ) ;
154151 this . styledHeader ( 'Agent Details' ) ;
155- const type = ( await this . getFlagOrPrompt ( flags . type , FLAGGABLE_PROMPTS . type ) ) as 'customer' | 'internal' ;
156- const role = await this . getFlagOrPrompt ( flags . role , FLAGGABLE_PROMPTS . role ) ;
157- const companyName = await this . getFlagOrPrompt ( flags [ 'company-name' ] , FLAGGABLE_PROMPTS [ 'company-name' ] ) ;
158- const companyDescription = await this . getFlagOrPrompt (
159- flags [ 'company-description' ] ,
160- FLAGGABLE_PROMPTS [ 'company-description' ]
161- ) ;
162- const companyWebsite = await this . getFlagOrPrompt ( flags [ 'company-website' ] , FLAGGABLE_PROMPTS [ 'company-website' ] ) ;
152+
153+ // If spec is provided, read it first
154+ let inputSpec : Partial < AgentJobSpecV2 > = { } ;
155+ if ( flags . spec ) {
156+ inputSpec = YAML . parse ( readFileSync ( resolve ( flags . spec ) , 'utf8' ) ) as Partial < AgentJobSpecV2 > ;
157+ }
158+
159+ // Flags override inputSpec values. Prompt if neither is set.
160+ const type = flags . type ?? validateAgentType ( inputSpec ?. agentType ) ?? ( await promptForFlag ( FLAGGABLE_PROMPTS . type ) ) ;
161+ const role = flags . role ?? inputSpec ?. role ?? ( await promptForFlag ( FLAGGABLE_PROMPTS . role ) ) ;
162+ const companyName =
163+ flags [ 'company-name' ] ?? inputSpec ?. companyName ?? ( await promptForFlag ( FLAGGABLE_PROMPTS [ 'company-name' ] ) ) ;
164+ const companyDescription =
165+ flags [ 'company-description' ] ??
166+ inputSpec ?. companyDescription ??
167+ ( await promptForFlag ( FLAGGABLE_PROMPTS [ 'company-description' ] ) ) ;
168+ const companyWebsite =
169+ flags [ 'company-website' ] ??
170+ inputSpec ?. companyWebsite ??
171+ ( await promptForFlag ( FLAGGABLE_PROMPTS [ 'company-website' ] ) ) ;
163172
164173 this . log ( ) ;
165174 this . spinner . start ( 'Creating agent spec' ) ;
166175
167176 const connection = flags [ 'target-org' ] . getConnection ( flags [ 'api-version' ] ) ;
168177 const agent = new Agent ( connection , this . project as SfProject ) ;
169178 const specConfig : AgentJobSpecCreateConfigV2 = {
170- agentType : type ,
179+ agentType : type as 'customer' | 'internal' ,
171180 role,
172181 companyName,
173182 companyDescription,
174183 } ;
175184 if ( companyWebsite ) {
176185 specConfig . companyWebsite = companyWebsite ;
177186 }
178- if ( flags [ 'prompt-template' ] ) {
179- specConfig . promptTemplateName = flags [ 'prompt-template' ] ;
180- if ( flags [ 'grounding-context' ] ) {
181- specConfig . groundingContext = flags [ 'grounding-context' ] ;
187+ const promptTemplateName = flags [ 'prompt-template' ] ?? inputSpec ?. promptTemplateName ;
188+ if ( promptTemplateName ) {
189+ specConfig . promptTemplateName = promptTemplateName ;
190+ const groundingContext = flags [ 'grounding-context' ] ?? inputSpec ?. groundingContext ;
191+ if ( groundingContext ) {
192+ specConfig . groundingContext = groundingContext ;
182193 }
183194 }
184- if ( flags [ 'max-topics' ] ) {
185- specConfig . maxNumOfTopics = flags [ 'max-topics' ] ;
195+ const maxNumOfTopics = flags [ 'max-topics' ] ?? validateMaxTopics ( inputSpec ?. maxNumOfTopics ) ;
196+ if ( maxNumOfTopics ) {
197+ specConfig . maxNumOfTopics = maxNumOfTopics ;
186198 }
199+ // Should we log the specConfig being used? It's returned in the JSON and the generated spec.
200+ // this.log(`${ansis.green(figures.tick)} ${ansis.bold(message)} ${ansis.cyan(valueFromFlag)}`);
187201 const agentSpec = await agent . createSpecV2 ( specConfig ) ;
188202
189- // create the directory if not already created
190- mkdirSync ( join ( flags [ 'output-dir' ] ) , { recursive : true } ) ;
191-
192- // Write a yaml file with the returned job specs
193- const filePath = join ( flags [ 'output-dir' ] , flags [ 'file-name' ] ) ;
194- writeFileSync ( filePath , YAML . stringify ( agentSpec ) ) ;
203+ const outputFilePath = writeSpecFile ( flags [ 'output-file' ] , agentSpec ) ;
195204
196205 this . spinner . stop ( ) ;
197206
198- this . log ( `\nSaved agent spec: ${ filePath } ` ) ;
207+ this . log ( `\nSaved agent spec: ${ outputFilePath } ` ) ;
199208
200- return { ...{ isSuccess : true , specPath : filePath } , ...agentSpec } ;
209+ return { ...{ isSuccess : true , specPath : outputFilePath } , ...agentSpec } ;
201210 }
211+ }
202212
203- /**
204- * Get a flag value or prompt the user for a value.
205- *
206- * Resolution order:
207- * - Flag value provided by the user
208- * - Prompt the user for a value
209- */
210- public async getFlagOrPrompt ( valueFromFlag : string | undefined , flagDef : FlaggablePrompt ) : Promise < string > {
211- const message = flagDef . message . replace ( / \. $ / , '' ) ;
213+ const promptForFlag = async ( flagDef : FlaggablePrompt ) : Promise < string > => {
214+ const message = flagDef . message . replace ( / \. $ / , '' ) ;
215+ if ( flagDef . options ) {
216+ return select ( {
217+ choices : flagDef . options . map ( ( o ) => ( { name : o , value : o } ) ) ,
218+ message ,
219+ theme ,
220+ } ) ;
221+ }
212222
213- if ( valueFromFlag ) {
214- this . log ( `${ ansis . green ( figures . tick ) } ${ ansis . bold ( message ) } ${ ansis . cyan ( valueFromFlag ) } ` ) ;
223+ return inquirerInput ( {
224+ message,
225+ validate : flagDef . validate ,
226+ theme,
227+ } ) ;
228+ } ;
215229
216- return valueFromFlag ;
230+ const validateAgentType = ( agentType ?: string ) : string | undefined => {
231+ if ( agentType ) {
232+ if ( ! [ 'customer' , 'internal' ] . includes ( agentType . trim ( ) ) ) {
233+ throw messages . createError ( 'error.invalidAgentType' , [ agentType ] ) ;
217234 }
235+ return agentType . trim ( ) ;
236+ }
237+ } ;
218238
219- if ( flagDef . options ) {
220- return select ( {
221- choices : flagDef . options . map ( ( o ) => ( { name : o , value : o } ) ) ,
222- message ,
223- theme ,
224- } ) ;
239+ const validateMaxTopics = ( maxTopics ?: number ) : number | undefined => {
240+ if ( maxTopics ) {
241+ if ( ! isNaN ( maxTopics ) && isFinite ( maxTopics ) ) {
242+ if ( maxTopics > 0 ) {
243+ return maxTopics ;
244+ }
225245 }
226-
227- return inquirerInput ( {
228- message,
229- validate : flagDef . validate ,
230- theme,
231- } ) ;
246+ throw messages . createError ( 'error.invalidMaxTopics' , [ maxTopics ] ) ;
232247 }
233- }
248+ } ;
249+
250+ const writeSpecFile = ( outputFile : string , agentSpec : AgentJobSpecV2 ) : string => {
251+ // create the directory if not already created
252+ const outputFilePath = resolve ( outputFile ) ;
253+ mkdirSync ( dirname ( outputFilePath ) , { recursive : true } ) ;
254+
255+ // Write a yaml file with the returned job specs
256+ writeFileSync ( outputFilePath , YAML . stringify ( agentSpec ) ) ;
257+
258+ return outputFilePath ;
259+ } ;
0 commit comments