@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.
Files changed (70) hide show
  1. package/README.md +33 -0
  2. package/dist/{index-92deaa5f.cjs → exports-212ef6be.cjs} +46636 -4591
  3. package/dist/index.cjs +58 -15
  4. package/dist/{multipart-parser-090f08a9.cjs → multipart-parser-54a3ab5f.cjs} +13 -7
  5. package/dist/spirits-3b603262.cjs +1218 -0
  6. package/dist/spirits.cjs +9 -0
  7. package/dist/testing-tools.cjs +48 -0
  8. package/package.json +37 -8
  9. package/src/cli.js +162 -69
  10. package/src/core/config/agents.js +300 -7
  11. package/src/core/config/entities.js +58 -28
  12. package/src/core/config/index.js +37 -15
  13. package/src/core/config/project.js +160 -6
  14. package/src/core/config/workflow.js +13 -12
  15. package/src/core/data.js +27 -0
  16. package/src/core/index.js +386 -137
  17. package/src/core/sync.js +71 -0
  18. package/src/core/templates/Dockerfile +22 -0
  19. package/src/core/templates/app.js +453 -0
  20. package/src/core/templates/project-files.js +36 -0
  21. package/src/core/templates/template-package.json +13 -0
  22. package/src/exports.js +21 -17
  23. package/src/platform.js +189 -33
  24. package/src/public.d.ts.text +330 -0
  25. package/src/report.js +117 -0
  26. package/src/runtime/client/api.js +56 -159
  27. package/src/runtime/client/config.js +60 -11
  28. package/src/runtime/client/entity.js +19 -6
  29. package/src/runtime/client/index.js +5 -3
  30. package/src/runtime/client/message.js +13 -3
  31. package/src/runtime/client/platform.js +86 -0
  32. package/src/runtime/client/{agent.js → users.js} +35 -3
  33. package/src/runtime/client/utils.js +10 -9
  34. package/src/runtime/client/workflow.js +132 -9
  35. package/src/runtime/entry.js +2 -2
  36. package/src/testing-tools/dev.js +373 -0
  37. package/src/testing-tools/index.js +1 -0
  38. package/src/testing-tools/mocks.js +37 -5
  39. package/src/testing-tools/spirits.js +530 -0
  40. package/src/utils/audio-buffer.js +16 -0
  41. package/src/utils/audio-type.js +27 -0
  42. package/src/utils/configs/agents.js +68 -0
  43. package/src/utils/configs/entities.js +145 -0
  44. package/src/utils/configs/project.js +23 -0
  45. package/src/utils/configs/workflow.js +47 -0
  46. package/src/utils/file-type.js +569 -0
  47. package/src/utils/file.js +164 -0
  48. package/src/utils/glob.js +30 -0
  49. package/src/utils/image-buffer.js +23 -0
  50. package/src/utils/image-type.js +39 -0
  51. package/src/utils/index.js +1 -0
  52. package/src/utils/is-svg.js +37 -0
  53. package/src/utils/logger.js +111 -0
  54. package/src/utils/module.js +14 -25
  55. package/src/utils/project-templates.js +191 -0
  56. package/src/utils/project.js +387 -0
  57. package/src/utils/video-type.js +29 -0
  58. package/types/index.d.ts +7588 -206
  59. package/types/index.d.ts.map +97 -22
  60. package/dist/index-1b8d7dd2.cjs +0 -49555
  61. package/dist/index-2ccb115e.cjs +0 -49514
  62. package/dist/index-66b06a30.cjs +0 -49549
  63. package/dist/index-bc029a1d.cjs +0 -49528
  64. package/dist/index-d9a93523.cjs +0 -49527
  65. package/dist/multipart-parser-1508046a.cjs +0 -413
  66. package/dist/multipart-parser-7007403a.cjs +0 -413
  67. package/dist/multipart-parser-70c32c1d.cjs +0 -413
  68. package/dist/multipart-parser-71dec101.cjs +0 -413
  69. package/dist/multipart-parser-f15bf2e0.cjs +0 -414
  70. package/src/public.d.ts +0 -209
@@ -1,11 +1,94 @@
1
1
  import path from 'node:path';
2
- import { globSync } from 'glob/dist/esm/index.js';
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
- export default async function loadAgentConfig({cwd = process.cwd(), folder = 'src'} = {}) {
7
- console.log('@TODO implement loadAgentConfig');
8
- const paths = globSync(path.resolve(cwd, `${folder}/entities/agents/{index,config}.{ts,js}`));
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(paths[0]).then(mod => mod.default);
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
- const result = agentsConfigurationSchema.safeParse(agents);
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
- entitiesRootProjectConfigurationSchema,
5
- entityApiConfigurationSchema,
6
- entityConfigurationSchema,
7
- entityRootProjectConfigurationSchema
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.join(dir, `api${extension}`);
15
- const root = cwd.split('/').pop();
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(), folder = 'src'} = {}
37
+ {cwd = process.cwd(), src = 'src', logger, cb = (message) => {}} = {}
37
38
  ) {
38
- console.log('loadEntitiesConfig', {cwd, folder});
39
+ /** @type import('../../runtime/client/config.js').IEntitiesRootProjectConfiguration */
39
40
  const config = [];
40
- const paths = globSync(path.resolve(cwd, `${folder}/entities/**/{index,config,api}.{ts,js}`));
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 path of paths) {
43
- const segments = path.split('/');
44
- const srcIndex = segments.findIndex((segment, index) => segment === folder && segments[index + 1] === 'entities');
45
- const parents = segments.slice(srcIndex + 2, -1).reverse(); // +2 to skip "${folder}" and "entities"
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, path);
48
- data.push({path, parents, api});
50
+ const api = await loadEntityApiConfig(cwd, filePath);
51
+ data.push({filePath, parents, api});
49
52
  } else {
50
- console.log(`WARNING: "${path}" Is not a valid entity path, must be contained in a named folder under entities/`);
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 {path, parents, api} of data) {
59
+ for (const {filePath, parents, api} of data) {
57
60
  let entityConfig = {};
58
- const fileName = path.split('/')?.pop()?.split('.')?.[0];
59
- if (!fileName) throw new Error(`Invalid file name "${path}"`);
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(path).then(mod => mod.default);
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 "${path}"`);
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
- config.push(entityProjectConfig);
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]"`);
@@ -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({ cwd = process.cwd()} = {}) {
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('Missing SCOUT9_API_KEY within .env file');
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
- * @param folder
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({ cwd = process.cwd(), folder = 'src'} = {}) {
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
- ...await loadProjectConfig({cwd, folder}),
38
- entities: await loadEntitiesConfig({cwd, folder}),
39
- agents: await loadAgentConfig({cwd, folder}),
40
- workflows: await loadWorkflowsConfig({cwd, folder})
41
- }
55
+ ...baseProjectConfig,
56
+ entities: entitiesConfig,
57
+ agents: agentsConfig,
58
+ workflows: workflowsConfig
59
+ };
42
60
 
43
61
  // Validate the config
44
- Scout9ProjectBuildConfigSchema.parse(projectConfig);
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
  }