@scout9/app 1.0.0-alpha.0.0.1
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/package.json +59 -0
- package/postinstall.js +2 -0
- package/scout9-app.js +1 -0
- package/src/cli.js +35 -0
- package/src/core/config/agents.js +5 -0
- package/src/core/config/entities.js +110 -0
- package/src/core/config/index.js +31 -0
- package/src/core/config/project.js +17 -0
- package/src/core/config/workflow.js +46 -0
- package/src/core/index.js +167 -0
- package/src/exports.js +49 -0
- package/src/index.js +1 -0
- package/src/platform.js +71 -0
- package/src/runtime/client/agent.js +44 -0
- package/src/runtime/client/api.js +183 -0
- package/src/runtime/client/conversation.js +0 -0
- package/src/runtime/client/entity.js +81 -0
- package/src/runtime/client/index.js +5 -0
- package/src/runtime/client/message.js +8 -0
- package/src/runtime/client/utils.js +11 -0
- package/src/runtime/client/workflow.js +75 -0
- package/src/runtime/entry.js +92 -0
- package/src/runtime/index.js +2 -0
- package/src/testing-tools/index.js +1 -0
- package/src/testing-tools/mocks.js +47 -0
- package/src/types/api.ts +6410 -0
- package/src/types/index.ts +4 -0
- package/src/types/project.ts +49 -0
- package/src/types/schemas.ts +22 -0
- package/src/types/utils.ts +1 -0
- package/src/utils/error.js +22 -0
- package/src/utils/index.js +3 -0
- package/src/utils/module.js +67 -0
- package/src/utils/stats.js +83 -0
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@scout9/app",
|
|
3
|
+
"version": "1.0.0-alpha.0.0.1",
|
|
4
|
+
"description": "Build and deploy your Scout9 app for SMS auto reply",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git://github.com/Scout9Official/scout9-nodejs.git"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"postinstall": "node postinstall.js",
|
|
14
|
+
"types": "npx -p typescript tsc src/**/*.ts --declaration --allowJs --emitDeclarationOnly --esModuleInterop --outDir dist/types"
|
|
15
|
+
},
|
|
16
|
+
"type": "module",
|
|
17
|
+
"bin": {
|
|
18
|
+
"scout9-app": "./scout9-app.js"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"src",
|
|
22
|
+
"!src/**/*.spec.js",
|
|
23
|
+
"!src/core/**/fixtures",
|
|
24
|
+
"!src/core/**/test",
|
|
25
|
+
"types",
|
|
26
|
+
"scout9-app.js",
|
|
27
|
+
"postinstall.js"
|
|
28
|
+
],
|
|
29
|
+
"exports": {
|
|
30
|
+
"./package.json": "./package.json",
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./types/index.d.ts",
|
|
33
|
+
"import": "./src/exports/index.js"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"keywords": [],
|
|
37
|
+
"author": "",
|
|
38
|
+
"license": "ISC",
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/archiver": "^6.0.2",
|
|
41
|
+
"@types/jest": "^29.5.11",
|
|
42
|
+
"ts-jest": "^29.1.1",
|
|
43
|
+
"typescript": "^5.3.3"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"archiver": "^6.0.1",
|
|
47
|
+
"decompress": "^4.2.1",
|
|
48
|
+
"dotenv": "^16.3.1",
|
|
49
|
+
"glob": "^10.3.10",
|
|
50
|
+
"kleur": "^4.1.5",
|
|
51
|
+
"moment": "^2.29.4",
|
|
52
|
+
"node-fetch": "^3.3.2",
|
|
53
|
+
"sade": "^1.8.1",
|
|
54
|
+
"zod": "^3.22.4"
|
|
55
|
+
},
|
|
56
|
+
"engines": {
|
|
57
|
+
"node": ">=18.13"
|
|
58
|
+
}
|
|
59
|
+
}
|
package/postinstall.js
ADDED
package/scout9-app.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './src/cli';
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import sade from 'sade';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { Scout9Platform } from './platform.js';
|
|
5
|
+
|
|
6
|
+
// const pkg = JSON.parse(fs.readFileSync(new URL('../package.json'), 'utf-8'));
|
|
7
|
+
// const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
|
|
8
|
+
const prog = sade('scout9-auto-reply').version('0.0.1');
|
|
9
|
+
|
|
10
|
+
prog
|
|
11
|
+
.command('build')
|
|
12
|
+
.describe('Builds your scout9 auto reply app')
|
|
13
|
+
.option('--mode', 'Specify a mode for loading environment variables', 'development')
|
|
14
|
+
.action(async ({ mode }) => {
|
|
15
|
+
if (!fs.existsSync('.env')) {
|
|
16
|
+
console.warn(`Missing ${path.resolve('.env')} — skipping`);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
return Scout9Platform.build({cwd: process.cwd(), mode});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
prog
|
|
24
|
+
.command('deploy')
|
|
25
|
+
.describe('Deploy your scout9 auto reply app')
|
|
26
|
+
.option('--mode', 'Specify a mode for loading environment variables', 'development')
|
|
27
|
+
.action(async ({ mode }) => {
|
|
28
|
+
if (!fs.existsSync('.env')) {
|
|
29
|
+
console.warn(`Missing ${path.resolve('.env')} — skipping`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
return Scout9Platform.build({cwd: process.cwd(), mode});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
prog.parse(process.argv, { unknown: (arg) => `Unknown option: ${arg}` });
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { globSync } from 'glob';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
entitiesRootProjectConfigurationSchema,
|
|
5
|
+
entityApiConfigurationSchema,
|
|
6
|
+
entityConfigurationSchema,
|
|
7
|
+
entityRootProjectConfigurationSchema
|
|
8
|
+
} from '../../runtime/index.js';
|
|
9
|
+
import { checkVariableType, requireOptionalProjectFile, requireProjectFile } from '../../utils/index.js';
|
|
10
|
+
|
|
11
|
+
async function loadEntityApiConfig(cwd, filePath) {
|
|
12
|
+
const dir = path.dirname(filePath);
|
|
13
|
+
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);
|
|
19
|
+
if (mod) {
|
|
20
|
+
const config = {};
|
|
21
|
+
const methods = ['GET', 'UPDATE', 'QUERY', 'PUT', 'PATCH', 'DELETE'];
|
|
22
|
+
for (const key in mod) {
|
|
23
|
+
if (methods.includes(key)) {
|
|
24
|
+
config[key] = true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
entityApiConfigurationSchema.parse(config);
|
|
28
|
+
return config;
|
|
29
|
+
} else {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default async function loadEntitiesConfig(
|
|
35
|
+
{cwd = process.cwd(), folder = 'src'} = {}
|
|
36
|
+
) {
|
|
37
|
+
console.log('loadEntitiesConfig', {cwd, folder});
|
|
38
|
+
const config = [];
|
|
39
|
+
const paths = globSync(path.resolve(cwd, `${folder}/entities/**/{index,config,api}.{ts,js}`));
|
|
40
|
+
const data = [];
|
|
41
|
+
for (const path of paths) {
|
|
42
|
+
const segments = path.split('/');
|
|
43
|
+
const srcIndex = segments.findIndex((segment, index) => segment === folder && segments[index + 1] === 'entities');
|
|
44
|
+
const parents = segments.slice(srcIndex + 2, -1).reverse(); // +2 to skip "${folder}" and "entities"
|
|
45
|
+
if (parents.length > 0) {
|
|
46
|
+
const api = await loadEntityApiConfig(cwd, path);
|
|
47
|
+
data.push({path, parents, api});
|
|
48
|
+
} else {
|
|
49
|
+
console.log(`WARNING: "${path}" Is not a valid entity path, must be contained in a named folder under entities/`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const specialEntities = ['agents'];
|
|
54
|
+
|
|
55
|
+
for (const {path, parents, api} of data) {
|
|
56
|
+
let entityConfig = {};
|
|
57
|
+
const fileName = path.split('/')?.pop()?.split('.')?.[0];
|
|
58
|
+
if (!fileName) throw new Error(`Invalid file name "${path}"`);
|
|
59
|
+
const isSpecial = specialEntities.some(se => parents.includes(se));
|
|
60
|
+
if ((fileName === 'index' || fileName === 'config') && !isSpecial) {
|
|
61
|
+
const entityConfigHandler = await requireProjectFile(path).then(mod => mod.default);
|
|
62
|
+
// Check if entityConfig is a function or object
|
|
63
|
+
const entityType = checkVariableType(entityConfigHandler);
|
|
64
|
+
switch (entityType) {
|
|
65
|
+
case 'async function':
|
|
66
|
+
case 'function':
|
|
67
|
+
entityConfig = await entityConfigHandler();
|
|
68
|
+
break;
|
|
69
|
+
case 'object':
|
|
70
|
+
entityConfig = entityConfigHandler;
|
|
71
|
+
break;
|
|
72
|
+
default:
|
|
73
|
+
throw new Error(`Invalid entity type (${entityType}) returned at "${path}"`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Validate entity configuration
|
|
77
|
+
const result = entityConfigurationSchema.safeParse(entityConfig, {path: ['entities', config.length]});
|
|
78
|
+
if (!result.success) {
|
|
79
|
+
throw result.error;
|
|
80
|
+
}
|
|
81
|
+
} else if (isSpecial && (fileName === 'index' || fileName === 'config')) {
|
|
82
|
+
// If this is a special entity file, then ignore as we will capture it another method
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Validate project configuration
|
|
87
|
+
const entityProjectConfig = {
|
|
88
|
+
...entityConfig,
|
|
89
|
+
entity: parents[0],
|
|
90
|
+
entities: parents,
|
|
91
|
+
api
|
|
92
|
+
};
|
|
93
|
+
entityRootProjectConfigurationSchema.parse(entityProjectConfig);
|
|
94
|
+
|
|
95
|
+
config.push(entityProjectConfig);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!config.some(c => c.entity === 'customers')) {
|
|
99
|
+
throw new Error(`Missing required entity: "entities/customers"`);
|
|
100
|
+
}
|
|
101
|
+
if (!config.some(c => c.entity === '[customer]')) {
|
|
102
|
+
throw new Error(`Missing required entity: "entities/customers/[customer]"`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Validate the config
|
|
106
|
+
entitiesRootProjectConfigurationSchema.parse(config);
|
|
107
|
+
|
|
108
|
+
return config;
|
|
109
|
+
}
|
|
110
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { config } from 'dotenv';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import loadAgentConfig from './agents.js';
|
|
5
|
+
import loadEntitiesConfig from './entities.js';
|
|
6
|
+
import loadProjectConfig from './project.js';
|
|
7
|
+
import loadWorkflowsConfig from './workflow.js';
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
export function loadEnvConfig({ cwd = process.cwd()} = {}) {
|
|
11
|
+
const configFilePath = path.resolve(cwd, './.env');
|
|
12
|
+
config({path: configFilePath});
|
|
13
|
+
if (!process.env.SCOUT9_API_KEY) {
|
|
14
|
+
const exists = fs.existsSync(configFilePath);
|
|
15
|
+
if (!exists) {
|
|
16
|
+
throw new Error(`Missing .env file with SCOUT9_API_KEY`);
|
|
17
|
+
} else {
|
|
18
|
+
throw new Error('Missing SCOUT9_API_KEY within .env file');
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export async function loadConfig({ cwd = process.cwd(), folder = 'src'} = {}) {
|
|
23
|
+
// Load globals
|
|
24
|
+
loadEnvConfig({cwd});
|
|
25
|
+
return {
|
|
26
|
+
...await loadProjectConfig({cwd, folder}),
|
|
27
|
+
entities: await loadEntitiesConfig({cwd, folder}),
|
|
28
|
+
agents: await loadAgentConfig({cwd, folder}),
|
|
29
|
+
workflows: await loadWorkflowsConfig({cwd, folder})
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { requireProjectFile } from '../../utils/index.js';
|
|
3
|
+
import { globSync } from 'glob';
|
|
4
|
+
|
|
5
|
+
export default async function loadProjectConfig({cwd = process.cwd(), folder = 'src'} = {}) {
|
|
6
|
+
console.log('loadProjectConfig', {cwd, folder});
|
|
7
|
+
const paths = globSync(path.resolve(cwd, `${folder}/index.{ts,js}`));
|
|
8
|
+
if (paths.length === 0) {
|
|
9
|
+
throw new Error(`Missing main project entry file ${folder}/index.{js|ts}`);
|
|
10
|
+
} else if (paths.length > 1) {
|
|
11
|
+
throw new Error(`Multiple main project entry files found ${folder}/index.{js|ts}`);
|
|
12
|
+
}
|
|
13
|
+
const [filePath] = paths;
|
|
14
|
+
const project = await requireProjectFile(filePath).then(mod => mod.default);
|
|
15
|
+
// @TODO validation type check with zod
|
|
16
|
+
return project;
|
|
17
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { globSync } from 'glob';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
WorkflowConfigurationSchema,
|
|
5
|
+
WorkflowsConfigurationSchema
|
|
6
|
+
} from '../../runtime/index.js';
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
export default async function loadWorkflowsConfig(
|
|
10
|
+
{
|
|
11
|
+
cwd = process.cwd(),
|
|
12
|
+
folder = 'src'
|
|
13
|
+
} = {}
|
|
14
|
+
) {
|
|
15
|
+
console.log('loadWorkflowsConfig', {cwd, folder});
|
|
16
|
+
const config = globSync(path.resolve(cwd, `${folder}/workflows/**/workflow.{ts,js}`))
|
|
17
|
+
.map((path) => {
|
|
18
|
+
const segments = path.split('/');
|
|
19
|
+
const srcIndex = segments.findIndex((segment, index) => segment === folder && segments[index + 1] === 'workflows');
|
|
20
|
+
const parents = segments.slice(srcIndex + 2, -1).reverse(); // +2 to skip "${folder}" and "workflows"
|
|
21
|
+
return {path, parents};
|
|
22
|
+
})
|
|
23
|
+
.filter(path => {
|
|
24
|
+
if (path.parents.length > 0) {
|
|
25
|
+
return true;
|
|
26
|
+
} else {
|
|
27
|
+
console.log(`WARNING: "${path}" Is not a valid entity path, must be contained in a named folder under workflows/`);
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
.map(({path, parents}) => {
|
|
31
|
+
|
|
32
|
+
// Validate project configuration
|
|
33
|
+
const workflowConfig = {
|
|
34
|
+
entity: parents[0],
|
|
35
|
+
entities: parents,
|
|
36
|
+
}
|
|
37
|
+
WorkflowConfigurationSchema.parse(workflowConfig);
|
|
38
|
+
|
|
39
|
+
return workflowConfig;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Validate the config
|
|
43
|
+
WorkflowsConfigurationSchema.parse(config);
|
|
44
|
+
|
|
45
|
+
return config;
|
|
46
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import archiver from 'archiver';
|
|
2
|
+
import { globSync } from 'glob';
|
|
3
|
+
import { exec } from 'node:child_process';
|
|
4
|
+
import fss from 'node:fs';
|
|
5
|
+
import fs from 'node:fs/promises';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import fetch, {FormData} from 'node-fetch';
|
|
8
|
+
import { runInVM } from '../runtime/index.js';
|
|
9
|
+
import { checkVariableType, requireProjectFile } from '../utils/index.js';
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async function runNpmRunBuild({cwd = process.cwd()} = {}) {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
exec('npm run build', {cwd}, (error, stdout, stderr) => {
|
|
16
|
+
if (error) {
|
|
17
|
+
console.error(`Build failed: ${error.message}`);
|
|
18
|
+
return reject(error);
|
|
19
|
+
}
|
|
20
|
+
console.log('Build successful');
|
|
21
|
+
return resolve(undefined);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
function zipDirectory(source, out) {
|
|
28
|
+
const archive = archiver('zip', {zlib: {level: 9}});
|
|
29
|
+
const stream = fss.createWriteStream(out);
|
|
30
|
+
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
archive
|
|
33
|
+
.directory(source, false)
|
|
34
|
+
.on('error', err => reject(err))
|
|
35
|
+
.pipe(stream);
|
|
36
|
+
|
|
37
|
+
stream.on('close', () => resolve(undefined));
|
|
38
|
+
archive.finalize();
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function deployZipDirectory(zipFilePath) {
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
const form = new FormData();
|
|
46
|
+
const blob = new Blob([await fs.readFile(zipFilePath)], {type: 'application/zip'});
|
|
47
|
+
form.set("file", blob, path.basename(zipFilePath), {contentType: 'application/zip'});
|
|
48
|
+
form.set("foo", "bar");
|
|
49
|
+
|
|
50
|
+
// @TODO append signature secret header
|
|
51
|
+
const response = await fetch(`https://pocket-guide.vercel.app/api/b/platform/upload`, {
|
|
52
|
+
method: 'POST',
|
|
53
|
+
body: form,
|
|
54
|
+
headers: {
|
|
55
|
+
'Authorization': process.env.SCOUT9_API_KEY || ''
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
throw new Error(`Server responded with ${response.status}: ${response.statusText}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
console.log('File sent successfully');
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function downloadAndUnpackZip(outputDir) {
|
|
67
|
+
const downloadLocalResponse = await fetch(
|
|
68
|
+
`https://pocket-guide.vercel.app/api/b/platform/download`,
|
|
69
|
+
{
|
|
70
|
+
headers: {
|
|
71
|
+
'Authorization': process.env.SCOUT9_API_KEY || ''
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
if (!downloadLocalResponse.ok) {
|
|
76
|
+
throw new Error(`Error downloading project file ${downloadLocalResponse.statusText}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const buffer = await downloadLocalResponse.arrayBuffer();
|
|
81
|
+
const decompress = require('decompress');
|
|
82
|
+
await decompress(buffer, outputDir + '/build');
|
|
83
|
+
|
|
84
|
+
console.log('Files unpacked successfully at ' + outputDir + '/build');
|
|
85
|
+
|
|
86
|
+
return outputDir + '/build';
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error('Error unpacking file:', error);
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function getApp({cwd = process.cwd(), folder = 'src', ignoreAppRequire = false} = {}) {
|
|
94
|
+
const indexTsPath = path.resolve(cwd, folder, 'index.ts');
|
|
95
|
+
const indexJsPath = path.join(cwd, folder, 'index.js');
|
|
96
|
+
let exe = '';
|
|
97
|
+
if (fss.existsSync(indexTsPath)) {
|
|
98
|
+
exe = path.extname(indexTsPath);
|
|
99
|
+
} else if (fss.existsSync(indexJsPath)) {
|
|
100
|
+
exe = path.extname(indexJsPath);
|
|
101
|
+
} else {
|
|
102
|
+
throw new Error(`Missing main project entry file ${folder}/index.{js|ts}`);
|
|
103
|
+
}
|
|
104
|
+
const filePath = path.resolve(cwd, folder, `app${exe}`);
|
|
105
|
+
let app;
|
|
106
|
+
if (!ignoreAppRequire) {
|
|
107
|
+
app = await requireProjectFile(filePath).then(mod => mod.default);
|
|
108
|
+
const type = checkVariableType(app);
|
|
109
|
+
if (!(type === 'function' || type === 'async function')) {
|
|
110
|
+
throw new Error(`App must return a default function, received "${type}"`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return {app, exe, filePath, fileName: `app${exe}`}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Runs a given project container from scout9 to given environment
|
|
119
|
+
*/
|
|
120
|
+
export async function run(event, {cwd = process.cwd()} = {}) {
|
|
121
|
+
|
|
122
|
+
// @TODO use scout9/admin
|
|
123
|
+
await downloadAndUnpackZip(path.resolve(cwd, 'tmp'));
|
|
124
|
+
|
|
125
|
+
const {filePath, fileName} = await getApp({cwd, folder: 'tmp/build', ignoreAppRequire: true});
|
|
126
|
+
|
|
127
|
+
return runInVM(event, {folder: path.resolve(cwd, 'tmp/build'), filePath, fileName});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Builds a local project
|
|
132
|
+
*/
|
|
133
|
+
export async function build({cwd = process.cwd()} = {}, config) {
|
|
134
|
+
// 1. Lint: Run validation checks
|
|
135
|
+
|
|
136
|
+
// Check if app looks good
|
|
137
|
+
await getApp({cwd, folder: 'src'});
|
|
138
|
+
|
|
139
|
+
// Check if workflows look good
|
|
140
|
+
console.log('@TODO check if workflows are properly written');
|
|
141
|
+
|
|
142
|
+
// 2. Build code in user's project
|
|
143
|
+
await runNpmRunBuild({cwd});
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
// 3. Remove unnecessary files
|
|
147
|
+
const files = globSync(path.resolve(cwd, 'build/**/*(*.test.*|*.spec.*)'))
|
|
148
|
+
for (const file of files) {
|
|
149
|
+
await fs.unlink(file);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 3. Run tests
|
|
153
|
+
// console.log('@TODO run tests');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Deploys a local project to scout9
|
|
158
|
+
*/
|
|
159
|
+
export async function deploy({cwd = process.cwd()}) {
|
|
160
|
+
const zipFilePath = path.join(cwd, 'build.zip');
|
|
161
|
+
await zipDirectory(path.resolve(cwd, 'build'), zipFilePath);
|
|
162
|
+
|
|
163
|
+
console.log('Project zipped successfully.');
|
|
164
|
+
|
|
165
|
+
const response = await deployZipDirectory(zipFilePath);
|
|
166
|
+
console.log('Response from Firebase Function:', response);
|
|
167
|
+
}
|
package/src/exports.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Scout9Platform } from './platform.js';
|
|
2
|
+
import { EventResponse } from './runtime/index.js';
|
|
3
|
+
|
|
4
|
+
export { EventResponse } from './runtime/index.js';
|
|
5
|
+
// export * from './types';
|
|
6
|
+
export * from './testing-tools/index.js';
|
|
7
|
+
|
|
8
|
+
export async function run(
|
|
9
|
+
event,
|
|
10
|
+
{cwd = process.cwd()} = {},
|
|
11
|
+
) {
|
|
12
|
+
return Scout9Platform.run(event, {cwd})
|
|
13
|
+
}
|
|
14
|
+
export async function sendEvent(
|
|
15
|
+
event,
|
|
16
|
+
{cwd = process.cwd()} = {},
|
|
17
|
+
) {
|
|
18
|
+
return Scout9Platform.run(event, {cwd})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param data {T}
|
|
23
|
+
* @param init {ResponseInit | undefined}
|
|
24
|
+
* @returns {EventResponse<T>}
|
|
25
|
+
*/
|
|
26
|
+
export function json(data, init) {
|
|
27
|
+
// TODO deprecate this in favour of `Response.json` when it's
|
|
28
|
+
// more widely supported
|
|
29
|
+
const body = JSON.stringify(data);
|
|
30
|
+
|
|
31
|
+
// we can't just do `text(JSON.stringify(data), init)` because
|
|
32
|
+
// it will set a default `content-type` header. duplicated code
|
|
33
|
+
// means less duplicated work
|
|
34
|
+
const headers = new Headers(init?.headers);
|
|
35
|
+
if (!headers.has('content-length')) {
|
|
36
|
+
headers.set('content-length', encoder.encode(body).byteLength.toString());
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!headers.has('content-type')) {
|
|
40
|
+
headers.set('content-type', 'application/json');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return new EventResponse(data, {
|
|
44
|
+
...init,
|
|
45
|
+
headers
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const encoder = new TextEncoder();
|
package/src/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './exports.js';
|
package/src/platform.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import colors from 'kleur';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { build as _build, deploy as _deploy, run as _run } from './core/index.js';
|
|
4
|
+
import { loadConfig, loadEnvConfig } from './core/config/index.js';
|
|
5
|
+
import { coalesceToError } from './utils/index.js';
|
|
6
|
+
|
|
7
|
+
export const Scout9Platform = {
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Builds & Deploys the project
|
|
11
|
+
*/
|
|
12
|
+
deploy: async function ({cwd = process.cwd()} = {}) {
|
|
13
|
+
try {
|
|
14
|
+
const config = await loadConfig({cwd, folder: 'src'});
|
|
15
|
+
await _build({cwd}, config);
|
|
16
|
+
await _deploy({cwd});
|
|
17
|
+
return config;
|
|
18
|
+
} catch (e) {
|
|
19
|
+
this.handleError(e);
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Builds the project
|
|
25
|
+
*/
|
|
26
|
+
build: async function({cwd = process.cwd(), mode = 'development'} = {}) {
|
|
27
|
+
try {
|
|
28
|
+
const config = await loadConfig({cwd, folder: 'src'});
|
|
29
|
+
await _build({cwd}, config);
|
|
30
|
+
return config;
|
|
31
|
+
} catch (e) {
|
|
32
|
+
this.handleError(e);
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Runs the project in a container
|
|
38
|
+
*/
|
|
39
|
+
run: async function (
|
|
40
|
+
event,
|
|
41
|
+
{cwd = process.cwd(), mode = 'remote'}
|
|
42
|
+
) {
|
|
43
|
+
if (mode !== 'remote') {
|
|
44
|
+
throw new Error(`Unimplemented mode "${mode}"`);
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
loadEnvConfig({cwd});
|
|
48
|
+
return _run(event, {cwd});
|
|
49
|
+
} catch (e) {
|
|
50
|
+
this.handleError(e);
|
|
51
|
+
throw e;
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
handleError: function (e) {
|
|
55
|
+
const error = coalesceToError(e);
|
|
56
|
+
|
|
57
|
+
if (error.name === 'SyntaxError') throw error;
|
|
58
|
+
|
|
59
|
+
console.error(colors.bold().red(`> ${error.message}`));
|
|
60
|
+
if (error instanceof z.ZodError) {
|
|
61
|
+
console.error(error.issues.map(i => colors.red(`${colors.bold(`\tZod Error (${i.code}): `)}"${i.message}" ${JSON.stringify(i.path)}`)).join('\n'));
|
|
62
|
+
console.error(colors.gray(JSON.stringify(error.format(), null, 2)));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (error.stack) {
|
|
66
|
+
console.error(colors.gray(error.stack.split('\n').slice(0).join('\n')));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { zId } from './utils.js';
|
|
3
|
+
import { MessageSchema } from './message.js';
|
|
4
|
+
|
|
5
|
+
export const customerValueSchema = z.union([z.boolean(), z.number(), z.string()]);
|
|
6
|
+
|
|
7
|
+
export const customerSchema = z.object({
|
|
8
|
+
firstName: z.string().optional(),
|
|
9
|
+
lastName: z.string().optional(),
|
|
10
|
+
name: z.string(),
|
|
11
|
+
email: z.string().nullable().optional(),
|
|
12
|
+
phone: z.string().nullable().optional(),
|
|
13
|
+
img: z.string().nullable().optional(),
|
|
14
|
+
neighborhood: z.string().nullable().optional(),
|
|
15
|
+
city: z.string().nullable().optional(),
|
|
16
|
+
country: z.string().nullable().optional(),
|
|
17
|
+
line1: z.string().nullable().optional(),
|
|
18
|
+
line2: z.string().nullable().optional(),
|
|
19
|
+
postal_code: z.string().nullable().optional(),
|
|
20
|
+
state: z.string().nullable().optional(),
|
|
21
|
+
town: z.string().nullable().optional(),
|
|
22
|
+
joined: z.string().nullable().optional(),
|
|
23
|
+
stripe: z.string().nullable().optional(),
|
|
24
|
+
stripeDev: z.string().nullable().optional()
|
|
25
|
+
}).catchall(customerValueSchema);
|
|
26
|
+
|
|
27
|
+
export const agentConfigurationSchema = z.object({
|
|
28
|
+
id: zId('Agent ID', z.string({description: 'Unique ID for agent'})),
|
|
29
|
+
firstName: z.string({description: 'Agent first name'}).optional(),
|
|
30
|
+
lastName: z.string({description: 'Agent last name'}).optional(),
|
|
31
|
+
inactive: z.boolean({description: 'Agent is inactive'}).optional(),
|
|
32
|
+
programmablePhoneNumber: z.string({description: 'Programmable phone number'}).optional(),
|
|
33
|
+
programmablePhoneNumberSid: z.string({description: 'Programmable phone number SID'}).optional(),
|
|
34
|
+
programmableEmail: z.string({description: 'Email address from Scout9 gmail subdomain'}).optional(),
|
|
35
|
+
forwardEmail: z.string({description: 'Email address to forward to'}).optional(),
|
|
36
|
+
forwardPhone: z.string({description: 'Phone number to forward to'}).optional(),
|
|
37
|
+
title: z.string({description: 'Agent title '}).optional().default('Agent'),
|
|
38
|
+
context: z.string({description: 'Context of the agent'}).optional().default('You represent the agent when they are away'),
|
|
39
|
+
includedLocations: z.array(z.string({description: 'Locations the agent is included in'})).optional(),
|
|
40
|
+
excludedLocations: z.array(z.string({description: 'Locations the agent is excluded from'})).optional(),
|
|
41
|
+
model: z.enum(['Scout9', 'bard', 'openai']).optional().default('openai'),
|
|
42
|
+
transcripts: z.array(z.array(MessageSchema)).optional(),
|
|
43
|
+
audioRef: z.array(z.any()).optional()
|
|
44
|
+
});
|