-
Notifications
You must be signed in to change notification settings - Fork 0
/
spec.ts
220 lines (195 loc) · 7.2 KB
/
spec.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
/*
* Copyright (c) 2024, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import { join } from 'node:path';
import { writeFileSync } from 'node:fs';
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
import { Messages, SfProject } from '@salesforce/core';
import { Interfaces } from '@oclif/core';
import ansis from 'ansis';
import select from '@inquirer/select';
import inquirerInput from '@inquirer/input';
import figures from '@inquirer/figures';
import { Agent, AgentCreateConfig, SfAgent } from '@salesforce/agents';
import { theme } from '../../../inquirer-theme.js';
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/plugin-agent', 'agent.generate.spec');
export type AgentCreateSpecResult = {
isSuccess: boolean;
errorMessage?: string;
jobSpec?: string; // the location of the job spec file
// We probably need more than this in the returned JSON like
// all the parameters used to generate the spec and the spec contents
};
type FlaggablePrompt = {
message: string;
options?: readonly string[] | string[];
validate: (d: string) => boolean | string;
char?: Interfaces.AlphabetLowercase | Interfaces.AlphabetUppercase;
required?: boolean;
};
type FlagsOfPrompts<T extends Record<string, FlaggablePrompt>> = Record<
keyof T,
Interfaces.OptionFlag<string | undefined, Interfaces.CustomOptions>
>;
const FLAGGABLE_PROMPTS = {
type: {
message: messages.getMessage('flags.type.summary'),
validate: (d: string): boolean | string => d.length > 0 || 'Type cannot be empty',
char: 't',
options: ['customer', 'internal'],
required: true,
},
role: {
message: messages.getMessage('flags.role.summary'),
validate: (d: string): boolean | string => d.length > 0 || 'Role cannot be empty',
required: true,
},
'company-name': {
message: messages.getMessage('flags.company-name.summary'),
validate: (d: string): boolean | string => d.length > 0 || 'Company name cannot be empty',
required: true,
},
'company-description': {
message: messages.getMessage('flags.company-description.summary'),
validate: (d: string): boolean | string => d.length > 0 || 'Company description cannot be empty',
required: true,
},
'company-website': {
message: messages.getMessage('flags.company-website.summary'),
validate: (d: string): boolean | string => {
// Allow empty string
if (d.length === 0) return true;
try {
new URL(d);
return true;
} catch (e) {
return 'Please enter a valid URL';
}
},
},
} satisfies Record<string, FlaggablePrompt>;
function validateInput(input: string, validate: (input: string) => boolean | string): never | string {
const result = validate(input);
if (typeof result === 'string') throw new Error(result);
return input;
}
function makeFlags<T extends Record<string, FlaggablePrompt>>(flaggablePrompts: T): FlagsOfPrompts<T> {
return Object.fromEntries(
Object.entries(flaggablePrompts).map(([key, value]) => [
key,
Flags.string({
summary: value.message,
options: value.options,
char: value.char,
// eslint-disable-next-line @typescript-eslint/require-await
async parse(input) {
return validateInput(input, value.validate);
},
// NOTE: we purposely omit the required property here because we want to allow the flag to be missing in interactive mode
}),
])
) as FlagsOfPrompts<T>;
}
export default class AgentCreateSpec extends SfCommand<AgentCreateSpecResult> {
public static readonly summary = messages.getMessage('summary');
public static readonly description = messages.getMessage('description');
public static readonly examples = messages.getMessages('examples');
public static state = 'beta';
public static readonly requiresProject = true;
public static readonly flags = {
'target-org': Flags.requiredOrg(),
'api-version': Flags.orgApiVersion(),
...makeFlags(FLAGGABLE_PROMPTS),
'output-dir': Flags.directory({
char: 'd',
exists: true,
summary: messages.getMessage('flags.output-dir.summary'),
default: 'config',
}),
'file-name': Flags.string({
char: 'f',
summary: messages.getMessage('flags.file-name.summary'),
default: 'agentSpec.json',
}),
};
public async run(): Promise<AgentCreateSpecResult> {
const { flags } = await this.parse(AgentCreateSpec);
// throw error if --json is used and not all required flags are provided
if (this.jsonEnabled()) {
const missingFlags = Object.entries(FLAGGABLE_PROMPTS)
.filter(([key, prompt]) => 'required' in prompt && prompt.required && !(key in flags))
.map(([key]) => key);
if (missingFlags.length) {
throw new Error(`Missing required flags: ${missingFlags.join(', ')}`);
}
}
this.log();
this.styledHeader('Agent Details');
const type = (await this.getFlagOrPrompt(flags.type, FLAGGABLE_PROMPTS.type)) as 'customer' | 'internal';
const role = await this.getFlagOrPrompt(flags.role, FLAGGABLE_PROMPTS.role);
const companyName = await this.getFlagOrPrompt(flags['company-name'], FLAGGABLE_PROMPTS['company-name']);
const companyDescription = await this.getFlagOrPrompt(
flags['company-description'],
FLAGGABLE_PROMPTS['company-description']
);
const companyWebsite = await this.getFlagOrPrompt(flags['company-website'], FLAGGABLE_PROMPTS['company-website']);
this.log();
this.spinner.start('Creating agent spec');
const connection = flags['target-org'].getConnection(flags['api-version']);
const agent = new Agent(connection, this.project as SfProject) as SfAgent;
const agentSpec = await agent.createSpec({
name: flags['file-name'].split('.json')[0],
type,
role,
companyName,
companyDescription,
companyWebsite,
});
// Write a file with the returned job specs
const filePath = join(flags['output-dir'], flags['file-name']);
writeFileSync(
filePath,
JSON.stringify(
{ type, role, companyName, companyDescription, companyWebsite, jobSpec: agentSpec } as AgentCreateConfig,
null,
4
)
);
this.spinner.stop();
this.log(`\nSaved agent spec: ${filePath}`);
return {
isSuccess: true,
jobSpec: filePath,
};
}
/**
* Get a flag value or prompt the user for a value.
*
* Resolution order:
* - Flag value provided by the user
* - Prompt the user for a value
*/
public async getFlagOrPrompt(valueFromFlag: string | undefined, flagDef: FlaggablePrompt): Promise<string> {
const message = flagDef.message.replace(/\.$/, '');
if (valueFromFlag) {
this.log(`${ansis.green(figures.tick)} ${ansis.bold(message)} ${ansis.cyan(valueFromFlag)}`);
return valueFromFlag;
}
if (flagDef.options) {
return select({
choices: flagDef.options.map((o) => ({ name: o, value: o })),
message,
theme,
});
}
return inquirerInput({
message,
validate: flagDef.validate,
theme,
});
}
}