@scout9/app 1.0.0-alpha.0.1.9 → 1.0.0-alpha.0.1.91
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -0
- package/dist/{index-92deaa5f.cjs → exports-212ef6be.cjs} +46636 -4591
- package/dist/index.cjs +58 -15
- package/dist/{multipart-parser-090f08a9.cjs → multipart-parser-54a3ab5f.cjs} +13 -7
- package/dist/spirits-3b603262.cjs +1218 -0
- package/dist/spirits.cjs +9 -0
- package/dist/testing-tools.cjs +48 -0
- package/package.json +37 -8
- package/src/cli.js +162 -69
- package/src/core/config/agents.js +300 -7
- package/src/core/config/entities.js +58 -28
- package/src/core/config/index.js +37 -15
- package/src/core/config/project.js +160 -6
- package/src/core/config/workflow.js +13 -12
- package/src/core/data.js +27 -0
- package/src/core/index.js +386 -137
- package/src/core/sync.js +71 -0
- package/src/core/templates/Dockerfile +22 -0
- package/src/core/templates/app.js +453 -0
- package/src/core/templates/project-files.js +36 -0
- package/src/core/templates/template-package.json +13 -0
- package/src/exports.js +21 -17
- package/src/platform.js +189 -33
- package/src/public.d.ts.text +330 -0
- package/src/report.js +117 -0
- package/src/runtime/client/api.js +56 -159
- package/src/runtime/client/config.js +60 -11
- package/src/runtime/client/entity.js +19 -6
- package/src/runtime/client/index.js +5 -3
- package/src/runtime/client/message.js +13 -3
- package/src/runtime/client/platform.js +86 -0
- package/src/runtime/client/{agent.js → users.js} +35 -3
- package/src/runtime/client/utils.js +10 -9
- package/src/runtime/client/workflow.js +132 -9
- package/src/runtime/entry.js +2 -2
- package/src/testing-tools/dev.js +373 -0
- package/src/testing-tools/index.js +1 -0
- package/src/testing-tools/mocks.js +37 -5
- package/src/testing-tools/spirits.js +530 -0
- package/src/utils/audio-buffer.js +16 -0
- package/src/utils/audio-type.js +27 -0
- package/src/utils/configs/agents.js +68 -0
- package/src/utils/configs/entities.js +145 -0
- package/src/utils/configs/project.js +23 -0
- package/src/utils/configs/workflow.js +47 -0
- package/src/utils/file-type.js +569 -0
- package/src/utils/file.js +164 -0
- package/src/utils/glob.js +30 -0
- package/src/utils/image-buffer.js +23 -0
- package/src/utils/image-type.js +39 -0
- package/src/utils/index.js +1 -0
- package/src/utils/is-svg.js +37 -0
- package/src/utils/logger.js +111 -0
- package/src/utils/module.js +14 -25
- package/src/utils/project-templates.js +191 -0
- package/src/utils/project.js +387 -0
- package/src/utils/video-type.js +29 -0
- package/types/index.d.ts +7588 -206
- package/types/index.d.ts.map +97 -22
- package/dist/index-1b8d7dd2.cjs +0 -49555
- package/dist/index-2ccb115e.cjs +0 -49514
- package/dist/index-66b06a30.cjs +0 -49549
- package/dist/index-bc029a1d.cjs +0 -49528
- package/dist/index-d9a93523.cjs +0 -49527
- package/dist/multipart-parser-1508046a.cjs +0 -413
- package/dist/multipart-parser-7007403a.cjs +0 -413
- package/dist/multipart-parser-70c32c1d.cjs +0 -413
- package/dist/multipart-parser-71dec101.cjs +0 -413
- package/dist/multipart-parser-f15bf2e0.cjs +0 -414
- package/src/public.d.ts +0 -209
|
@@ -1,11 +1,94 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import colors from 'kleur';
|
|
4
|
+
import { globSync } from 'glob';
|
|
5
|
+
import { Configuration, Scout9Api } from '@scout9/admin';
|
|
3
6
|
import { checkVariableType, requireProjectFile } from '../../utils/index.js';
|
|
4
|
-
import { agentsConfigurationSchema } from '../../runtime/index.js';
|
|
7
|
+
import { agentsBaseConfigurationSchema, agentsConfigurationSchema } from '../../runtime/index.js';
|
|
8
|
+
import { audioExtensions } from '../../utils/audio-type.js';
|
|
9
|
+
import { fileTypeFromBuffer } from '../../utils/file-type.js';
|
|
10
|
+
import { videoExtensions } from '../../utils/video-type.js';
|
|
11
|
+
import { projectTemplates } from '../../utils/project-templates.js';
|
|
12
|
+
import { toBuffer, writeFileToLocal } from '../../utils/file.js';
|
|
13
|
+
import imageBuffer from '../../utils/image-buffer.js';
|
|
14
|
+
import audioBuffer from '../../utils/audio-buffer.js';
|
|
15
|
+
import { yellow } from 'kleur/colors';
|
|
5
16
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
17
|
+
|
|
18
|
+
async function registerAgent({agent}) {
|
|
19
|
+
if (agent.id) {
|
|
20
|
+
console.log(`Agent already registered: ${agent.id}`);
|
|
21
|
+
return agent.id;
|
|
22
|
+
}
|
|
23
|
+
const {id} = (await (new Scout9Api(new Configuration({apiKey: process.env.SCOUT9_API_KEY}))).agentRegister(
|
|
24
|
+
agent
|
|
25
|
+
).then(res => res.data));
|
|
26
|
+
if (!id) {
|
|
27
|
+
throw new Error(`Failed to register agent`);
|
|
28
|
+
}
|
|
29
|
+
return id;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {Object} inputs
|
|
34
|
+
* @param {string | Buffer} inputs.img
|
|
35
|
+
* @param inputs.agentId
|
|
36
|
+
* @param [inputs.source='']
|
|
37
|
+
* @returns {Promise<string>}
|
|
38
|
+
*/
|
|
39
|
+
async function writeImgToServer({img, agentId, source = ''}) {
|
|
40
|
+
const {url} = (await (new Scout9Api(new Configuration({apiKey: process.env.SCOUT9_API_KEY}))).agentProfileUpload(
|
|
41
|
+
agentId,
|
|
42
|
+
await imageBuffer(img, source).then(r => r.buffer)
|
|
43
|
+
).then(res => res.data));
|
|
44
|
+
if (!url) {
|
|
45
|
+
throw new Error(`Failed to upload agent image`);
|
|
46
|
+
}
|
|
47
|
+
return url;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function writeTranscriptsToServer({transcripts, agentId, source = ''}) {
|
|
51
|
+
for (let i = 0; i < transcripts.length; i++) {
|
|
52
|
+
const result = await fileTypeFromBuffer(transcripts[i]);
|
|
53
|
+
if (!result || result.ext !== 'txt' || result.mime !== 'text/plain') {
|
|
54
|
+
throw new Error(`Invalid transcript type: ${result?.mime || 'N/A'}, expected text/plain (.txt file, got ${result?.ext || 'Missing ext'})`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const {urls} = (await (new Scout9Api(new Configuration({apiKey: process.env.SCOUT9_API_KEY}))).agentTranscriptUpload(
|
|
58
|
+
agentId,
|
|
59
|
+
transcripts,
|
|
60
|
+
).then(res => res.data));
|
|
61
|
+
if (!urls) {
|
|
62
|
+
throw new Error(`Failed to upload agent image`);
|
|
63
|
+
}
|
|
64
|
+
return urls;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function writeAudiosToServer({audios, agentId, source = ''}) {
|
|
68
|
+
const buffers = [];
|
|
69
|
+
for (let i = 0; i < audios.length; i++) {
|
|
70
|
+
const {buffer} = await audioBuffer(audios[i], true, source);
|
|
71
|
+
buffers.push(buffer);
|
|
72
|
+
}
|
|
73
|
+
const {urls} = (await (new Scout9Api(new Configuration({apiKey: process.env.SCOUT9_API_KEY}))).agentTranscriptUpload(
|
|
74
|
+
agentId,
|
|
75
|
+
buffers
|
|
76
|
+
).then(res => res.data));
|
|
77
|
+
if (!urls) {
|
|
78
|
+
throw new Error(`Failed to upload agent image`);
|
|
79
|
+
}
|
|
80
|
+
return urls;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export default async function loadAgentConfig({
|
|
84
|
+
cwd = process.cwd(),
|
|
85
|
+
dest = '/tmp/project',
|
|
86
|
+
deploying = false,
|
|
87
|
+
src = 'src',
|
|
88
|
+
cb = (message) => {
|
|
89
|
+
}
|
|
90
|
+
} = {}) {
|
|
91
|
+
const paths = globSync(`${src}/entities/agents/{index,config}.{ts,js}`, {cwd, absolute: true});
|
|
9
92
|
if (paths.length === 0) {
|
|
10
93
|
throw new Error(`Missing required agents entity file, rerun "scout9 sync" to fix`);
|
|
11
94
|
}
|
|
@@ -13,10 +96,12 @@ export default async function loadAgentConfig({cwd = process.cwd(), folder = 'sr
|
|
|
13
96
|
throw new Error(`Multiple agents entity files found, rerun "scout9 sync" to fix`);
|
|
14
97
|
}
|
|
15
98
|
|
|
99
|
+
const sourceFile = paths[0];
|
|
100
|
+
|
|
16
101
|
/**
|
|
17
102
|
* @type {Array<Agent> | function(): Array<Agent> | function(): Promise<Array<Agent>>}
|
|
18
103
|
*/
|
|
19
|
-
const mod = await requireProjectFile(
|
|
104
|
+
const mod = await requireProjectFile(sourceFile).then(mod => mod.default);
|
|
20
105
|
|
|
21
106
|
// @type {Array<Agent>}
|
|
22
107
|
let agents = [];
|
|
@@ -33,9 +118,217 @@ export default async function loadAgentConfig({cwd = process.cwd(), folder = 'sr
|
|
|
33
118
|
throw new Error(`Invalid entity type (${entityType}) returned at "${path}"`);
|
|
34
119
|
}
|
|
35
120
|
|
|
36
|
-
|
|
121
|
+
let serverDeployed = false; // Track whether we deployed to server, so that we can sync code base
|
|
122
|
+
|
|
123
|
+
// Send warnings if not properly registered
|
|
124
|
+
for (const agent of agents) {
|
|
125
|
+
|
|
126
|
+
if (!agent.id && deploying) {
|
|
127
|
+
const {img, transcripts, audios, ...rest} = agent;
|
|
128
|
+
agent.id = await registerAgent({agent: rest});
|
|
129
|
+
cb(`✅ Registered ${agent.firstName || 'agent'} with id: ${agent.id}`);
|
|
130
|
+
serverDeployed = true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!agent.forwardPhone && !agent.forwardEmail) {
|
|
134
|
+
cb(yellow(`⚠️src/entities/agents.js|ts: neither a ".forwardPhone" or ".forwardEmail" to ${agent.firstName || JSON.stringify(agent)} - messages cannot be forward to you.`));
|
|
135
|
+
}
|
|
136
|
+
if (!agent.programmablePhoneNumber) {
|
|
137
|
+
const userName = agent.firstName ? `${agent.firstName}${agent.lastName ? ' ' + agent.lastName : ''}` : agent.forwardPhone;
|
|
138
|
+
cb(`⚠️${colors.yellow('Warning')}: ${userName} does not have a masked phone number to do auto replies. You can register one at ${colors.cyan(
|
|
139
|
+
'https://scout9.com/b')} under ${colors.green('users')} > ${colors.green(userName)}. Then run ${colors.cyan(
|
|
140
|
+
'scout9 sync')} to update.`);
|
|
141
|
+
}
|
|
142
|
+
if (agent.forwardPhone && agents.filter(a => a.forwardPhone && (a.forwardPhone === agent.forwardPhone)).length > 1) {
|
|
143
|
+
cb(yellow(`⚠️ src/entities/agents.js|ts: ".forwardPhone: ${agent.forwardPhone}" should only be associated to one agent within your project`));
|
|
144
|
+
}
|
|
145
|
+
if (agent.forwardEmail && agents.filter(a => a.forwardEmail && (a.forwardEmail === agent.forwardEmail)).length > 1) {
|
|
146
|
+
cb(yellow(`⚠️ src/entities/agents.js|ts: ".forwardEmail: ${agent.forwardEmail}" can only be associated to one agent within your project`));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Handle agent image changes
|
|
150
|
+
if (agent.img) {
|
|
151
|
+
if (typeof agent.img === 'string') {
|
|
152
|
+
if (!agent.img.startsWith('https://storage.googleapis.com')) {
|
|
153
|
+
// Got string file, must either be a URL or a local file path
|
|
154
|
+
if (deploying) {
|
|
155
|
+
agent.img = await writeImgToServer({
|
|
156
|
+
img: agent.img,
|
|
157
|
+
agentId: agent.id,
|
|
158
|
+
source: sourceFile
|
|
159
|
+
});
|
|
160
|
+
cb(`✅ Uploaded ${agent.firstName || 'agent'}'s profile image to ${agent.img}`);
|
|
161
|
+
serverDeployed = true;
|
|
162
|
+
} else {
|
|
163
|
+
agent.img = await writeFileToLocal({
|
|
164
|
+
file: agent.img,
|
|
165
|
+
fileName: `${agent.firstName || 'agent'}.png`,
|
|
166
|
+
source: sourceFile
|
|
167
|
+
}).then(({uri, isImage}) => {
|
|
168
|
+
if (!isImage) {
|
|
169
|
+
throw new Error(`Invalid image type: ${typeof agent.img}`);
|
|
170
|
+
}
|
|
171
|
+
return uri;
|
|
172
|
+
});
|
|
173
|
+
cb(`✅ Copied ${agent.firstName || 'agent'}'s profile image to ${agent.img}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} else if (Buffer.isBuffer(agent.img)) {
|
|
177
|
+
if (deploying) {
|
|
178
|
+
agent.img = await writeImgToServer({
|
|
179
|
+
img: agent.img,
|
|
180
|
+
fileName: `${agent.firstName || 'agent'}.png`,
|
|
181
|
+
agentId: agent.id,
|
|
182
|
+
source: sourceFile
|
|
183
|
+
});
|
|
184
|
+
cb(`✅ Uploaded ${agent.firstName || 'agent'}'s profile image to ${agent.img}`);
|
|
185
|
+
serverDeployed = true;
|
|
186
|
+
} else {
|
|
187
|
+
agent.img = await writeFileToLocal({
|
|
188
|
+
file: agent.img,
|
|
189
|
+
fileName: `${agent.firstName || 'agent'}.png`,
|
|
190
|
+
source: sourceFile
|
|
191
|
+
}).then(({uri, isImage}) => {
|
|
192
|
+
if (!isImage) {
|
|
193
|
+
throw new Error(`Invalid image type: ${typeof agent.img}`);
|
|
194
|
+
}
|
|
195
|
+
return uri;
|
|
196
|
+
});
|
|
197
|
+
cb(`✅ Copied ${agent.firstName || 'agent'}'s profile image to ${agent.img}`);
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
throw new Error(`Invalid img type: ${typeof agent.img}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
// Handle transcripts
|
|
206
|
+
if ((agent?.transcripts || []).length > 0) {
|
|
207
|
+
const deployedTranscripts = [];
|
|
208
|
+
const pendingTranscripts = [];
|
|
209
|
+
for (const transcript of agent.transcripts) {
|
|
210
|
+
if (typeof transcript === 'string') {
|
|
211
|
+
if (!transcript.startsWith('https://storage.googleapis.com')) {
|
|
212
|
+
pendingTranscripts.push(await fs.readFile(transcript));
|
|
213
|
+
} else {
|
|
214
|
+
deployedTranscripts.push(transcript);
|
|
215
|
+
}
|
|
216
|
+
} else if (Buffer.isBuffer(transcript)) {
|
|
217
|
+
const txtResult = await fileTypeFromBuffer(transcript);
|
|
218
|
+
if (txtResult?.ext !== 'txt' || txtResult?.mime !== 'text/plain') {
|
|
219
|
+
throw new Error(`Invalid transcript type: ${txtResult?.mime || 'N/A'}, expected text/plain (.txt file, got ${txtResult?.ext || 'Missing ext'})`);
|
|
220
|
+
}
|
|
221
|
+
pendingTranscripts.push(transcript);
|
|
222
|
+
} else {
|
|
223
|
+
throw new Error(`Invalid transcript type: ${typeof transcript}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let urls = [];
|
|
228
|
+
if (deploying) {
|
|
229
|
+
urls = await writeTranscriptsToServer({transcripts: pendingTranscripts, agentId: agent.id});
|
|
230
|
+
cb(`✅ Uploaded ${agent.firstName || 'agent'}'s transcripts to ${urls}`);
|
|
231
|
+
serverDeployed = true;
|
|
232
|
+
} else {
|
|
233
|
+
for (let i = 0; i < pendingTranscripts.length; i++) {
|
|
234
|
+
const transcript = pendingTranscripts[i];
|
|
235
|
+
urls.push(await writeFileToLocal({
|
|
236
|
+
file: transcript,
|
|
237
|
+
fileName: `transcript_${i + deployedTranscripts.length}.txt`,
|
|
238
|
+
source: sourceFile
|
|
239
|
+
}).then(({uri, mime, ext}) => {
|
|
240
|
+
if (mime !== 'text/plain') {
|
|
241
|
+
throw new Error(`Invalid transcript type: ${mime}, expected text/plain (.txt file, got ${ext})`);
|
|
242
|
+
}
|
|
243
|
+
return uri;
|
|
244
|
+
})
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
cb(`✅ Copied ${agent.firstName || 'agent'}'s transcripts to ${urls}`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
agent.transcripts = [
|
|
251
|
+
...deployedTranscripts,
|
|
252
|
+
...urls
|
|
253
|
+
];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if ((agent?.audios || []).length > 0) {
|
|
257
|
+
const deployedAudios = [];
|
|
258
|
+
const pendingAudios = [];
|
|
259
|
+
for (const audio of agent.audios) {
|
|
260
|
+
if (typeof audio === 'string') {
|
|
261
|
+
if (!audio.startsWith('https://storage.googleapis.com')) {
|
|
262
|
+
// If not on GCS, then it must be a local file path or remote URL
|
|
263
|
+
pendingAudios.push(await toBuffer(audio, sourceFile).then(({buffer, ext, mime, isAudio, isVideo}) => {
|
|
264
|
+
if (!isAudio && !isVideo) {
|
|
265
|
+
throw new Error(`Invalid audio/video type: ${mime}, expected audio/* or video/* got ${ext}`);
|
|
266
|
+
}
|
|
267
|
+
return buffer;
|
|
268
|
+
}));
|
|
269
|
+
} else {
|
|
270
|
+
// Already deployed, append string URL
|
|
271
|
+
deployedAudios.push(audio);
|
|
272
|
+
}
|
|
273
|
+
} else if (Buffer.isBuffer(audio)) {
|
|
274
|
+
const fileType = await fileTypeFromBuffer(audio);
|
|
275
|
+
if (!fileType) {
|
|
276
|
+
throw new Error(`Invalid audio type: ${typeof audio}`);
|
|
277
|
+
}
|
|
278
|
+
if (videoExtensions.has(fileType.ext) || audioExtensions.has(fileType.ext)) {
|
|
279
|
+
pendingAudios.push(audio);
|
|
280
|
+
} else {
|
|
281
|
+
throw new Error(`Invalid audio/video type: ${fileType.mime}, expected audio/* or video/*, got ${fileType.ext}`);
|
|
282
|
+
}
|
|
283
|
+
} else {
|
|
284
|
+
throw new Error(`Invalid audio type: ${typeof audio}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
let urls = [];
|
|
289
|
+
if (deploying) {
|
|
290
|
+
urls = await writeAudiosToServer({audios: pendingAudios, agentId: agent.id, source: sourceFile});
|
|
291
|
+
cb(`✅ Uploaded ${agent.firstName || 'agent'}'s audios to ${urls}`);
|
|
292
|
+
serverDeployed = true;
|
|
293
|
+
} else {
|
|
294
|
+
for (let i = 0; i < pendingAudios.length; i++) {
|
|
295
|
+
const audio = pendingAudios[i];
|
|
296
|
+
urls.push(await writeFileToLocal({
|
|
297
|
+
file: audio,
|
|
298
|
+
fileName: `audio_${i + deployedAudios.length}`,
|
|
299
|
+
source: sourceFile
|
|
300
|
+
}).then(({uri, mime, ext, isAudio, isVideo}) => {
|
|
301
|
+
if (!isAudio && !isVideo) {
|
|
302
|
+
throw new Error(`Invalid audio/video type: ${mime}, expected audio/* or video/* got ${ext}`);
|
|
303
|
+
}
|
|
304
|
+
return uri;
|
|
305
|
+
}));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
agent.audios = [
|
|
310
|
+
...deployedAudios,
|
|
311
|
+
...urls
|
|
312
|
+
];
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const result = (deploying ? agentsConfigurationSchema : agentsBaseConfigurationSchema).safeParse(agents);
|
|
37
317
|
if (!result.success) {
|
|
318
|
+
result.error.source = paths[0];
|
|
38
319
|
throw result.error;
|
|
39
320
|
}
|
|
321
|
+
|
|
322
|
+
if (serverDeployed) {
|
|
323
|
+
cb(`Syncing ${sourceFile} with latest server changes`);
|
|
324
|
+
await fs.writeFile(sourceFile, projectTemplates.entities.agents(agents, path.extname(sourceFile)));
|
|
325
|
+
// const update = await p.confirm({
|
|
326
|
+
// message: `Changes uploaded, sync local entities/agents file?`,
|
|
327
|
+
// initialValue: true
|
|
328
|
+
// });
|
|
329
|
+
// if (update) {
|
|
330
|
+
// }
|
|
331
|
+
}
|
|
332
|
+
|
|
40
333
|
return agents;
|
|
41
334
|
}
|
|
@@ -1,21 +1,19 @@
|
|
|
1
1
|
import { globSync } from 'glob';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
entitiesRootProjectConfigurationSchema,
|
|
5
|
+
entityApiConfigurationSchema,
|
|
6
|
+
entityConfigurationSchema,
|
|
7
|
+
entityRootProjectConfigurationSchema
|
|
8
8
|
} from '../../runtime/index.js';
|
|
9
9
|
import { checkVariableType, requireOptionalProjectFile, requireProjectFile } from '../../utils/index.js';
|
|
10
|
+
import { logUserValidationError } from '../../report.js';
|
|
10
11
|
|
|
11
12
|
async function loadEntityApiConfig(cwd, filePath) {
|
|
12
13
|
const dir = path.dirname(filePath);
|
|
13
14
|
const extension = path.extname(filePath);
|
|
14
|
-
const apiFilePath = path.
|
|
15
|
-
const
|
|
16
|
-
const x = apiFilePath.replace(cwd, '').split('/').slice(1).join('/');
|
|
17
|
-
|
|
18
|
-
const mod = await requireOptionalProjectFile(x);
|
|
15
|
+
const apiFilePath = path.resolve(dir, `api${extension}`);
|
|
16
|
+
const mod = await requireOptionalProjectFile(apiFilePath);
|
|
19
17
|
|
|
20
18
|
if (mod) {
|
|
21
19
|
const config = {};
|
|
@@ -32,34 +30,40 @@ async function loadEntityApiConfig(cwd, filePath) {
|
|
|
32
30
|
}
|
|
33
31
|
}
|
|
34
32
|
|
|
33
|
+
/**
|
|
34
|
+
* @returns {Promise<import('../../runtime/client/config.js').IEntitiesRootProjectConfiguration>}
|
|
35
|
+
*/
|
|
35
36
|
export default async function loadEntitiesConfig(
|
|
36
|
-
{cwd = process.cwd(),
|
|
37
|
+
{cwd = process.cwd(), src = 'src', logger, cb = (message) => {}} = {}
|
|
37
38
|
) {
|
|
38
|
-
|
|
39
|
+
/** @type import('../../runtime/client/config.js').IEntitiesRootProjectConfiguration */
|
|
39
40
|
const config = [];
|
|
40
|
-
const paths = globSync(path.resolve(cwd, `${
|
|
41
|
+
// const paths = globSync(path.resolve(cwd, `${src}/entities/**/{index,config,api}.{ts,js}`), {cwd, absolute: true});
|
|
42
|
+
const filePaths = globSync(`${src}/entities/**/{index,config,api}.{ts,js}`, {cwd, absolute: true});
|
|
41
43
|
const data = [];
|
|
42
|
-
for (const
|
|
43
|
-
const segments =
|
|
44
|
-
const
|
|
45
|
-
const
|
|
44
|
+
for (const filePath of filePaths) {
|
|
45
|
+
const segments = filePath.split(path.sep); // Use path.sep for platform independence
|
|
46
|
+
// const segments = filePath.split('/');
|
|
47
|
+
const srcIndex = segments.findIndex((segment, index) => segment === src && segments[index + 1] === 'entities');
|
|
48
|
+
const parents = segments.slice(srcIndex + 2, -1).reverse(); // +2 to skip "${src}" and "entities"
|
|
46
49
|
if (parents.length > 0) {
|
|
47
|
-
const api = await loadEntityApiConfig(cwd,
|
|
48
|
-
data.push({
|
|
50
|
+
const api = await loadEntityApiConfig(cwd, filePath);
|
|
51
|
+
data.push({filePath, parents, api});
|
|
49
52
|
} else {
|
|
50
|
-
console.log(`WARNING: "${
|
|
53
|
+
console.log(`WARNING: "${filePath}" Is not a valid entity filePath, must be contained in a named src under entities/`);
|
|
51
54
|
}
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
const specialEntities = ['agents'];
|
|
55
58
|
|
|
56
|
-
for (const {
|
|
59
|
+
for (const {filePath, parents, api} of data) {
|
|
57
60
|
let entityConfig = {};
|
|
58
|
-
const fileName =
|
|
59
|
-
|
|
61
|
+
// const fileName = filePath.split('/')?.pop()?.split('.')?.[0];
|
|
62
|
+
const fileName = path.basename(filePath, path.extname(filePath));
|
|
63
|
+
if (!fileName) throw new Error(`Invalid file name "${filePath}"`);
|
|
60
64
|
const isSpecial = specialEntities.some(se => parents.includes(se));
|
|
61
65
|
if ((fileName === 'index' || fileName === 'config') && !isSpecial) {
|
|
62
|
-
const entityConfigHandler = await requireProjectFile(
|
|
66
|
+
const entityConfigHandler = await requireProjectFile(filePath).then(mod => mod.default);
|
|
63
67
|
// Check if entityConfig is a function or object
|
|
64
68
|
const entityType = checkVariableType(entityConfigHandler);
|
|
65
69
|
switch (entityType) {
|
|
@@ -71,12 +75,13 @@ export default async function loadEntitiesConfig(
|
|
|
71
75
|
entityConfig = entityConfigHandler;
|
|
72
76
|
break;
|
|
73
77
|
default:
|
|
74
|
-
throw new Error(`Invalid entity type (${entityType}) returned at "${
|
|
78
|
+
throw new Error(`Invalid entity type (${entityType}) returned at "${filePath}"`);
|
|
75
79
|
}
|
|
76
80
|
|
|
77
81
|
// Validate entity configuration
|
|
78
82
|
const result = entityConfigurationSchema.safeParse(entityConfig, {path: ['entities', config.length]});
|
|
79
83
|
if (!result.success) {
|
|
84
|
+
logUserValidationError(result.error, filePath);
|
|
80
85
|
throw result.error;
|
|
81
86
|
}
|
|
82
87
|
} else if (isSpecial && (fileName === 'index' || fileName === 'config')) {
|
|
@@ -88,16 +93,41 @@ export default async function loadEntitiesConfig(
|
|
|
88
93
|
const entityProjectConfig = {
|
|
89
94
|
...entityConfig,
|
|
90
95
|
entity: parents[0],
|
|
91
|
-
entities: parents,
|
|
96
|
+
entities: parents.reverse(),
|
|
92
97
|
api
|
|
93
98
|
};
|
|
94
99
|
entityRootProjectConfigurationSchema.parse(entityProjectConfig);
|
|
95
|
-
|
|
96
|
-
|
|
100
|
+
const existingIndex = config.findIndex(c => c.entity === entityProjectConfig.entity);
|
|
101
|
+
if (existingIndex > -1) {
|
|
102
|
+
if (config[existingIndex].entities.length !== entityProjectConfig.entities.length) {
|
|
103
|
+
throw new Error(`Invalid entity configuration at "${filePath}", entity name mismatch`);
|
|
104
|
+
}
|
|
105
|
+
config[existingIndex] = {
|
|
106
|
+
...config[existingIndex],
|
|
107
|
+
definitions: [
|
|
108
|
+
...(config[existingIndex].definitions),
|
|
109
|
+
...(entityProjectConfig.definitions || [])
|
|
110
|
+
],
|
|
111
|
+
training: [
|
|
112
|
+
...(config[existingIndex].training),
|
|
113
|
+
...(entityProjectConfig.training || [])
|
|
114
|
+
],
|
|
115
|
+
tests: [
|
|
116
|
+
...(config[existingIndex].tests),
|
|
117
|
+
...(entityProjectConfig.tests || [])
|
|
118
|
+
],
|
|
119
|
+
api: (!config[existingIndex].api && !entityProjectConfig.api) ? null : {
|
|
120
|
+
...(config[existingIndex].api || {}),
|
|
121
|
+
...(entityProjectConfig.api || {})
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
config.push(entityProjectConfig);
|
|
126
|
+
}
|
|
97
127
|
}
|
|
98
128
|
|
|
99
129
|
if (!config.some(c => c.entity === 'customers')) {
|
|
100
|
-
throw new Error(`Missing required entity: "entities/customers"`);
|
|
130
|
+
throw new Error(`Missing required entity: "entities/customers" in ${src}`);
|
|
101
131
|
}
|
|
102
132
|
if (!config.some(c => c.entity === '[customer]')) {
|
|
103
133
|
throw new Error(`Missing required entity: "entities/customers/[customer]"`);
|
package/src/core/config/index.js
CHANGED
|
@@ -6,10 +6,17 @@ import loadEntitiesConfig from './entities.js';
|
|
|
6
6
|
import loadProjectConfig from './project.js';
|
|
7
7
|
import loadWorkflowsConfig from './workflow.js';
|
|
8
8
|
import { Scout9ProjectBuildConfigSchema } from '../../runtime/index.js';
|
|
9
|
+
import { ProgressLogger } from '../../utils/index.js';
|
|
9
10
|
|
|
10
11
|
|
|
11
|
-
export function loadEnvConfig({
|
|
12
|
+
export function loadEnvConfig({
|
|
13
|
+
cwd = process.cwd(), cb = (msg) => {
|
|
14
|
+
}
|
|
15
|
+
} = {}) {
|
|
12
16
|
if (!!process.env.SCOUT9_API_KEY) {
|
|
17
|
+
if (process.env.SCOUT9_API_KEY.includes('insert-scout9-api-key')) {
|
|
18
|
+
throw new Error('Missing SCOUT9_API_KEY, please add your Scout9 API key to your .env file');
|
|
19
|
+
}
|
|
13
20
|
return;
|
|
14
21
|
}
|
|
15
22
|
const configFilePath = path.resolve(cwd, './.env');
|
|
@@ -17,31 +24,46 @@ export function loadEnvConfig({ cwd = process.cwd()} = {}) {
|
|
|
17
24
|
if (!process.env.SCOUT9_API_KEY) {
|
|
18
25
|
const exists = fs.existsSync(configFilePath);
|
|
19
26
|
if (!exists) {
|
|
20
|
-
throw new Error(`Missing .env file with SCOUT9_API_KEY`);
|
|
27
|
+
throw new Error(`Missing .env file with "SCOUT9_API_KEY".\n\n\tTo fix, create a .env file at the root of your project.\nAdd "SCOUT9_API_KEY=<your-scout9-api-key>" to the .env file.\n\n\t> You can get your API key at https://scout9.com\n\n`);
|
|
21
28
|
} else {
|
|
22
|
-
throw new Error(
|
|
29
|
+
throw new Error(`Missing "SCOUT9_API_KEY" within .env file.\n\n\tTo fix, add "SCOUT9_API_KEY=<your-scout9-api-key>" to the .env file.\n\n\tYou can get your API key at https://scout9.com\n\n`);
|
|
23
30
|
}
|
|
24
31
|
}
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
/**
|
|
28
|
-
*
|
|
29
|
-
* @param cwd
|
|
30
|
-
* @
|
|
31
|
-
* @returns {Promise<Scout9ProjectBuildConfig>}
|
|
35
|
+
* @deprecated use "new ProjectFiles(...).load()" instead
|
|
36
|
+
* @param {{cwd: string; src: string; logger?: ProgressLogger; deploying?: boolean; cb?: (message: string) => void}} - build options
|
|
37
|
+
* @returns {Promise<import('../../runtime/client/config.js').IScout9ProjectBuildConfig>}
|
|
32
38
|
*/
|
|
33
|
-
export async function loadConfig({
|
|
39
|
+
export async function loadConfig({
|
|
40
|
+
cwd = process.cwd(), src = 'src', dest = '/tmp/project', deploying = false, logger = new ProgressLogger(), cb = (msg) => {
|
|
41
|
+
}
|
|
42
|
+
} = {}) {
|
|
34
43
|
// Load globals
|
|
35
|
-
loadEnvConfig({cwd});
|
|
44
|
+
loadEnvConfig({cwd, src, logger, cb});
|
|
45
|
+
|
|
46
|
+
const baseProjectConfig = await loadProjectConfig({cwd, src, logger, cb, deploying});
|
|
47
|
+
const entitiesConfig = await loadEntitiesConfig({cwd, src, logger, cb, deploying});
|
|
48
|
+
const agentsConfig = await loadAgentConfig({cwd, src, logger, cb, deploying, dest});
|
|
49
|
+
const workflowsConfig = await loadWorkflowsConfig({cwd, src, logger, deploying, cb});
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @type {import('../../runtime/client/config.js').IScout9ProjectBuildConfig}
|
|
53
|
+
*/
|
|
36
54
|
const projectConfig = {
|
|
37
|
-
...
|
|
38
|
-
entities:
|
|
39
|
-
agents:
|
|
40
|
-
workflows:
|
|
41
|
-
}
|
|
55
|
+
...baseProjectConfig,
|
|
56
|
+
entities: entitiesConfig,
|
|
57
|
+
agents: agentsConfig,
|
|
58
|
+
workflows: workflowsConfig
|
|
59
|
+
};
|
|
42
60
|
|
|
43
61
|
// Validate the config
|
|
44
|
-
Scout9ProjectBuildConfigSchema.
|
|
62
|
+
const result = Scout9ProjectBuildConfigSchema.safeParse(projectConfig);
|
|
63
|
+
if (!result.success) {
|
|
64
|
+
result.error.source = `${src}/index.js`;
|
|
65
|
+
throw result.error;
|
|
66
|
+
}
|
|
45
67
|
|
|
46
68
|
return projectConfig;
|
|
47
69
|
}
|