@scout9/app 1.0.0-alpha.0.1.9 → 1.0.0-alpha.0.1.90

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 +32 -0
  2. package/dist/{index-92deaa5f.cjs → exports-e7d51b70.cjs} +46618 -4591
  3. package/dist/index.cjs +58 -15
  4. package/dist/{multipart-parser-090f08a9.cjs → multipart-parser-e09a67c9.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 +30 -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 +131 -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 +158 -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
package/src/core/index.js CHANGED
@@ -1,33 +1,39 @@
1
1
  import archiver from 'archiver';
2
2
  import { globSync } from 'glob';
3
- import { exec } from 'node:child_process';
4
3
  import fss from 'node:fs';
5
4
  import fs from 'node:fs/promises';
6
5
  import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
7
  import fetch, { FormData } from 'node-fetch';
8
- import { runInVM } from '../runtime/index.js';
9
- import { checkVariableType, requireProjectFile } from '../utils/index.js';
8
+ import { Configuration, Scout9Api } from '@scout9/admin';
9
+ import { checkVariableType, ProgressLogger, requireProjectFile } from '../utils/index.js';
10
+ import decompress from 'decompress';
11
+ import { loadUserPackageJson } from './config/project.js';
12
+ import { platformApi } from './data.js';
13
+ import { syncData } from './sync.js';
14
+ import ProjectFiles from '../utils/project.js';
15
+ import { projectTemplates } from '../utils/project-templates.js';
16
+ import { WorkflowEventSchema } from '../runtime/index.js';
17
+ import { logUserValidationError } from '../report.js';
10
18
 
11
-
12
- async function runNpmRunBuild({cwd = process.cwd()} = {}) {
13
- return new Promise((resolve, reject) => {
14
- exec('npm run build', {cwd}, (error, stdout, stderr) => {
15
- if (error) {
16
- console.error(`Build failed: ${error.message}`);
17
- return reject(error);
18
- }
19
- console.log('Build successful');
20
- return resolve(undefined);
21
- });
22
- });
23
- }
19
+ const __filename = fileURLToPath(import.meta.url);
20
+ const __dirname = path.dirname(__filename);
24
21
 
25
22
 
26
23
  function zipDirectory(source, out) {
27
- const archive = archiver('zip', {zlib: {level: 9}});
24
+ const archive = archiver('tar', {
25
+ gzip: true,
26
+ gzipOptions: {level: 9}
27
+ // zlib: {level: 9}
28
+ });
28
29
  const stream = fss.createWriteStream(out);
29
30
 
31
+
30
32
  return new Promise((resolve, reject) => {
33
+ setTimeout(() => {
34
+ reject(new Error('Zip timed out'));
35
+ }, 10 * 1000);
36
+
31
37
  archive
32
38
  .directory(source, false)
33
39
  .on('error', err => reject(err))
@@ -40,65 +46,196 @@ function zipDirectory(source, out) {
40
46
 
41
47
  async function deployZipDirectory(zipFilePath, config) {
42
48
  const form = new FormData();
43
- const blob = new Blob([await fs.readFile(zipFilePath)], {type: 'application/zip'});
44
- form.set('file', blob, path.basename(zipFilePath), {contentType: 'application/zip'});
49
+ if (!fss.existsSync(zipFilePath)) {
50
+ throw new Error(`Missing required zip file ${zipFilePath}`);
51
+ }
52
+ const blob = new Blob([await fs.readFile(zipFilePath)], {type: 'application/gzip'});
53
+ form.set('file', blob, path.basename(zipFilePath), {contentType: 'application/gzip'});
54
+ // const blob = new Blob([await fs.readFile(zipFilePath)], {type: 'application/zip'});
55
+ // form.set('file', blob, path.basename(zipFilePath), {contentType: 'application/zip'});
45
56
  form.set('config', JSON.stringify(config));
46
57
 
47
58
  // @TODO append signature secret header
48
- const response = await fetch(`https://pocket-guide.vercel.app/api/b/platform/upload`, {
59
+ // const url = 'http://localhost:3000/api/b/platform/upload';
60
+ const url = 'https://us-central1-jumpstart.cloudfunctions.net/v1-utils-platform-upload';
61
+ const response = await platformApi(url, {
49
62
  method: 'POST',
50
63
  body: form,
51
64
  headers: {
52
- 'Authorization': process.env.SCOUT9_API_KEY || ''
65
+ 'Authorization': 'Bearer ' + process.env.SCOUT9_API_KEY || ''
53
66
  }
54
67
  });
55
68
  if (!response.ok) {
56
- throw new Error(`Server responded with ${response.status}: ${response.statusText}`);
69
+ throw new Error(`${url} responded with ${response.status}: ${response.statusText}`);
57
70
  }
58
71
 
59
- console.log('File sent successfully');
60
- return true;
72
+ return response;
61
73
  }
62
74
 
63
75
  async function downloadAndUnpackZip(outputDir) {
64
- const downloadLocalResponse = await fetch(
65
- `https://pocket-guide.vercel.app/api/b/platform/download`,
66
- {
67
- headers: {
68
- 'Authorization': process.env.SCOUT9_API_KEY || ''
69
- }
70
- }
71
- );
76
+ const downloadLocalResponse = await platformApi(`https://scout9.com/api/b/platform/download`);
72
77
  if (!downloadLocalResponse.ok) {
73
78
  throw new Error(`Error downloading project file ${downloadLocalResponse.statusText}`);
74
79
  }
75
80
 
76
81
  try {
77
- const buffer = await downloadLocalResponse.arrayBuffer();
78
- const decompress = require('decompress');
79
- await decompress(buffer, outputDir + '/build');
82
+ const arrayBuffer = await downloadLocalResponse.arrayBuffer();
83
+ const outputPath = path.resolve(outputDir, 'build');
84
+ // Convert ArrayBuffer to Buffer
85
+ await decompress(Buffer.from(arrayBuffer), outputPath);
86
+
87
+ console.log('Files unpacked successfully at ' + outputPath);
88
+
89
+ return outputPath;
90
+ } catch (error) {
91
+ console.error('Error unpacking file:', error);
92
+ throw error;
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Builds the app to a specified location
98
+ * @param {string} cwd
99
+ * @param {string} src
100
+ * @param {string} dest
101
+ * @param {import('../runtime/client/config.js').IScout9ProjectBuildConfig} config
102
+ * @returns {Promise<void>}
103
+ */
104
+ async function buildApp(cwd, src, dest, config) {
105
+ // Remove existing directory
106
+ await fs.rm(dest, {recursive: true, force: true});
107
+ // Ensures directory exists
108
+ await fs.mkdir(dest, {recursive: true});
109
+
110
+ const root = src.replace(process.cwd(), '');
111
+
112
+ const copyDirectory = async (source, destination, permittedExtensions) => {
113
+ await fs.mkdir(destination, {recursive: true});
80
114
 
81
- console.log('Files unpacked successfully at ' + outputDir + '/build');
115
+ const dir = await fs.readdir(source, {withFileTypes: true});
116
+ for (const dirent of dir) {
117
+ if (dirent.name.includes('.spec') || dirent.name.includes('.test')) {
118
+ continue; // Skip this file or directory
119
+ }
120
+
121
+ const sourcePath = path.resolve(source, dirent.name);
122
+ const destinationPath = path.resolve(destination, dirent.name);
123
+
124
+ if (dirent.isDirectory()) {
125
+ await copyDirectory(sourcePath, destinationPath, permittedExtensions);
126
+ } else {
127
+ if (permittedExtensions && permittedExtensions.length > 0) {
128
+ const fileExtension = path.extname(dirent.name).replace('.', '');
129
+ if (!permittedExtensions.includes(fileExtension)) {
130
+ // console.log(`Skipping ${dirent.name} because it's not in the permitted extensions list`);
131
+ continue;
132
+ }
133
+ }
134
+ if (sourcePath.includes('entities/agents/index') || sourcePath.includes('entities/agents/config')) {
135
+ // Special case where we have to paste the agent raw data to avoid uploading large audio/txt files
136
+ await fs.writeFile(destinationPath, projectTemplates.entities.agents(config.agents, path.extname(destinationPath)));
137
+ } else if (sourcePath.includes(`${root}/index`)) {
138
+ await fs.writeFile(destinationPath, projectTemplates.root(config, path.extname(destinationPath)));
139
+ } else {
140
+ await fs.copyFile(sourcePath, destinationPath);
141
+ }
142
+
143
+ }
144
+
145
+ }
146
+ };
147
+
148
+ const srcDir = path.resolve(cwd, src);
149
+ const appTemplateJsPath = path.resolve(__dirname, './templates/app.js');
150
+ const templatePackagePath = path.resolve(__dirname, './templates/template-package.json');
151
+
152
+ // Copy src directory
153
+ await copyDirectory(srcDir, path.resolve(dest, 'src'), ['js', 'ts', 'cjs', 'mjs', 'json', 'env']);
154
+
155
+ // Copy user target package.json, first load app.js dependencies/scripts and append to target package.json
156
+ const packageTemplate = JSON.parse(await fs.readFile(new URL(templatePackagePath, import.meta.url), 'utf-8'));
157
+ const {pkg} = await loadUserPackageJson({cwd});
158
+ pkg.dependencies = {...pkg.dependencies, ...packageTemplate.dependencies};
159
+ pkg.scripts.start = 'node app.js';
160
+ await fs.writeFile(path.resolve(dest, 'package.json'), JSON.stringify(pkg, null, 2));
161
+
162
+ // Copy app.js
163
+ await fs.copyFile(appTemplateJsPath, path.resolve(dest, 'app.js'));
164
+
165
+ // Copy .env file
166
+ await fs.copyFile(path.resolve(cwd, '.env'), path.resolve(dest, '.env'));
167
+
168
+ // Copy config.js - redact any sensitive information // @TODO use security encoder
169
+ const redactedConfig = {
170
+ ...config
171
+ };
172
+ for (const agent of redactedConfig.agents) {
173
+ // agent.forwardEmail = 'REDACTED';
174
+ // agent.forwardPhone = 'REDACTED';
175
+ // agent.programmableEmail = 'REDACTED';
176
+ // agent.programmablePhoneNumber = 'REDACTED';
177
+ }
178
+ await fs.writeFile(path.resolve(dest, 'config.js'), `export default ${JSON.stringify(redactedConfig, null, 2)}`);
179
+
180
+ // Copy Dockerfile (if it exists)
181
+ const dockerfile = path.resolve(cwd, './Dockerfile');
182
+ if (fss.existsSync(dockerfile)) {
183
+ await fs.copyFile(dockerfile, path.resolve(dest, 'Dockerfile'));
184
+ } else {
185
+ await fs.copyFile(path.resolve(__dirname, './templates/Dockerfile'), path.resolve(dest, 'Dockerfile'));
186
+ }
187
+
188
+ if (process.env.DEV_MODE === 'true') {
189
+ // Copy dev app folder
190
+ // const clientFolder = path.resolve(__dirname, './templates/public');
191
+ // await copyDirectory(clientFolder, path.resolve(dest, 'public'));
192
+
193
+ // @TODO migrate this into a package
194
+ const devAppFolder = path.resolve(dest, 'public');
195
+ const exists = fss.existsSync(devAppFolder);
196
+ if (!exists) {
197
+ await downloadDevApp(devAppFolder, process.env.DEV_APP_VERSION || 'default');
198
+ }
199
+ }
200
+ }
82
201
 
83
- return outputDir + '/build';
202
+ // For dev server, downloads the dev app if it doesn't exist
203
+ async function downloadDevApp(destination, version) {
204
+ const url = `https://scout9.com/api/b/platform/dev?v=${version}`;
205
+ const downloadLocalResponse = await platformApi(url);
206
+ if (!downloadLocalResponse.ok) {
207
+ throw new Error(`Error downloading scout9 dev app project file ${downloadLocalResponse.statusText}`);
208
+ }
209
+ try {
210
+ const arrayBuffer = await downloadLocalResponse.arrayBuffer();
211
+ await decompress(Buffer.from(arrayBuffer), destination);
212
+ console.log('Dev server unpacked successfully at ' + destination);
84
213
  } catch (error) {
85
214
  console.error('Error unpacking file:', error);
86
215
  throw error;
87
216
  }
217
+
88
218
  }
89
219
 
90
- export async function getApp({cwd = process.cwd(), folder = 'src', ignoreAppRequire = false} = {}) {
91
- const indexTsPath = path.resolve(cwd, folder, 'index.ts');
92
- const indexJsPath = path.join(cwd, folder, 'index.js');
220
+ /**
221
+ * @param {Object} [options]
222
+ * @param {string} [options.cwd]
223
+ * @param {string} [options.src]
224
+ * @param {boolean} [options.ignoreAppRequire]
225
+ * @returns {Promise<{app: *, fileName: string, exe: string, filePath: string}>}
226
+ */
227
+ export async function getApp({cwd = process.cwd(), src = 'src', ignoreAppRequire = false} = {}) {
228
+ const indexTsPath = path.resolve(cwd, src, 'index.ts');
229
+ const indexJsPath = path.resolve(cwd, src, 'index.js');
93
230
  let exe = '';
94
231
  if (fss.existsSync(indexTsPath)) {
95
232
  exe = path.extname(indexTsPath);
96
233
  } else if (fss.existsSync(indexJsPath)) {
97
234
  exe = path.extname(indexJsPath);
98
235
  } else {
99
- throw new Error(`Missing main project entry file ${folder}/index.{js|ts}`);
236
+ throw new Error(`Missing main project entry file ${src}/index.{js|ts}`);
100
237
  }
101
- const filePath = path.resolve(cwd, folder, `app${exe}`);
238
+ const filePath = path.resolve(cwd, src, `app${exe}`);
102
239
  let app;
103
240
  if (!ignoreAppRequire) {
104
241
  app = await requireProjectFile(filePath).then(mod => mod.default);
@@ -110,146 +247,258 @@ export async function getApp({cwd = process.cwd(), folder = 'src', ignoreAppRequ
110
247
  return {app, exe, filePath, fileName: `app${exe}`};
111
248
  }
112
249
 
250
+ /**
251
+ * For each agent, lists out each method of contact
252
+ * @returns {Promise<T>}
253
+ */
254
+ export async function getAgentContacts() {
255
+ const configuration = new Configuration({
256
+ apiKey: process.env.SCOUT9_API_KEY
257
+ });
258
+ const scout9 = new Scout9Api(configuration);
259
+ return Promise.all([
260
+ scout9.agents(),
261
+ platformApi('https://us-central1-jumpstart.cloudfunctions.net/v1-utils-auth', {
262
+ method: 'GET',
263
+ headers: {
264
+ 'Authorization': 'Bearer ' + process.env.SCOUT9_API_KEY || ''
265
+ }
266
+ }).then((res) => {
267
+ console.log(res);
268
+ return res.json();
269
+ })
270
+ ])
271
+ .then((res) => {
272
+ console.log(res[1]);
273
+ return res[0].data.map((agent) => {
274
+ let output = `\n\t${agent.firstName || 'Agent'}${agent.lastName ? ' ' + agent.lastName : ''}:\n`;
275
+ if (agent.programmablePhoneNumber) {
276
+ output += `\t\t- ${agent.programmablePhoneNumber}\n`;
277
+ }
278
+ if (agent.programmableEmail) {
279
+ output += `\t\t- ${agent.programmableEmail}\n`;
280
+ }
281
+ output += `\t\t- https://scout9.com/${res[1].id}/${agent.id || agent.$id}\n`;
282
+ return output;
283
+ }).join('\n');
284
+ })
285
+ .catch((err) => {
286
+ err.message = `Error fetching agents: ${err.message}`;
287
+ throw err;
288
+ });
289
+ }
113
290
 
114
291
  /**
115
292
  * Runs a given project container from scout9 to given environment
293
+ * Runs the project in a container
294
+ *
295
+ * @param {import('../runtime/client/workflow.js').IWorkflowEvent} event - every workflow receives an event object
296
+ * @param {Object} options
297
+ * @param {string} options.eventSource - the source path of the event
298
+ * @returns {Promise<import('../runtime/client/workflow.js').IWorkflowResponse>}
116
299
  */
117
- export async function run(event, {cwd = process.cwd(), folder} = {}) {
118
-
119
- // @TODO use scout9/admin
120
- await downloadAndUnpackZip(folder ? folder : path.resolve(cwd, 'tmp'));
121
-
122
- const {filePath, fileName} = await getApp({
123
- cwd,
124
- folder: folder ? path.resolve(folder, '/build') : 'tmp/build',
125
- ignoreAppRequire: true
300
+ export async function run(event, {eventSource} = {}) {
301
+ const result = WorkflowEventSchema.safeParse(event);
302
+ if (!result.success) {
303
+ logUserValidationError(result.error, eventSource);
304
+ throw result.error;
305
+ }
306
+ const configuration = new Configuration({
307
+ apiKey: process.env.SCOUT9_API_KEY
126
308
  });
309
+ const scout9 = new Scout9Api(configuration);
310
+ const response = await scout9.runPlatform(event)
311
+ .catch((err) => {
312
+ err.message = `Error running platform: ${err.message}`;
313
+ throw err;
314
+ });
315
+ return response.data;
316
+ }
127
317
 
128
- return runInVM(
129
- event,
130
- {folder: folder ? path.resolve(folder, 'build') : path.resolve(cwd, 'tmp/build'), filePath, fileName}
131
- );
318
+ /**
319
+ * Calls scout9 backend to get project config file
320
+ */
321
+ export async function runConfig() {
322
+ if (!process.env.SCOUT9_API_KEY) {
323
+ throw new Error('Missing SCOUT9_API_KEY, please add your Scout9 API key to your .env file');
324
+ }
325
+ const configuration = new Configuration({
326
+ apiKey: process.env.SCOUT9_API_KEY
327
+ });
328
+ const scout9 = new Scout9Api(configuration);
329
+ const response = await scout9.config()
330
+ .catch((err) => {
331
+ err.message = `Error running platform: ${err.message}`;
332
+ throw err;
333
+ });
334
+ return response.data;
132
335
  }
133
336
 
337
+
134
338
  /**
135
339
  * Builds a local project
340
+ * @param {{cwd: string; src: string; dest: string; logger: ProgressLogger; mode: string;}} - build options
341
+ * @param {import('../runtime/client/').IScout9ProjectBuildConfig} config
342
+ * @returns {messages: string[]}
136
343
  */
137
- export async function build({cwd = process.cwd()} = {}, config) {
344
+ export async function build({
345
+ cwd = process.cwd(),
346
+ src = './src',
347
+ dest = '/tmp/project',
348
+ logger = new ProgressLogger(),
349
+ mode
350
+ } = {}, config) {
351
+ const messages = [];
138
352
  // 1. Lint: Run validation checks
139
353
 
140
354
  // Check if app looks good
141
- await getApp({cwd, folder: 'src'});
355
+ await getApp({cwd, src});
142
356
 
143
357
  // Check if workflows look good
144
- console.log('@TODO check if workflows are properly written');
358
+ // console.log('@TODO check if workflows are properly written');
359
+
360
+ // 2. Build app
361
+ await buildApp(cwd, src, dest, config);
145
362
 
146
363
  // 2. Build code in user's project
147
- await runNpmRunBuild({cwd});
364
+ // const buildDir = await runNpmRunBuild({cwd, src: src});
365
+ // const buildPath = buildDir.split('/');
366
+
367
+ // Check if directory "build" exists
368
+ // if (!fss.existsSync(buildDir)) {
369
+ // throw new Error(`Missing required "${buildPath[buildPath.length - 1]}" directory, make sure your build script outputs to a "${buildPath[buildPath.length - 1]}" directory or modify your scout9 config`);
370
+ // }
148
371
 
149
372
 
150
373
  // 3. Remove unnecessary files
151
- const files = globSync(path.resolve(cwd, 'build/**/*(*.test.*|*.spec.*)'));
374
+ // const files = globSync(path.resolve(cwd, `${dest}/**/*(*.test.*|*.spec.*)`), {cwd, absolute: true});
375
+ const files = globSync(`${dest}/**/*(*.test.*|*.spec.*)`, {cwd, absolute: true});
152
376
  for (const file of files) {
153
377
  await fs.unlink(file);
154
378
  }
155
379
 
156
380
  // 3. Run tests
157
381
  // console.log('@TODO run tests');
382
+ return {messages};
158
383
  }
159
384
 
385
+
160
386
  /**
161
387
  * Deploys a local project to scout9
388
+ * @param {{cwd: string; src: string, dest: string}} - build options
389
+ * @param {Scout9ProjectBuildConfig} config
390
+ * @return {Promise<{deploy: Object, contacts: any}>}
162
391
  */
163
- export async function deploy({cwd = process.cwd()}, config) {
164
- const zipFilePath = path.join(cwd, 'build.zip');
165
- await zipDirectory(path.resolve(cwd, 'build'), zipFilePath);
392
+ export async function deploy(
393
+ {cwd = process.cwd(), src = './src', dest = '/tmp/project', logger = new ProgressLogger()},
394
+ config
395
+ ) {
166
396
 
167
- console.log('Project zipped successfully.');
397
+ // Check if app looks good
398
+ await getApp({cwd, src});
168
399
 
400
+ await buildApp(cwd, src, dest, config);
401
+ logger.info(`App built ${dest}`);
402
+
403
+ await test({cwd, src, dest, logger}, config);
404
+
405
+ const destPaths = dest.split('/');
406
+ const zipFilePath = path.resolve(dest, `${destPaths[destPaths.length - 1]}.tar.gz`);
407
+ if (fss.existsSync(zipFilePath)) {
408
+ fss.unlinkSync(zipFilePath);
409
+ }
410
+ await zipDirectory(dest, zipFilePath);
411
+
412
+ logger.info('Project zipped successfully.', zipFilePath);
413
+
414
+ logger.log(`Uploading ${zipFilePath} to Scout9...`);
169
415
  const response = await deployZipDirectory(zipFilePath, config);
170
- console.log('Response from Firebase Function:', response);
416
+
417
+ if (response.status !== 200) {
418
+ logger.error(`Error uploading project to Scout9 ${response.status} - ${response.statusText}`);
419
+ } else {
420
+ logger.info(`File sent successfully: ${response.status} - ${response.statusText}`);
421
+ }
422
+
423
+ logger.log(`Fetching agent contacts...`);
424
+ const contacts = await getAgentContacts();
425
+ logger.info(`Fetched agent contacts`);
426
+
427
+ // Remove temporary directory
428
+ // logger.log(`Cleaning up ${dest}...`);
429
+ // await fs.rmdir(dest, { recursive: true });
430
+ // logger.info(`Cleaned up ${dest}`);
431
+
432
+ return {deploy: response, contacts};
171
433
  }
172
434
 
173
435
  /**
174
- *
175
- * @param {Object} options
176
- * @param {Scout9ProjectBuildConfig} config
177
- * @returns {Promise<void>}
436
+ * Tests a local project to scout9 by running a dummy parse command with the project's local entities
437
+ * @param {{cwd: string; src: string, dest: string}} - build options
438
+ * @param {import('../runtime/client/config.js').IScout9ProjectBuildConfig} config
178
439
  */
179
- export async function sync({cwd = process.cwd(), folder = 'src'} = {}, config) {
180
- const {entities, agents} = await fetch(`https://pocket-guide.vercel.app/api/b/platform/sync`, {
181
- method: 'GET',
182
- headers: {
183
- 'Authorization': process.env.SCOUT9_API_KEY || ''
184
- }
185
- }).then(res => res.json());
186
-
187
- // Merge
188
- config.agents = agents.reduce((accumulator, agent) => {
189
- // Check if agent already exists
190
- const existingAgentIndex = accumulator.findIndex(a => a.id === agent.id);
191
- if (existingAgentIndex === -1) {
192
- accumulator.push(agent);
193
- } else {
194
- // Merge agent
195
- accumulator[existingAgentIndex] = {
196
- ...accumulator[existingAgentIndex],
197
- ...agent
198
- };
199
- }
200
- return accumulator;
201
- }, config.agents);
202
-
203
- config.entities = entities.reduce((accumulator, entity) => {
204
- // Check if agent already exists
205
- const existingEntityIndex = accumulator.findIndex(a => a.id === entity.id);
206
- if (existingEntityIndex === -1) {
207
- accumulator.push(entity);
208
- } else {
209
- // Merge agent
210
- accumulator[existingEntityIndex] = {
211
- ...accumulator[existingEntityIndex],
212
- ...entity
213
- };
214
- }
215
- return accumulator;
216
- }, config.entities);
217
-
218
- // Write to src/agents
219
- const paths = globSync(path.resolve(cwd, `${folder}/entities/agents/{index,config}.{ts,js}`));
220
- if (paths.length === 0) {
221
- throw new Error(`Missing required agents entity file, rerun "scout9 sync" to fix`);
440
+ export async function test(
441
+ {cwd = process.cwd(), src = './src', dest = '/tmp/project', logger = new ProgressLogger()},
442
+ config
443
+ ) {
444
+
445
+ const testableEntities = config.entities.filter(e => e?.definitions?.length > 0 || e?.training?.length > 0);
446
+ if (testableEntities.length === 0) {
447
+ throw new Error(
448
+ 'No testable entities found - make sure you have at least one entity with definitions or training data - learn more at https://scout9.com/docs');
222
449
  }
223
- if (paths.length > 1) {
224
- throw new Error(`Multiple agents entity files found, rerun "scout9 sync" to fix`);
450
+
451
+ const tests = testableEntities.reduce((accumulator, entity) => accumulator += (entity?.tests || []).length, 0);
452
+
453
+ if (tests === 0) {
454
+ logger.warn('No tests found for any entities, skipping test run');
455
+ return;
225
456
  }
226
- const [filePath] = paths;
227
457
 
228
- await fs.writeFile(filePath, `
229
- /**
230
- * Required core entity type: Agents represents you and your team
231
- * @returns {Array<Agent>}
232
- */
233
- export default function Agents() {
234
- return ${JSON.stringify(config.agents, null, 2)};
458
+ // @TODO format errors
459
+ logger.log(`Running ${tests} entity test points...`);
460
+ await new Scout9Api(new Configuration({apiKey: process.env.SCOUT9_API_KEY || ''})).parse({
461
+ message: 'Dummy message to parse',
462
+ language: 'en',
463
+ entities: testableEntities
464
+ });
235
465
  }
236
- `);
237
466
 
238
- for (const entity of config.entities) {
239
- const {entity: _entity, entities, ...rest} = entity;
240
- const fileContent = `
241
467
  /**
242
- * Example entity to help us differentiate if a user wants a delivery or pickup order
243
- * @returns {IEntityBuildConfig}
468
+ *
469
+ * @param {{cwd: string; src: string; projectFiles: ProjectFiles; logger: ProgressLogger}} options
470
+ * @param {import('../runtime/client/config.js').IScout9ProjectBuildConfig} config
471
+ * @returns {Promise<{success: boolean; config: import('../runtime/client/config.js').IScout9ProjectBuildConfig}>}
244
472
  */
245
- export default function () {
246
- return {
247
- ${JSON.stringify(rest, null, 2).replace(/"/g, '')}
248
- }
249
- }
250
- `
251
- await fs.writeFile(`${cwd}/${folder}/entities/${_entity}/index.js`, fileContent);
473
+ export async function sync({
474
+ cwd = process.cwd(), src = 'src',
475
+ projectFiles = new ProjectFiles({src, autoSave: true, cwd}),
476
+ logger = new ProgressLogger()} = {},
477
+ config
478
+ ) {
479
+ if (!process.env.SCOUT9_API_KEY) {
480
+ throw new Error('Missing required environment variable "SCOUT9_API_KEY"');
252
481
  }
253
-
254
- return {success: true}
482
+ logger.log('Fetching project data...');
483
+ // Grabs saved server data on Scout9
484
+ config = await syncData(config);
485
+ logger.log(`Syncing project`);
486
+
487
+ // Uses saved server data to sync project with local data
488
+ await projectFiles.sync(config, (message, type) => {
489
+ switch (type) {
490
+ case 'info':
491
+ logger.info(message);
492
+ break;
493
+ case 'warn':
494
+ logger.warn(message);
495
+ break;
496
+ case 'error':
497
+ logger.error(message);
498
+ break;
499
+ default:
500
+ logger.info(message);
501
+ }
502
+ });
503
+ return {success: true, config};
255
504
  }