Skip to content

Commit e750805

Browse files
Merge pull request #326 from salesforcecli/ml/W-21220749-agent-creation-cli-interview
feat: rewrite authoring bundle creation as wizard-style flow @W-21220749@
2 parents 9c29017 + 7601628 commit e750805

File tree

3 files changed

+493
-54
lines changed

3 files changed

+493
-54
lines changed

messages/agent.generate.authoring-bundle.md

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ This command requires an org because it uses it to access an LLM for generating
1616

1717
# flags.spec.summary
1818

19-
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.
19+
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.
2020

2121
# flags.spec.prompt
2222

@@ -44,7 +44,7 @@ API name of the new authoring bundle; if not specified, the API name is derived
4444

4545
# flags.api-name.prompt
4646

47-
API name of the new authoring bundle
47+
Enter authoring bundle API name
4848

4949
# examples
5050

@@ -78,4 +78,52 @@ The specified file is not a valid agent spec YAML file.
7878

7979
# error.failed-to-create-agent
8080

81-
Failed to create an authoring bundle from the agent spec YAML file.
81+
Failed to generate authoring bundle: %s.
82+
83+
# wizard.specType.prompt
84+
85+
Select an authoring bundle template
86+
87+
# wizard.specType.option.default.name
88+
89+
Default template (Recommended)
90+
91+
# wizard.specType.option.default.description
92+
93+
Start with a ready-to-use Agent Script template.
94+
95+
# wizard.specType.option.fromSpec.name
96+
97+
From an agent spec YAML file (Advanced)
98+
99+
# wizard.specType.option.fromSpec.description
100+
101+
Generate an Agent Script file from an existing agent spec YAML file.
102+
103+
# wizard.specFile.prompt
104+
105+
Select the agent spec YAML file
106+
107+
# wizard.name.prompt
108+
109+
Enter the authoring bundle name
110+
111+
# wizard.name.validation.required
112+
113+
Authoring bundle name is required.
114+
115+
# wizard.name.validation.empty
116+
117+
Authoring bundle name can't be empty.
118+
119+
# progress.title
120+
121+
Generating authoring bundle: %s
122+
123+
# success.message
124+
125+
Authoring bundle "%s" was generated successfully.
126+
127+
# warning.noSpecDir
128+
129+
No agent spec directory found at %s.

src/commands/agent/generate/authoring-bundle.ts

Lines changed: 86 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,13 @@
1515
*/
1616

1717
import { join, resolve } from 'node:path';
18-
import { readFileSync, existsSync } from 'node:fs';
18+
import { readFileSync, existsSync, readdirSync } from 'node:fs';
1919
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
2020
import { generateApiName, Messages, SfError } from '@salesforce/core';
2121
import { AgentJobSpec, ScriptAgent } from '@salesforce/agents';
2222
import YAML from 'yaml';
23-
import { input as inquirerInput } from '@inquirer/prompts';
23+
import { select, input as inquirerInput } from '@inquirer/prompts';
2424
import { theme } from '../../../inquirer-theme.js';
25-
import { FlaggablePrompt, promptForFlag, promptForSpecYaml } from '../../../flags.js';
2625

2726
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
2827
const messages = Messages.loadMessages('@salesforce/plugin-agent', 'agent.generate.authoring-bundle');
@@ -62,45 +61,6 @@ export default class AgentGenerateAuthoringBundle extends SfCommand<AgentGenerat
6261
}),
6362
};
6463

65-
private static readonly FLAGGABLE_PROMPTS = {
66-
name: {
67-
message: messages.getMessage('flags.name.summary'),
68-
promptMessage: messages.getMessage('flags.name.prompt'),
69-
validate: (d: string): boolean | string =>
70-
d.trim().length > 0 || 'Name cannot be empty or contain only whitespace',
71-
required: true,
72-
},
73-
'api-name': {
74-
message: messages.getMessage('flags.api-name.summary'),
75-
promptMessage: messages.getMessage('flags.api-name.prompt'),
76-
validate: (d: string): boolean | string => {
77-
if (d.length === 0) {
78-
return true;
79-
}
80-
if (d.length > 80) {
81-
return 'API name cannot be over 80 characters.';
82-
}
83-
const regex = /^[A-Za-z][A-Za-z0-9_]*[A-Za-z0-9]+$/;
84-
if (!regex.test(d)) {
85-
return 'Invalid API name.';
86-
}
87-
return true;
88-
},
89-
},
90-
spec: {
91-
message: messages.getMessage('flags.spec.summary'),
92-
promptMessage: messages.getMessage('flags.spec.prompt'),
93-
validate: (d: string): boolean | string => {
94-
const specPath = resolve(d);
95-
if (!existsSync(specPath)) {
96-
return 'Please enter an existing agent spec (yaml) file';
97-
}
98-
return true;
99-
},
100-
required: true,
101-
},
102-
} satisfies Record<string, FlaggablePrompt>;
103-
10464
public async run(): Promise<AgentGenerateAuthoringBundleResult> {
10565
const { flags } = await this.parse(AgentGenerateAuthoringBundle);
10666
const { 'output-dir': outputDir } = flags;
@@ -109,7 +69,7 @@ export default class AgentGenerateAuthoringBundle extends SfCommand<AgentGenerat
10969
throw new SfError(messages.getMessage('error.specAndNoSpec'));
11070
}
11171

112-
// Resolve spec: --no-spec => undefined (default spec), --spec <path> => path, missing => prompt
72+
// Resolve spec: --no-spec => undefined, --spec <path> => path, missing => wizard prompts
11373
let spec: string | undefined;
11474
if (flags['no-spec']) {
11575
spec = undefined;
@@ -120,19 +80,89 @@ export default class AgentGenerateAuthoringBundle extends SfCommand<AgentGenerat
12080
}
12181
spec = specPath;
12282
} else {
123-
spec = await promptForSpecYaml(AgentGenerateAuthoringBundle.FLAGGABLE_PROMPTS['spec']);
83+
// Find spec files in specs/ directory
84+
const specsDir = join(this.project!.getPath(), 'specs');
85+
let specFiles: string[] = [];
86+
87+
if (existsSync(specsDir)) {
88+
specFiles = readdirSync(specsDir).filter(
89+
(f) => (f.endsWith('.yaml') || f.endsWith('.yml')) && !f.includes('-testSpec')
90+
);
91+
} else {
92+
this.warn(messages.getMessage('warning.noSpecDir', [specsDir]));
93+
}
94+
95+
// Build spec type choices
96+
const specTypeChoices: Array<{ name: string; value: 'default' | 'fromSpec'; description: string }> = [
97+
{
98+
name: messages.getMessage('wizard.specType.option.default.name'),
99+
value: 'default',
100+
description: messages.getMessage('wizard.specType.option.default.description'),
101+
},
102+
];
103+
104+
if (specFiles.length > 0) {
105+
specTypeChoices.push({
106+
name: messages.getMessage('wizard.specType.option.fromSpec.name'),
107+
value: 'fromSpec',
108+
description: messages.getMessage('wizard.specType.option.fromSpec.description'),
109+
});
110+
}
111+
112+
const specType = await select({
113+
message: messages.getMessage('wizard.specType.prompt'),
114+
choices: specTypeChoices,
115+
theme,
116+
});
117+
118+
if (specType === 'fromSpec') {
119+
const selectedFile = await select({
120+
message: messages.getMessage('wizard.specFile.prompt'),
121+
choices: specFiles.map((f) => ({ name: f, value: join(specsDir, f) })),
122+
theme,
123+
});
124+
spec = selectedFile;
125+
} else {
126+
spec = undefined;
127+
}
124128
}
125129

126-
// If we don't have a name yet, prompt for it
127-
const name = flags['name'] ?? (await promptForFlag(AgentGenerateAuthoringBundle.FLAGGABLE_PROMPTS['name']));
130+
// Resolve name: --name flag or prompt
131+
const name =
132+
flags['name'] ??
133+
(await inquirerInput({
134+
message: messages.getMessage('wizard.name.prompt'),
135+
validate: (d: string): boolean | string => {
136+
if (d.length === 0) {
137+
return messages.getMessage('wizard.name.validation.required');
138+
}
139+
if (d.trim().length === 0) {
140+
return messages.getMessage('wizard.name.validation.empty');
141+
}
142+
return true;
143+
},
144+
theme,
145+
}));
128146

129-
// If we don't have an api name yet, prompt for it
147+
// Resolve API name: --api-name flag or auto-generate from name with prompt to confirm
130148
let bundleApiName = flags['api-name'];
131149
if (!bundleApiName) {
132150
bundleApiName = generateApiName(name);
133151
const promptedValue = await inquirerInput({
134152
message: messages.getMessage('flags.api-name.prompt'),
135-
validate: AgentGenerateAuthoringBundle.FLAGGABLE_PROMPTS['api-name'].validate,
153+
validate: (d: string): boolean | string => {
154+
if (d.length === 0) {
155+
return true;
156+
}
157+
if (d.length > 80) {
158+
return 'API name cannot be over 80 characters.';
159+
}
160+
const regex = /^[A-Za-z][A-Za-z0-9_]*[A-Za-z0-9]+$/;
161+
if (!regex.test(d)) {
162+
return 'Invalid API name.';
163+
}
164+
return true;
165+
},
136166
default: bundleApiName,
137167
theme,
138168
});
@@ -150,8 +180,10 @@ export default class AgentGenerateAuthoringBundle extends SfCommand<AgentGenerat
150180
const agentPath = join(targetOutputDir, `${bundleApiName}.agent`);
151181
const metaXmlPath = join(targetOutputDir, `${bundleApiName}.bundle-meta.xml`);
152182

153-
// Write Agent file
183+
this.spinner.start(messages.getMessage('progress.title', [bundleApiName]));
184+
154185
const parsedSpec = spec ? (YAML.parse(readFileSync(spec, 'utf8')) as AgentJobSpec) : undefined;
186+
155187
await ScriptAgent.createAuthoringBundle({
156188
agentSpec: {
157189
...parsedSpec,
@@ -163,16 +195,19 @@ export default class AgentGenerateAuthoringBundle extends SfCommand<AgentGenerat
163195
bundleApiName,
164196
});
165197

166-
this.logSuccess(`Successfully generated ${bundleApiName} Authoring Bundle`);
198+
this.spinner.stop();
199+
200+
this.logSuccess(messages.getMessage('success.message', [name]));
167201

168202
return {
169203
agentPath,
170204
metaXmlPath,
171205
outputDir: targetOutputDir,
172206
};
173207
} catch (error) {
208+
this.spinner.stop('failed');
174209
const err = SfError.wrap(error);
175-
throw new SfError(messages.getMessage('error.failed-to-create-agent'), 'AgentGenerationError', [err.message]);
210+
throw new SfError(messages.getMessage('error.failed-to-create-agent', [err.message]), 'AgentGenerationError');
176211
}
177212
}
178213
}

0 commit comments

Comments
 (0)