Skip to content

Commit 2050744

Browse files
Merge pull request #215 from salesforcecli/wr/generateAuthoringBUndle
Wr/generate authoring bundle @W-19470925@
2 parents d917ff1 + 555470e commit 2050744

File tree

5 files changed

+287
-0
lines changed

5 files changed

+287
-0
lines changed

command-snapshot.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,14 @@
5151
],
5252
"plugin": "@salesforce/plugin-agent"
5353
},
54+
{
55+
"alias": [],
56+
"command": "agent:generate:authoring-bundle",
57+
"flagAliases": [],
58+
"flagChars": ["d", "f", "n", "o"],
59+
"flags": ["api-version", "flags-dir", "json", "name", "output-dir", "spec", "target-org"],
60+
"plugin": "@salesforce/plugin-agent"
61+
},
5462
{
5563
"alias": [],
5664
"command": "agent:generate:template",
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# summary
2+
3+
Generate an authoring bundle from an agent specification.
4+
5+
# description
6+
7+
Generates an authoring bundle containing AFScript and its meta.xml file from an agent specification file.
8+
9+
# flags.spec.summary
10+
11+
Path to the agent specification file.
12+
13+
# flags.output-dir.summary
14+
15+
Directory where the authoring bundle files will be generated.
16+
17+
# flags.name.summary
18+
19+
Name (label) of the authoring bundle. If not provided, you will be prompted for it.
20+
21+
# examples
22+
23+
- Generate an authoring bundle from a specification file:
24+
<%= config.bin %> <%= command.id %> --spec-file path/to/spec.yaml --name "My Authoring Bundle"
25+
26+
- Generate an authoring bundle with a custom output directory:
27+
<%= config.bin %> <%= command.id %> --spec-file path/to/spec.yaml --name "My Authoring Bundle" --output-dir path/to/output
28+
29+
# error.no-spec-file
30+
31+
No agent specification file found at the specified path.
32+
33+
# error.invalid-spec-file
34+
35+
The specified file is not a valid agent specification file.
36+
37+
# error.failed-to-create-afscript
38+
39+
Failed to create AFScript from the agent specification.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"$ref": "#/definitions/AgentGenerateAuthoringBundleResult",
4+
"definitions": {
5+
"AgentGenerateAuthoringBundleResult": {
6+
"type": "object",
7+
"properties": {
8+
"afScriptPath": {
9+
"type": "string"
10+
},
11+
"metaXmlPath": {
12+
"type": "string"
13+
},
14+
"outputDir": {
15+
"type": "string"
16+
}
17+
},
18+
"required": ["afScriptPath", "metaXmlPath", "outputDir"],
19+
"additionalProperties": false
20+
}
21+
}
22+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
* Copyright 2025, Salesforce, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { join } from 'node:path';
18+
import { mkdirSync, writeFileSync, readFileSync } from 'node:fs';
19+
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
20+
import { Messages, SfError } from '@salesforce/core';
21+
import { Agent, AgentJobSpec } from '@salesforce/agents';
22+
import YAML from 'yaml';
23+
import { FlaggablePrompt, promptForFlag } from '../../../flags.js';
24+
25+
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
26+
const messages = Messages.loadMessages('@salesforce/plugin-agent', 'agent.generate.authoring-bundle');
27+
28+
export type AgentGenerateAuthoringBundleResult = {
29+
afScriptPath: string;
30+
metaXmlPath: string;
31+
outputDir: string;
32+
};
33+
34+
export default class AgentGenerateAuthoringBundle extends SfCommand<AgentGenerateAuthoringBundleResult> {
35+
public static readonly summary = messages.getMessage('summary');
36+
public static readonly description = messages.getMessage('description');
37+
public static readonly examples = messages.getMessages('examples');
38+
public static readonly requiresProject = true;
39+
public static state = 'beta';
40+
41+
public static readonly flags = {
42+
'target-org': Flags.requiredOrg(),
43+
'api-version': Flags.orgApiVersion(),
44+
spec: Flags.file({
45+
summary: messages.getMessage('flags.spec.summary'),
46+
char: 'f',
47+
exists: true,
48+
}),
49+
'output-dir': Flags.directory({
50+
summary: messages.getMessage('flags.output-dir.summary'),
51+
char: 'd',
52+
}),
53+
name: Flags.string({
54+
summary: messages.getMessage('flags.name.summary'),
55+
char: 'n',
56+
}),
57+
};
58+
59+
private static readonly FLAGGABLE_PROMPTS = {
60+
name: {
61+
message: messages.getMessage('flags.name.summary'),
62+
validate: (d: string): boolean | string => d.length > 0 || 'Name cannot be empty',
63+
required: true,
64+
},
65+
spec: {
66+
message: messages.getMessage('flags.spec.summary'),
67+
validate: (d: string): boolean | string => d.length > 0 || 'Spec file path cannot be empty',
68+
required: true,
69+
},
70+
} satisfies Record<string, FlaggablePrompt>;
71+
72+
public async run(): Promise<AgentGenerateAuthoringBundleResult> {
73+
const { flags } = await this.parse(AgentGenerateAuthoringBundle);
74+
const { 'output-dir': outputDir, 'target-org': targetOrg } = flags;
75+
76+
// If we don't have a spec yet, prompt for it
77+
const spec = flags['spec'] ?? (await promptForFlag(AgentGenerateAuthoringBundle.FLAGGABLE_PROMPTS['spec']));
78+
79+
// If we don't have a name yet, prompt for it
80+
const name = (
81+
flags['name'] ?? (await promptForFlag(AgentGenerateAuthoringBundle.FLAGGABLE_PROMPTS['name']))
82+
).replaceAll(' ', '_');
83+
84+
try {
85+
// Get default output directory if not specified
86+
const defaultOutputDir = join(this.project!.getDefaultPackage().fullPath, 'main', 'default');
87+
const targetOutputDir = join(outputDir ?? defaultOutputDir, 'aiAuthoringBundle', name);
88+
89+
// Generate file paths
90+
const afScriptPath = join(targetOutputDir, `${name}.afscript`);
91+
const metaXmlPath = join(targetOutputDir, `${name}.authoring-bundle-meta.xml`);
92+
93+
// Write AFScript file
94+
const conn = targetOrg.getConnection(flags['api-version']);
95+
const specContents = YAML.parse(readFileSync(spec, 'utf8')) as AgentJobSpec;
96+
const afScript = await Agent.createAfScript(conn, specContents);
97+
// Create output directory if it doesn't exist
98+
mkdirSync(targetOutputDir, { recursive: true });
99+
writeFileSync(afScriptPath, afScript);
100+
101+
// Write meta.xml file
102+
const metaXml = `<?xml version="1.0" encoding="UTF-8"?>
103+
<aiAuthoringBundle>
104+
<Label>${specContents.role}</Label>
105+
<BundleType>${specContents.agentType}</BundleType>
106+
<VersionTag>Spring2026</VersionTag>
107+
<VersionDescription>Initial release for ${name}</VersionDescription>
108+
<SourceBundleVersion></SourceBundleVersion>
109+
<Target></Target>
110+
</aiAuthoringBundle>`;
111+
writeFileSync(metaXmlPath, metaXml);
112+
113+
this.logSuccess(`Successfully generated ${name} Authoring Bundle`);
114+
115+
return {
116+
afScriptPath,
117+
metaXmlPath,
118+
outputDir: targetOutputDir,
119+
};
120+
} catch (error) {
121+
const err = SfError.wrap(error);
122+
throw new SfError(messages.getMessage('error.failed-to-create-afscript'), 'AfScriptGenerationError', [
123+
err.message,
124+
]);
125+
}
126+
}
127+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* Copyright 2025, Salesforce, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { join } from 'node:path';
18+
import { existsSync, readFileSync } from 'node:fs';
19+
import { expect } from 'chai';
20+
import { genUniqueString, TestSession } from '@salesforce/cli-plugins-testkit';
21+
import { execCmd } from '@salesforce/cli-plugins-testkit';
22+
import type { AgentGenerateAuthoringBundleResult } from '../../src/commands/agent/generate/authoring-bundle.js';
23+
24+
let session: TestSession;
25+
26+
describe.skip('agent generate authoring-bundle NUTs', () => {
27+
before(async () => {
28+
session = await TestSession.create({
29+
project: {
30+
sourceDir: join('test', 'mock-projects', 'agent-generate-template'),
31+
},
32+
devhubAuthStrategy: 'AUTO',
33+
scratchOrgs: [
34+
{
35+
setDefault: true,
36+
config: join('config', 'project-scratch-def.json'),
37+
},
38+
],
39+
});
40+
});
41+
42+
after(async () => {
43+
await session?.clean();
44+
});
45+
46+
describe('agent generate authoring-bundle', () => {
47+
const specFileName = genUniqueString('agentSpec_%s.yaml');
48+
const bundleName = 'Test_Bundle';
49+
50+
it('should generate authoring bundle from spec file', async () => {
51+
const username = session.orgs.get('default')!.username as string;
52+
const specPath = join(session.project.dir, 'specs', specFileName);
53+
54+
// First generate a spec file
55+
const specCommand = `agent generate agent-spec --target-org ${username} --type customer --role "test agent role" --company-name "Test Company" --company-description "Test Description" --output-file ${specPath} --json`;
56+
execCmd(specCommand, { ensureExitCode: 0 });
57+
58+
// Now generate the authoring bundle
59+
const command = `agent generate authoring-bundle --spec ${specPath} --name ${bundleName} --target-org ${username} --json`;
60+
const result = execCmd<AgentGenerateAuthoringBundleResult>(command, { ensureExitCode: 0 }).jsonOutput?.result;
61+
62+
expect(result).to.be.ok;
63+
expect(result?.afScriptPath).to.be.ok;
64+
expect(result?.metaXmlPath).to.be.ok;
65+
expect(result?.outputDir).to.be.ok;
66+
67+
// Verify files exist
68+
expect(existsSync(result!.afScriptPath)).to.be.true;
69+
expect(existsSync(result!.metaXmlPath)).to.be.true;
70+
71+
// Verify file contents
72+
const afScript = readFileSync(result!.afScriptPath, 'utf8');
73+
const metaXml = readFileSync(result!.metaXmlPath, 'utf8');
74+
expect(afScript).to.be.ok;
75+
expect(metaXml).to.include('<aiAuthoringBundle>');
76+
expect(metaXml).to.include(bundleName);
77+
});
78+
79+
it('should use default output directory when not specified', async () => {
80+
const username = session.orgs.get('default')!.username as string;
81+
const specPath = join(session.project.dir, 'specs', specFileName);
82+
const defaultPath = join('force-app', 'main', 'default', 'aiAuthoringBundle');
83+
84+
const command = `agent generate authoring-bundle --spec ${specPath} --name ${bundleName} --target-org ${username} --json`;
85+
const result = execCmd<AgentGenerateAuthoringBundleResult>(command, { ensureExitCode: 0 }).jsonOutput?.result;
86+
87+
expect(result).to.be.ok;
88+
expect(result?.outputDir).to.include(defaultPath);
89+
});
90+
});
91+
});

0 commit comments

Comments
 (0)