├── test ├── scripts │ ├── compile.spec.ts │ ├── helper.spec.ts │ └── tester.spec.ts ├── users │ └── team.spec.ts └── bots │ ├── deployment.spec.ts │ └── bot.spec.ts ├── components ├── api │ ├── zaun.ts │ └── api.ts ├── deprecated │ └── deprecated.ts ├── scripts │ ├── compile.ts │ ├── tester.ts │ └── helper.ts ├── bots │ ├── session.ts │ ├── environment.ts │ ├── deployment.ts │ └── channel.ts ├── main.ts ├── users │ └── team.ts └── projects │ └── project.ts ├── lib ├── interfaces │ └── main.js └── components │ ├── api │ ├── zaun.js │ └── api.js │ ├── deprecated │ └── deprecated.js │ ├── scripts │ ├── compile.js │ ├── tester.js │ └── helper.js │ ├── bots │ ├── session.js │ ├── deployment.js │ ├── environment.js │ └── channel.js │ ├── main.js │ ├── users │ └── team.js │ └── projects │ └── project.js ├── .gitignore ├── start.js ├── bin ├── kata.js └── coverage.sh ├── index.js ├── tsconfig.json ├── tslint.json ├── .eslintrc.json ├── bitbucket-pipelines.yml ├── package.json ├── CHANGELOG.md └── interfaces └── main.ts /test/scripts/compile.spec.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/scripts/helper.spec.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/scripts/tester.spec.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/api/zaun.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Component } from "merapi"; 3 | 4 | export default function() { 5 | return require("zaun"); 6 | } 7 | -------------------------------------------------------------------------------- /lib/interfaces/main.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | //# sourceMappingURL=main.js.map -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | coverage 4 | typings 5 | .nyc_output 6 | *.map 7 | .env 8 | .DS_Store 9 | .idea 10 | *.markdownlint* -------------------------------------------------------------------------------- /start.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | let boot = require("./index.js"); 4 | 5 | try { 6 | boot.start(); 7 | } 8 | catch (e) { 9 | console.log(e.stack); 10 | process.exit(); 11 | } -------------------------------------------------------------------------------- /bin/kata.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | "use strict"; 3 | 4 | let boot = require("../index.js"); 5 | 6 | try { 7 | boot.start(); 8 | } 9 | catch (e) { 10 | console.log(e.stack); 11 | process.exit(); 12 | } -------------------------------------------------------------------------------- /lib/components/api/zaun.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | function default_1() { 4 | return require("zaun"); 5 | } 6 | exports.default = default_1; 7 | //# sourceMappingURL=zaun.js.map -------------------------------------------------------------------------------- /bin/coverage.sh: -------------------------------------------------------------------------------- 1 | #!bin/bash 2 | 3 | NODE_ENV=test nyc npm t 4 | NODE_ENV=test nyc report --reporter=text-summary 5 | NODE_ENV=test nyc report --reporter=lcov 6 | cat ./coverage/lcov.info | ./node_modules/.bin/codacy-coverage && rm -rf ./coverage -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const path = require("path"); 3 | const merapi = require("merapi"); 4 | const yaml = require("js-yaml"); 5 | const fs = require("fs"); 6 | const config = yaml.safeLoad(fs.readFileSync(`${__dirname}/service.yml`, "utf8")); 7 | config.package = require(`${__dirname}/package`); 8 | 9 | module.exports = merapi({ 10 | basepath: path.resolve(__dirname, "lib"), 11 | config: config, 12 | delimiters: {left: "${", right: "}"} 13 | }); 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | "target": "es6", 5 | "module": "commonjs", 6 | "baseUrl": "./", 7 | "sourceMap": true, 8 | "sourceRoot": "./", 9 | "noImplicitAny": true, 10 | "experimentalDecorators": true, 11 | "emitDecoratorMetadata": true, 12 | "outDir": "./lib" 13 | }, 14 | 15 | "include": [ 16 | "components/**/*.ts", 17 | "components/*.ts", 18 | "interfaces/*.ts" 19 | ], 20 | 21 | "exclude": [ 22 | "node_modules" 23 | ] 24 | } -------------------------------------------------------------------------------- /components/deprecated/deprecated.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "merapi"; 2 | 3 | export default class Deprecated extends Component { 4 | public addChannel() { 5 | console.log("This command is deprecated, please use create-channel"); 6 | } 7 | 8 | public removeChannel() { 9 | console.log("This command is deprecated, please use delete-channel"); 10 | } 11 | 12 | public configView() { 13 | console.log("This command is deprecated, please use view-config"); 14 | } 15 | 16 | public addMember() { 17 | console.log("This command is deprecated, please use invite-member"); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "member-ordering": [false], 9 | "align": [false], 10 | "object-literal-sort-keys": [false], 11 | "new-parens": true, 12 | "no-arg": true, 13 | "no-bitwise": true, 14 | "array-type": [false], 15 | "trailing-comma": [false], 16 | "ordered-imports": [false], 17 | "no-trailing-whitespace": [false], 18 | "no-conditional-assignment": true, 19 | "no-consecutive-blank-lines": false, 20 | "no-console": [false] 21 | }, 22 | "rulesDirectory": [] 23 | } -------------------------------------------------------------------------------- /lib/components/deprecated/deprecated.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const merapi_1 = require("merapi"); 4 | class Deprecated extends merapi_1.Component { 5 | addChannel() { 6 | console.log("This command is deprecated, please use create-channel"); 7 | } 8 | removeChannel() { 9 | console.log("This command is deprecated, please use delete-channel"); 10 | } 11 | configView() { 12 | console.log("This command is deprecated, please use view-config"); 13 | } 14 | addMember() { 15 | console.log("This command is deprecated, please use invite-member"); 16 | } 17 | } 18 | exports.default = Deprecated; 19 | //# sourceMappingURL=deprecated.js.map -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "node": true, 6 | "es6": true, 7 | "mocha": true 8 | }, 9 | "extends": "eslint:recommended", 10 | "parserOptions": { 11 | "sourceType": "module", 12 | "ecmaVersion": 6 13 | }, 14 | "rules": { 15 | "indent": [ 16 | "warn", 17 | 4 18 | ], 19 | "linebreak-style": [ 20 | "error", 21 | "unix" 22 | ], 23 | "quotes": [ 24 | "warn", 25 | "double" 26 | ], 27 | "semi": [ 28 | "warn", 29 | "always" 30 | ], 31 | "no-unused-vars": [ 32 | "warn" 33 | ], 34 | "no-var": 2, 35 | "require-yield": [ 36 | 0 37 | ], 38 | "no-cond-assign": [ 39 | 0 40 | ] 41 | } 42 | } -------------------------------------------------------------------------------- /bitbucket-pipelines.yml: -------------------------------------------------------------------------------- 1 | image: node:6.9.4 2 | 3 | pipelines: 4 | 5 | default: 6 | - step: 7 | caches: 8 | - node 9 | script: 10 | - curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 0.18.1 11 | - export PATH=$HOME/.yarn/bin:$PATH 12 | - yarn 13 | - npm run cover 14 | 15 | tags: 16 | '*': 17 | - step: 18 | caches: 19 | - node 20 | script: 21 | - curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 0.18.1 22 | - export PATH=$HOME/.yarn/bin:$PATH 23 | - yarn 24 | - npm run cover 25 | 26 | branches: 27 | develop: 28 | - step: 29 | caches: 30 | - node 31 | script: 32 | - curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 0.18.1 33 | - export PATH=$HOME/.yarn/bin:$PATH 34 | - yarn 35 | - npm run cover 36 | 37 | options: 38 | docker: true -------------------------------------------------------------------------------- /lib/components/api/api.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const merapi_1 = require("merapi"); 4 | class Api extends merapi_1.Component { 5 | constructor(helper, zaun) { 6 | super(); 7 | this.helper = helper; 8 | this.zaun = zaun; 9 | this.apiClient = this.zaun.ApiClient.instance; 10 | let basePath = this.helper.getProp("zaunUrl") || "https://api.kata.ai"; 11 | this.apiClient.basePath = basePath; 12 | this.bearer = this.apiClient.authentications.Bearer; 13 | const currentLogin = this.helper.getProp("current_login") || "user"; 14 | const tokenObj = this.helper.getProp("token") || {}; 15 | this.bearer.apiKey = `Bearer ${tokenObj[currentLogin]}`; 16 | this.botApi = new this.zaun.BotApi(); 17 | this.authApi = new this.zaun.AuthApi(); 18 | this.userApi = new this.zaun.UserApi(); 19 | this.teamApi = new this.zaun.TeamApi(); 20 | this.deploymentApi = new this.zaun.DeploymentApi(); 21 | this.projectApi = new zaun.ProjectApi(); 22 | // this.draftApi = new this.zaun.DraftApi(); 23 | // this.channelApi = new this.zaun.ChannelApi(); 24 | // this.sessionApi = new this.zaun.SessionApi(); 25 | // this.cachesApi = new this.zaun.CacheApi(); 26 | // this.utilApi = new this.zaun.UtilApi(); 27 | this.nluApi = new this.zaun.NluApi(); 28 | } 29 | } 30 | exports.default = Api; 31 | //# sourceMappingURL=api.js.map -------------------------------------------------------------------------------- /components/scripts/compile.ts: -------------------------------------------------------------------------------- 1 | 2 | import { IConfig, IHash, Component } from "merapi"; 3 | import { ICompile, IHelper } from "interfaces/main"; 4 | 5 | const { Config } = require("merapi"); 6 | const path = require("path"); 7 | const fs = require("fs"); 8 | const yaml = require("js-yaml"); 9 | 10 | export default class Compile extends Component implements ICompile { 11 | private directives : any = { 12 | include(file: string, basepath: string, helper: IHelper) { 13 | let ext = path.extname(file); 14 | if (ext === ".yml" || ext === ".yaml") 15 | return helper.loadYaml(path.resolve(basepath, file)); 16 | else if(ext === ".json") 17 | return require(path.resolve(basepath, file)); 18 | else 19 | return fs.readFileSync(path.resolve(basepath, file)).toString(); 20 | } 21 | }; 22 | 23 | constructor(private helper: IHelper) { 24 | super(); 25 | } 26 | 27 | execDirective(name : string, directive: any, dict : IHash, basepath : string) { 28 | for (let i in dict) { 29 | if (typeof dict[i] !== "string") 30 | continue; 31 | let val = dict[i].trim(); 32 | if (val.indexOf("$"+name+"(") === 0 && val.charAt(val.length-1) === ")") { 33 | dict[i] = directive(dict[i].substring(2+name.length, dict[i].length-1), basepath, this.helper); 34 | } 35 | } 36 | } 37 | 38 | execDirectives(config : IConfig, basepath : string) : IConfig { 39 | let flattened = config.flatten(); 40 | for (let i in this.directives) { 41 | this.execDirective(i, this.directives[i], flattened, basepath); 42 | } 43 | return config.create(flattened); 44 | } 45 | } -------------------------------------------------------------------------------- /lib/components/scripts/compile.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const merapi_1 = require("merapi"); 4 | const { Config } = require("merapi"); 5 | const path = require("path"); 6 | const fs = require("fs"); 7 | const yaml = require("js-yaml"); 8 | class Compile extends merapi_1.Component { 9 | constructor(helper) { 10 | super(); 11 | this.helper = helper; 12 | this.directives = { 13 | include(file, basepath, helper) { 14 | let ext = path.extname(file); 15 | if (ext === ".yml" || ext === ".yaml") 16 | return helper.loadYaml(path.resolve(basepath, file)); 17 | else if (ext === ".json") 18 | return require(path.resolve(basepath, file)); 19 | else 20 | return fs.readFileSync(path.resolve(basepath, file)).toString(); 21 | } 22 | }; 23 | } 24 | execDirective(name, directive, dict, basepath) { 25 | for (let i in dict) { 26 | if (typeof dict[i] !== "string") 27 | continue; 28 | let val = dict[i].trim(); 29 | if (val.indexOf("$" + name + "(") === 0 && val.charAt(val.length - 1) === ")") { 30 | dict[i] = directive(dict[i].substring(2 + name.length, dict[i].length - 1), basepath, this.helper); 31 | } 32 | } 33 | } 34 | execDirectives(config, basepath) { 35 | let flattened = config.flatten(); 36 | for (let i in this.directives) { 37 | this.execDirective(i, this.directives[i], flattened, basepath); 38 | } 39 | return config.create(flattened); 40 | } 41 | } 42 | exports.default = Compile; 43 | //# sourceMappingURL=compile.js.map -------------------------------------------------------------------------------- /components/api/api.ts: -------------------------------------------------------------------------------- 1 | 2 | import { JsonObject, Component } from "merapi"; 3 | import { IHelper } from "interfaces/main"; 4 | 5 | export default class Api extends Component { 6 | private draftApi: any; 7 | private apiClient: any; 8 | private bearer: any; 9 | private botApi: any; 10 | private authApi: any; 11 | private userApi: any; 12 | private teamApi: any; 13 | public deploymentApi: any; 14 | private sessionApi: any; 15 | private cachesApi: any; 16 | private utilApi: any; 17 | public nluApi: any; 18 | public projectApi: any; 19 | 20 | constructor( 21 | private helper: IHelper, 22 | private zaun: any 23 | ) { 24 | super(); 25 | 26 | this.apiClient = this.zaun.ApiClient.instance; 27 | let basePath = this.helper.getProp("zaunUrl") || "https://api.kata.ai"; 28 | 29 | this.apiClient.basePath = basePath; 30 | this.bearer = this.apiClient.authentications.Bearer; 31 | const currentLogin = this.helper.getProp("current_login") as string || "user"; 32 | const tokenObj = this.helper.getProp("token") as JsonObject || {}; 33 | this.bearer.apiKey = `Bearer ${tokenObj[currentLogin]}`; 34 | 35 | this.botApi = new this.zaun.BotApi(); 36 | this.authApi = new this.zaun.AuthApi(); 37 | this.userApi = new this.zaun.UserApi(); 38 | this.teamApi = new this.zaun.TeamApi(); 39 | this.deploymentApi = new this.zaun.DeploymentApi(); 40 | this.projectApi = new zaun.ProjectApi(); 41 | // this.draftApi = new this.zaun.DraftApi(); 42 | // this.channelApi = new this.zaun.ChannelApi(); 43 | // this.sessionApi = new this.zaun.SessionApi(); 44 | // this.cachesApi = new this.zaun.CacheApi(); 45 | // this.utilApi = new this.zaun.UtilApi(); 46 | this.nluApi = new this.zaun.NluApi(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kata-cli", 3 | "version": "2.7.2", 4 | "description": "Kata AI Command Line Tools", 5 | "main": "index.js", 6 | "bin": { 7 | "kata": "./bin/kata.js" 8 | }, 9 | "directories": { 10 | "test": "test" 11 | }, 12 | "scripts": { 13 | "clean": "rm -rf ./lib", 14 | "build": "tsc", 15 | "test": "NODE_ENV=test mocha --recursive --compilers ts:ts-node/register --bail", 16 | "cover": "sh bin/coverage.sh" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/kata-ai/kata-cli.git" 21 | }, 22 | "keywords": [ 23 | "cli" 24 | ], 25 | "nyc": { 26 | "include": [ 27 | "components/**/*.ts", 28 | "components/*.ts", 29 | "interfaces/*.ts" 30 | ], 31 | "exclude": [ 32 | "node_modules" 33 | ], 34 | "extension": [ 35 | ".ts" 36 | ], 37 | "require": [ 38 | "ts-node/register" 39 | ], 40 | "reporter": [ 41 | "json", 42 | "html" 43 | ], 44 | "all": true 45 | }, 46 | "author": "Febry Antonius, Yoga Aliarham", 47 | "license": "ISC", 48 | "homepage": "https://github.com/kata-ai/kata-cli#readme", 49 | "dependencies": { 50 | "catch-decorator": "^2.0.0", 51 | "cli-table": "^0.3.1", 52 | "commander": "^2.20.3", 53 | "deasync": "^0.1.16", 54 | "inquirer": "^3.0.6", 55 | "js-yaml": "^3.12.0", 56 | "lodash": "^4.17.15", 57 | "merapi": "^0.17.1", 58 | "merapi-proxy": "^0.1.7", 59 | "universal-analytics": "^0.4.20", 60 | "uuid": "^3.3.3", 61 | "zaun": "2.7.1" 62 | }, 63 | "devDependencies": { 64 | "@types/inquirer": "0.0.43", 65 | "@types/js-yaml": "^3.9.0", 66 | "@types/lodash": "^4.14.149", 67 | "@types/node": "^7.10.9", 68 | "@types/sinon": "^2.3.3", 69 | "@types/uuid": "^3.4.6", 70 | "codacy-coverage": "^2.1.1", 71 | "mocha": "^5.2.0", 72 | "mocha-lcov-reporter": "^1.3.0", 73 | "mocha-typescript": "^1.1.17", 74 | "nyc": "^11.9.0", 75 | "sinon": "^7.5.0", 76 | "ts-node": "^3.3.0", 77 | "tslint": "^5.20.1", 78 | "typescript": "^3.7.2" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /components/bots/session.ts: -------------------------------------------------------------------------------- 1 | 2 | import { IHelper } from "interfaces/main"; 3 | import { Component, JsonObject } from "merapi"; 4 | 5 | export default class Session extends Component { 6 | private defaultDeploymentId = "f223c9e0-6ba1-434d-8313-a9f18ca364bd"; 7 | 8 | constructor(private helper : IHelper, private api : any) { 9 | super(); 10 | } 11 | 12 | public async get(id : string, deploymentId : string, options : JsonObject) { 13 | const botId = this.helper.getBotId(); 14 | 15 | try { 16 | deploymentId = deploymentId || this.defaultDeploymentId; 17 | const {data} = await this.helper.toPromise(this.api.sessionApi, this.api.sessionApi.botsBotIdDeploymentsDeploymentIdSessionsSessionIdGet, botId, deploymentId, id, ""); 18 | 19 | console.dir(data, { depth: null }); 20 | } catch (e) { 21 | console.log(this.helper.wrapError(e)); 22 | } 23 | } 24 | 25 | public async create(id : string, deploymentId : string, options : JsonObject) { 26 | const botId = this.helper.getBotId(); 27 | 28 | try { 29 | let session : JsonObject; 30 | 31 | if (options.data) { 32 | session = JSON.parse(options.data as string); 33 | } else if (options.file) { 34 | session = this.helper.loadYamlOrJsonFile(options.file as string); 35 | } 36 | 37 | if (id && !session.id) { 38 | session.id = id; 39 | } 40 | 41 | deploymentId = deploymentId || this.defaultDeploymentId; 42 | 43 | const {data} = await this.helper.toPromise(this.api.sessionApi, this.api.sessionApi.botsBotIdDeploymentsDeploymentIdSessionsPost, botId, deploymentId, session); 44 | 45 | console.log(data); 46 | console.log("Session created successfully"); 47 | } catch (e) { 48 | console.log(this.helper.wrapError(e)); 49 | } 50 | } 51 | 52 | public async update(id : string, deploymentId : string, options : JsonObject) { 53 | const botId = this.helper.getBotId(); 54 | 55 | try { 56 | let session : JsonObject; 57 | 58 | if (options.data) { 59 | session = JSON.parse(options.data as string); 60 | } else if (options.file) { 61 | session = this.helper.loadYamlOrJsonFile(options.file as string); 62 | } 63 | 64 | if (!session.id) { 65 | session.id = id; 66 | } 67 | 68 | deploymentId = deploymentId || this.defaultDeploymentId; 69 | 70 | const {data} = await this.helper.toPromise(this.api.sessionApi, this.api.sessionApi.botsBotIdDeploymentsDeploymentIdSessionsSessionIdPut, botId, deploymentId, id, session); 71 | 72 | console.log(data); 73 | console.log("Session updated successfully"); 74 | } catch (e) { 75 | console.log(this.helper.wrapError(e)); 76 | } 77 | } 78 | 79 | public async delete(id : string, deploymentId : string, options : JsonObject) { 80 | const botId = this.helper.getBotId(); 81 | 82 | try { 83 | deploymentId = deploymentId || this.defaultDeploymentId; 84 | const {data} = await this.helper.toPromise(this.api.sessionApi, this.api.sessionApi.botsBotIdDeploymentsDeploymentIdSessionsSessionIdDelete, botId, deploymentId, id); 85 | 86 | console.dir(data, { depth: null }); 87 | console.log("Session deleted successfully"); 88 | } catch (e) { 89 | console.log(this.helper.wrapError(e)); 90 | } 91 | } 92 | 93 | public async timestamp() { 94 | const { response } = await this.helper.toPromise(this.api.utilApi, this.api.utilApi.timestampGet); 95 | 96 | console.log(`Current server timestamp: ${response.text}`); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /components/main.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandDescriptor, CommandList, IHelper } from "interfaces/main"; 2 | import { Component, IConfigReader, IInjector, Json, JsonObject } from "merapi"; 3 | 4 | const commander = require("commander"); 5 | 6 | export default class Main extends Component { 7 | 8 | constructor( 9 | private config : IConfigReader, 10 | private injector : IInjector, 11 | private helper : IHelper 12 | ) { 13 | super(); 14 | } 15 | 16 | async start(argv:string[]) { 17 | let commands = this.config.get("commands"); 18 | commander.version(`Kata CLI version ${this.config.default("version", "1.0.0")}`); 19 | await this.compile(commands, commander); 20 | commander.parse(argv); 21 | const validCommands = commander.commands.map((x:any) => x.name()); 22 | if (argv.length === 2 || validCommands.indexOf(argv[2]) === -1) { 23 | commander.parse([argv[0], argv[1], '-h']); 24 | } 25 | 26 | this.sendNotificationTracking() 27 | this.sendDataAnalytics(argv) 28 | this.saveCommandSession(argv) 29 | } 30 | 31 | async compile(commands: CommandList, program: Command, currKey : string = "") { 32 | for (let key in commands) { 33 | let command = commands[key]; 34 | 35 | if (command.type === "group") { 36 | await this.compileGroup(`${currKey}${key}`, command, program); 37 | } 38 | else if (command.type === "alias") { 39 | this.compileAlias(key, command, program); 40 | } 41 | else { 42 | await this.compileCommand(`${currKey}${key}`, command, program); 43 | } 44 | } 45 | } 46 | 47 | async compileGroup(key: string, command: CommandDescriptor, program: Command) { 48 | await this.compile(command.subcommands, program, `${key}-`); 49 | } 50 | 51 | compileAlias(key: string, command: CommandDescriptor, program: Command) { 52 | program.command(key).action((self) => { 53 | let args = self._args; 54 | commander.parse(command.alias.split(/\s+/).concat(args)); 55 | }); 56 | } 57 | 58 | async compileCommand(key: string, command: CommandDescriptor, program: Command) { 59 | let subcommand; 60 | let commandKey = command.alias ? command.alias : key; 61 | 62 | if (command.args) { 63 | subcommand = program.command(`${commandKey} ${command.args}`) 64 | } 65 | else { 66 | subcommand = program.command(commandKey); 67 | } 68 | 69 | if (command.params) { 70 | for (let i in command.params) { 71 | let param = command.params[i]; 72 | let flag = param.short ? `-${param.short}, --${i}` : `--${i}`; 73 | if (param.value !== undefined) 74 | subcommand.option(`${flag} `, param.desc || "", param.value); 75 | else if(param.bool) 76 | subcommand.option(flag, param.desc || ""); 77 | else 78 | subcommand.option(`${flag} [value]`, param.desc || ""); 79 | } 80 | } 81 | 82 | subcommand.action(await this.createAction(command.handler, command.middleware)); 83 | } 84 | 85 | async createAction(handler:string, middleware : string[] = []) : Promise<(...args:any[]) => void> { 86 | let methods : any[] = []; 87 | for (let i=0; i { 93 | for (let i=0; i x.name()); 28 | if (argv.length === 2 || validCommands.indexOf(argv[2]) === -1) { 29 | commander.parse([argv[0], argv[1], '-h']); 30 | } 31 | this.sendNotificationTracking(); 32 | this.sendDataAnalytics(argv); 33 | this.saveCommandSession(argv); 34 | }); 35 | } 36 | compile(commands, program, currKey = "") { 37 | return __awaiter(this, void 0, void 0, function* () { 38 | for (let key in commands) { 39 | let command = commands[key]; 40 | if (command.type === "group") { 41 | yield this.compileGroup(`${currKey}${key}`, command, program); 42 | } 43 | else if (command.type === "alias") { 44 | this.compileAlias(key, command, program); 45 | } 46 | else { 47 | yield this.compileCommand(`${currKey}${key}`, command, program); 48 | } 49 | } 50 | }); 51 | } 52 | compileGroup(key, command, program) { 53 | return __awaiter(this, void 0, void 0, function* () { 54 | yield this.compile(command.subcommands, program, `${key}-`); 55 | }); 56 | } 57 | compileAlias(key, command, program) { 58 | program.command(key).action((self) => { 59 | let args = self._args; 60 | commander.parse(command.alias.split(/\s+/).concat(args)); 61 | }); 62 | } 63 | compileCommand(key, command, program) { 64 | return __awaiter(this, void 0, void 0, function* () { 65 | let subcommand; 66 | let commandKey = command.alias ? command.alias : key; 67 | if (command.args) { 68 | subcommand = program.command(`${commandKey} ${command.args}`); 69 | } 70 | else { 71 | subcommand = program.command(commandKey); 72 | } 73 | if (command.params) { 74 | for (let i in command.params) { 75 | let param = command.params[i]; 76 | let flag = param.short ? `-${param.short}, --${i}` : `--${i}`; 77 | if (param.value !== undefined) 78 | subcommand.option(`${flag} `, param.desc || "", param.value); 79 | else if (param.bool) 80 | subcommand.option(flag, param.desc || ""); 81 | else 82 | subcommand.option(`${flag} [value]`, param.desc || ""); 83 | } 84 | } 85 | subcommand.action(yield this.createAction(command.handler, command.middleware)); 86 | }); 87 | } 88 | createAction(handler, middleware = []) { 89 | return __awaiter(this, void 0, void 0, function* () { 90 | let methods = []; 91 | for (let i = 0; i < middleware.length; i++) { 92 | methods.push(yield this.injector.resolveMethod(middleware[i])); 93 | } 94 | let handlerMethod = yield this.injector.resolveMethod(handler); 95 | return (...args) => { 96 | for (let i = 0; i < methods.length; i++) 97 | args = methods[i](...args); 98 | handlerMethod(...args); 99 | }; 100 | }); 101 | } 102 | sendDataAnalytics(argv) { 103 | const command = Object.assign([], argv); 104 | this.helper.sendGoogleAnalytics("track", argv[2], command.splice(2).join(" ")); 105 | } 106 | sendNotificationTracking() { 107 | const status = this.helper.checkNotificationStatus(); 108 | if (!status) { 109 | console.log("\n[NOTICE]" + 110 | "\nStarting from Kata CLI v2.1.0, " + 111 | "we added analytics that tracks your CLI usage to improve user experience. " + 112 | "Please contact support@kata.ai if you have any questions.\n"); 113 | } 114 | } 115 | saveCommandSession(argv) { 116 | const command = Object.assign([], argv); 117 | this.helper.addCommandSession(command.splice(2).join(" ")); 118 | } 119 | } 120 | exports.default = Main; 121 | //# sourceMappingURL=main.js.map -------------------------------------------------------------------------------- /components/users/team.ts: -------------------------------------------------------------------------------- 1 | import { IHelper } from "interfaces/main"; 2 | import { Component, JsonObject } from "merapi"; 3 | 4 | export default class Team extends Component { 5 | 6 | constructor(private helper: IHelper, private api: any) { 7 | super(); 8 | } 9 | 10 | public async addMember(username: string, options?: JsonObject) { 11 | try { 12 | const role = options.admin ? "teamAdmin" : "teamMember"; 13 | let firstLogin = this.helper.getProp("first_login") as JsonObject; 14 | 15 | const { userInfo, teamInfo, teamMember, currentLogin } = await this.getInfo( 16 | username, 17 | (firstLogin.username).toString() 18 | ); 19 | 20 | if (userInfo && userInfo.id) { 21 | if (this.checkUser(userInfo.id, teamMember)) { 22 | throw new Error(`User ${username} already on this team`); 23 | } 24 | const { response } = await this.helper.toPromise( 25 | this.api.teamApi, 26 | this.api.teamApi.teamsTeamIdUsersUserIdPost, 27 | teamInfo.teamId, 28 | userInfo.username, 29 | { 30 | roleId: role 31 | }); 32 | if (!response.body) { 33 | throw new Error("Error adding user to team: invalid roleId"); 34 | } 35 | console.log(`Success register ${username} to ${currentLogin}`); 36 | } else { 37 | console.log(`User ${username} not found`); 38 | } 39 | } catch (error) { 40 | console.log(this.helper.wrapError(error)); 41 | } 42 | } 43 | 44 | public async removeMember(username: string) { 45 | const answer = await this.helper.inquirerPrompt([ 46 | { 47 | type: "confirm", 48 | name: "confirmation", 49 | message: `Do you want to remove ${username} ?`, 50 | default: false 51 | } 52 | ]); 53 | 54 | if (!answer.confirmation) { 55 | return; 56 | } 57 | 58 | try { 59 | const { userInfo, teamInfo, teamMember, currentLogin } = await this.getInfo(username); 60 | if (userInfo && userInfo.id) { 61 | if (!this.checkUser(userInfo.id, teamMember)) { 62 | throw new Error(`User ${username} not a member of this team`); 63 | } 64 | 65 | const { response } = await this.helper.toPromise(this.api.teamApi, this.api.teamApi.teamsTeamIdUsersUserIdDelete, teamInfo.id, userInfo.id); 66 | 67 | console.log(`Success remove ${username} from ${currentLogin}`); 68 | } else { 69 | console.log(`User ${username} not found`); 70 | } 71 | 72 | } catch (error) { 73 | console.log(this.helper.wrapError(error)); 74 | } 75 | } 76 | 77 | private async getInfo(username: string, firstLoginUsername?: string) { 78 | const currentLogin = this.helper.getProp("current_login") as string; 79 | const currentUserType = this.helper.getProp("current_user_type") as string; 80 | 81 | if (currentUserType !== "team") { 82 | throw new Error("Must be on team to do this operation"); 83 | } 84 | 85 | const requestTeamData = 86 | await this.helper.toPromise( 87 | this.api.userApi, 88 | this.api.userApi.usersUserIdTeamsGet, 89 | firstLoginUsername 90 | ); 91 | 92 | let teamInfo: any; 93 | if (requestTeamData.response && requestTeamData.response.body) { 94 | const filterTeamBasedOnCurrentTeam = (requestTeamData.response.body).filter( 95 | (singleTeam: any) => ( singleTeam.username == currentLogin )); 96 | teamInfo = filterTeamBasedOnCurrentTeam[0]; 97 | } else { 98 | throw new Error("Cannot add user to team"); 99 | } 100 | // console.log('teamInfo ', teamInfo); 101 | 102 | const requestUserData = 103 | await this.helper.toPromise( 104 | this.api.userApi, 105 | this.api.userApi.usersGetInfoKeyGet, 106 | username 107 | ); 108 | 109 | // console.log('requestUserData ', requestUserData); 110 | let userInfo: any; 111 | if (requestUserData && requestUserData.response) { 112 | userInfo = requestUserData.response.body; 113 | } else { 114 | throw new Error("Cannot add user to team"); 115 | } 116 | 117 | // console.log('userInfo ', userInfo); 118 | const requestTeamMember = 119 | await this.helper.toPromise( 120 | this.api.teamApi, 121 | this.api.teamApi.teamsTeamIdUsersGet, 122 | teamInfo.teamId 123 | ); 124 | 125 | let teamMember: any[]; 126 | if (requestTeamMember && requestTeamMember.response) { 127 | teamMember = requestTeamMember.response.body; 128 | } else { 129 | throw new Error("Cannot add user to team"); 130 | } 131 | 132 | // console.log('teamMember ', teamMember); 133 | return { 134 | teamInfo, 135 | userInfo, 136 | teamMember, 137 | currentLogin 138 | }; 139 | } 140 | 141 | private checkUser(userId: string, member: any[]): boolean { 142 | const teamMember = member.map((x: JsonObject) => x.userId); 143 | if (teamMember.indexOf(userId) > -1) { 144 | return true; 145 | } 146 | 147 | return false; 148 | } 149 | 150 | } 151 | -------------------------------------------------------------------------------- /components/scripts/tester.ts: -------------------------------------------------------------------------------- 1 | import { Component, IConfig, JsonObject, IHash } from "merapi"; 2 | import { IHelper } from "interfaces/main"; 3 | 4 | let colors = require("colors/safe"); 5 | 6 | export default class Tester extends Component { 7 | constructor(private config: IConfig, private helper: IHelper) { 8 | super(); 9 | } 10 | 11 | async execIntentTest(yaml: any, botApi: any, botId: string, print = function(text: string){}) : Promise> { 12 | let flow = yaml.flow; 13 | let context = yaml.context || {}; 14 | let data = yaml.data || {}; 15 | let result : IHash<{field: string, expect: any, result: any}[]> = {}; 16 | print(yaml.desc); 17 | let tab = " "; 18 | 19 | for (let name in yaml.test) { 20 | let test = yaml.test[name]; 21 | let testIntent = test.intent || null; 22 | let testContext = Object.assign({}, context, test.context || {}); 23 | let testData = Object.assign({}, data, test.data || {}); 24 | let testMessage = Object.assign({type:"text", content:"", payload:{}}, test.message || {}); 25 | 26 | let body = { 27 | flow, 28 | "intent": testIntent, 29 | "message": testMessage, 30 | "context": testContext, 31 | "data": testData 32 | }; 33 | 34 | let execResult = await this.helper.toPromise(botApi, botApi.botsBotIdExecObjectPost, botId, "intent", body); 35 | result[name] = this.helper.compareTestResult(execResult.data, test.expect); 36 | print(tab+name+": "+ (result[name] && !result[name].length ? colors.green("passed") : colors.red("not passing"))); 37 | } 38 | 39 | return result; 40 | } 41 | 42 | async execStateTest(yaml: any, botApi: any, botId: string, print = function(text: string){}) : Promise> { 43 | let flow = yaml.flow; 44 | let context = yaml.context || {}; 45 | let data = yaml.data || {}; 46 | let result : IHash<{field: string, expect: any, result: any}[]> = {}; 47 | print(yaml.desc); 48 | let tab = " "; 49 | 50 | for (let name in yaml.test) { 51 | let test = yaml.test[name]; 52 | let testState = test.state; 53 | let testContext = Object.assign({}, context, test.context || {}); 54 | let testData = Object.assign({}, data, test.data || {}); 55 | let testMessage = Object.assign({type:"text", content:"", payload:{}}, test.message || {} 56 | , {intent:test.intent, attributes:test.attributes}); 57 | 58 | let body = { 59 | flow, 60 | state: testState, 61 | message: testMessage, 62 | context: testContext, 63 | data: testData 64 | }; 65 | 66 | let execResult = await this.helper.toPromise(botApi, botApi.botsBotIdExecObjectPost, botId, "state-mapper", body); 67 | result[name] = this.helper.compareTestResult(execResult.data, test.expect); 68 | print(tab+name+": "+ (result[name] && !result[name].length ? colors.green("passed") : colors.red("not passing"))); 69 | } 70 | 71 | return result; 72 | } 73 | 74 | async execActionsTest(yaml: any, botApi: any, botId: string, print = function(text: string){}) : Promise> { 75 | let flow = yaml.flow; 76 | let context = yaml.context || {}; 77 | let data = yaml.data || {}; 78 | let result : IHash<{field: string, expect: any, result: any}[]> = {}; 79 | print(yaml.desc); 80 | let tab = " "; 81 | 82 | for (let name in yaml.test) { 83 | let test = yaml.test[name]; 84 | let testState = test.state; 85 | let testContext = Object.assign({}, context, test.context || {}); 86 | let testData = Object.assign({}, data, test.data || {}); 87 | let testMessage = Object.assign({type:"text", content:"", payload:{}}, test.message || {} 88 | , {intent:test.intent, attributes:test.attributes}); 89 | 90 | let body = { 91 | flow, 92 | state: testState, 93 | message: testMessage, 94 | context: testContext, 95 | data: testData 96 | }; 97 | 98 | let execResult = await this.helper.toPromise(botApi, botApi.botsBotIdExecObjectPost, botId, "action", body); 99 | result[name] = this.helper.compareTestResult(execResult.data, test.expect); 100 | print(tab+name+": "+ (result[name] && !result[name].length ? colors.green("passed") : colors.red("not passing"))); 101 | } 102 | 103 | return result; 104 | } 105 | 106 | async execFlowTest(yaml: any, botApi: any, botId: string, print = function(text: string){}) : Promise> { 107 | let flow = yaml.flow; 108 | let state = yaml.state || null; 109 | let context = yaml.context || {}; 110 | let data = yaml.data || {}; 111 | let result : IHash<{field: string, expect: any, result: any}[]> = {}; 112 | print(yaml.desc); 113 | let tab = " "; 114 | 115 | for (let name in yaml.test) { 116 | let test = yaml.test[name]; 117 | let message = Object.assign({type:"text", content:"", payload:{}}, test.message || {} 118 | , {intent:test.intent, attributes:test.attributes}); 119 | 120 | let body = { flow, state, message, context, data }; 121 | 122 | let execResult = await this.helper.toPromise(botApi, botApi.botsBotIdExecObjectPost, botId, "flow", body); 123 | result[name] = this.helper.compareTestResult(execResult.data, test.expect); 124 | print(tab+name+": "+ (result[name] && !result[name].length ? colors.green("passed") : colors.red("not passing"))); 125 | if (!execResult) break; 126 | state = execResult.data.state; 127 | context = execResult.data.context; 128 | data = execResult.data; 129 | } 130 | 131 | return result; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /lib/components/users/team.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | Object.defineProperty(exports, "__esModule", { value: true }); 12 | const merapi_1 = require("merapi"); 13 | class Team extends merapi_1.Component { 14 | constructor(helper, api) { 15 | super(); 16 | this.helper = helper; 17 | this.api = api; 18 | } 19 | addMember(username, options) { 20 | return __awaiter(this, void 0, void 0, function* () { 21 | try { 22 | const role = options.admin ? "teamAdmin" : "teamMember"; 23 | let firstLogin = this.helper.getProp("first_login"); 24 | const { userInfo, teamInfo, teamMember, currentLogin } = yield this.getInfo(username, (firstLogin.username).toString()); 25 | if (userInfo && userInfo.id) { 26 | if (this.checkUser(userInfo.id, teamMember)) { 27 | throw new Error(`User ${username} already on this team`); 28 | } 29 | const { response } = yield this.helper.toPromise(this.api.teamApi, this.api.teamApi.teamsTeamIdUsersUserIdPost, teamInfo.teamId, userInfo.username, { 30 | roleId: role 31 | }); 32 | if (!response.body) { 33 | throw new Error("Error adding user to team: invalid roleId"); 34 | } 35 | console.log(`Success register ${username} to ${currentLogin}`); 36 | } 37 | else { 38 | console.log(`User ${username} not found`); 39 | } 40 | } 41 | catch (error) { 42 | console.log(this.helper.wrapError(error)); 43 | } 44 | }); 45 | } 46 | removeMember(username) { 47 | return __awaiter(this, void 0, void 0, function* () { 48 | const answer = yield this.helper.inquirerPrompt([ 49 | { 50 | type: "confirm", 51 | name: "confirmation", 52 | message: `Do you want to remove ${username} ?`, 53 | default: false 54 | } 55 | ]); 56 | if (!answer.confirmation) { 57 | return; 58 | } 59 | try { 60 | const { userInfo, teamInfo, teamMember, currentLogin } = yield this.getInfo(username); 61 | if (userInfo && userInfo.id) { 62 | if (!this.checkUser(userInfo.id, teamMember)) { 63 | throw new Error(`User ${username} not a member of this team`); 64 | } 65 | const { response } = yield this.helper.toPromise(this.api.teamApi, this.api.teamApi.teamsTeamIdUsersUserIdDelete, teamInfo.id, userInfo.id); 66 | console.log(`Success remove ${username} from ${currentLogin}`); 67 | } 68 | else { 69 | console.log(`User ${username} not found`); 70 | } 71 | } 72 | catch (error) { 73 | console.log(this.helper.wrapError(error)); 74 | } 75 | }); 76 | } 77 | getInfo(username, firstLoginUsername) { 78 | return __awaiter(this, void 0, void 0, function* () { 79 | const currentLogin = this.helper.getProp("current_login"); 80 | const currentUserType = this.helper.getProp("current_user_type"); 81 | if (currentUserType !== "team") { 82 | throw new Error("Must be on team to do this operation"); 83 | } 84 | const requestTeamData = yield this.helper.toPromise(this.api.userApi, this.api.userApi.usersUserIdTeamsGet, firstLoginUsername); 85 | let teamInfo; 86 | if (requestTeamData.response && requestTeamData.response.body) { 87 | const filterTeamBasedOnCurrentTeam = (requestTeamData.response.body).filter((singleTeam) => (singleTeam.username == currentLogin)); 88 | teamInfo = filterTeamBasedOnCurrentTeam[0]; 89 | } 90 | else { 91 | throw new Error("Cannot add user to team"); 92 | } 93 | // console.log('teamInfo ', teamInfo); 94 | const requestUserData = yield this.helper.toPromise(this.api.userApi, this.api.userApi.usersGetInfoKeyGet, username); 95 | // console.log('requestUserData ', requestUserData); 96 | let userInfo; 97 | if (requestUserData && requestUserData.response) { 98 | userInfo = requestUserData.response.body; 99 | } 100 | else { 101 | throw new Error("Cannot add user to team"); 102 | } 103 | // console.log('userInfo ', userInfo); 104 | const requestTeamMember = yield this.helper.toPromise(this.api.teamApi, this.api.teamApi.teamsTeamIdUsersGet, teamInfo.teamId); 105 | let teamMember; 106 | if (requestTeamMember && requestTeamMember.response) { 107 | teamMember = requestTeamMember.response.body; 108 | } 109 | else { 110 | throw new Error("Cannot add user to team"); 111 | } 112 | // console.log('teamMember ', teamMember); 113 | return { 114 | teamInfo, 115 | userInfo, 116 | teamMember, 117 | currentLogin 118 | }; 119 | }); 120 | } 121 | checkUser(userId, member) { 122 | const teamMember = member.map((x) => x.userId); 123 | if (teamMember.indexOf(userId) > -1) { 124 | return true; 125 | } 126 | return false; 127 | } 128 | } 129 | exports.default = Team; 130 | //# sourceMappingURL=team.js.map -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## [2.7.2] - 2021/11/04 4 | 5 | - Update zaun version to 2.7.1 6 | 7 | ## [2.7.1] - 2021/10/11 8 | 9 | - Fix create project timezone and description bug 10 | 11 | ## [2.7.0] - 2021/07/19 12 | 13 | - Add cli-login endpoint 14 | 15 | - Fix default session to use camelCase 16 | 17 | 18 | ## [2.6.4] - 2021/01/26 19 | 20 | - Fix nl training condition 21 | 22 | ## [2.6.3] - 2020/11/11 23 | 24 | - Fix select project by name 25 | 26 | ## [2.6.1] - 2020/08/25 27 | 28 | - Fix when create deployment not set cmsId, nluId, botId 29 | 30 | ## [2.6.0] - 2019/11/19 31 | 32 | - Fix search user by username or email on`kata invite-member` and `kata impersonate`command ([#133](https://github.com/kata-ai/kata-cli/pull/133), [#139](https://github.com/kata-ai/kata-cli/pull/139)) 33 | 34 | ## [2.5.0] - 2019/09/02 35 | 36 | - Fix bug when switch to team during in impersonate mode ([#126](https://github.com/kata-ai/kata-cli/pull/126)) 37 | - Add feature to select project name inline ([#127](https://github.com/kata-ai/kata-cli/pull/127)) 38 | - Add handler to display notification message when error happened, instead of letting string quotes `''` pop up ([#128](https://github.com/kata-ai/kata-cli/pull/128)) 39 | 40 | ## [2.4.0] - 2019/07/12 41 | 42 | - Add missing feature so that Kata-CLI is aligned with Platform. (#122) 43 | - For the sake of consistency, we renamed several commands, such as: 44 | - `kata add-channel` to `kata create-channel` 45 | - `kata edit-channel` to `kata update-channel` 46 | - `kata config-view` to `kata view-config` 47 | - `kata add-member` to `kata invite-member` 48 | - Add feature: impersonate user when logging in as administrator (#121) 49 | 50 | ## [2.3.0] - 2019/04/16 51 | 52 | - Improved user experience for update-environment command (#120) 53 | 54 | ## [2.2.0] - 2019/03/20 55 | 56 | - Improved user experience for update-environment command (#114) 57 | - Add 20 character limit validation for environment URL and project name (#115) 58 | - Improved error message for missing NLU.yml (#117) 59 | - Fixed bug on create-deployment command not choosing the correct NLU revision (#118) 60 | 61 | ## [2.1.1] - 2019/03/01 62 | 63 | - Change analytics message (#119) 64 | 65 | ## [2.1.0] - 2019/02/14 66 | 67 | ### Fixed : 68 | 69 | - Change endpoint API for command `push` (#109) 70 | - Check job training status on command `nl-train` (#108) 71 | - Add training error detail for command `nl-train` (#107) 72 | - Fix endpoint API for command `nl-snapshot` (#106) 73 | - Add Google Analytics for command tracking (#105) 74 | 75 | ## [2.0.11] - 2019/01/03 76 | 77 | ### Fixed : 78 | 79 | - Create new revision on push (#102) 80 | - Fix get latest revision (#101) 81 | - Limit new entity name to 20 chars and add detailed error messages (#98) 82 | - Set substring to 7 (#97) 83 | 84 | ## [2.0.8] - 2018/12/20 85 | 86 | ### Fixed : 87 | 88 | - Fix nl-pull for entity type dictionary 89 | - Latest bot, nlu, and cms revision endpoint 90 | - Remove yarn.lock 91 | 92 | ## [2.0.6] - 2018/12/20 93 | 94 | ### Fixed : 95 | 96 | - Remove warning http2 97 | 98 | ## [2.0.5] - 2018/12/20 99 | 100 | ### Fixed : 101 | 102 | - Restrict create-environment when name exist 103 | - Change Project Push endpoint 104 | 105 | ### Added : 106 | 107 | - NL docs on README 108 | - Move CONTRIBUTING to README 109 | 110 | ## [2.0.4] - 2018/12/13 111 | 112 | ### Fixed : 113 | 114 | - Remove \*.yml from .gitignore 115 | 116 | ## [2.0.3] - 2018/12/13 117 | 118 | ### Fixed : 119 | 120 | - Fix vulnerabilities 121 | 122 | ## [2.0.2] - 2018/12/13 123 | 124 | ### Fixed : 125 | 126 | - Switch team error 127 | - Create team error namespace 128 | - Add or remove member to team 129 | 130 | ### Added : 131 | 132 | - Pull with revision 133 | - Version type when create-deployment 134 | 135 | ## [2.0.1] - 2018/12/05 136 | 137 | ### Fixed : 138 | 139 | - Fix .gitignore 140 | 141 | ## [2.0.0] - 2018/12/05 142 | 143 | ### Added : 144 | 145 | - Kata CLI v2.0 146 | 147 | ## [1.4.0] - 2018/08/16 148 | 149 | ### Added : 150 | 151 | - Add twitter channel type 152 | 153 | ### Fixed : 154 | 155 | - Switch team method 156 | 157 | ## [1.3.2] - 2018/07/12 158 | 159 | ### Added : 160 | 161 | - Add config-view command 162 | 163 | ### Fixed 164 | 165 | - Integrate function 'versions' to new bot version object 166 | 167 | ## [1.3.1] - 2018/02/12 168 | 169 | ### Fixed 170 | 171 | - Fix undefined context and state in flow spec 172 | 173 | ## [1.3.0] - 2018/02/05 174 | 175 | ### Added : 176 | 177 | - Kata pull bot 178 | 179 | ### Fixed 180 | 181 | - Fix version still increment when failed push bot 182 | 183 | ## [1.2.4] - 2017/01/08 184 | 185 | ### Fixed : 186 | 187 | - Handle kata console error when no bot.yml in directory 188 | 189 | ## [1.2.3] - 2017/12/27 190 | 191 | ### Fixed : 192 | 193 | - Fix bug on kata deploy and kata nl-push command 194 | 195 | ## [1.2.1] - 2017/12/21 196 | 197 | ### Changed : 198 | 199 | - Update kata init command 200 | 201 | ## [1.2.0] - 2017/12/06 202 | 203 | ### Added : 204 | 205 | - Add discard and update draft 206 | - Add new version information when pushing bot 207 | - Generate challenge token for fbmessenger channel type 208 | 209 | ### Changed : 210 | 211 | - Change kata init output message 212 | 213 | ### Fixed : 214 | 215 | - Fix proper handling for kata versions 216 | 217 | ## [1.1.0] - 2017/11/28 218 | 219 | ### Added : 220 | 221 | - Handle NLU Management 222 | 223 | ### Fixed : 224 | 225 | - Fix wrong username and password 226 | - Fix invalid user command input 227 | 228 | ## 1.0.0 - 2017/10/26 229 | 230 | ### Added : 231 | 232 | - Handle Bot Management 233 | - Handle Deployment Management 234 | - Handle User & Team Management 235 | 236 | [2.0.11]: https://github.com/kata-ai/kata-cli/compare/v2.0.8...v2.0.11 237 | [2.0.8]: https://github.com/kata-ai/kata-cli/compare/v2.0.6...v2.0.8 238 | [2.0.6]: https://github.com/kata-ai/kata-cli/compare/v2.0.5...v2.0.6 239 | [2.0.5]: https://github.com/kata-ai/kata-cli/compare/v2.0.4...v2.0.5 240 | [2.0.4]: https://github.com/kata-ai/kata-cli/compare/v2.0.3...v2.0.4 241 | [2.0.3]: https://github.com/kata-ai/kata-cli/compare/v2.0.2...v2.0.3 242 | [2.0.2]: https://github.com/kata-ai/kata-cli/compare/v2.0.1...v2.0.2 243 | [2.0.1]: https://github.com/kata-ai/kata-cli/compare/v2.0.0...v2.0.1 244 | [2.0.0]: https://github.com/kata-ai/kata-cli/compare/v1.4.0...v2.0.0 245 | [1.4.0]: https://github.com/kata-ai/kata-cli/compare/v1.3.2...v1.4.0 246 | [1.3.2]: https://github.com/kata-ai/kata-cli/compare/v1.3.1...v1.3.2 247 | [1.3.1]: https://github.com/kata-ai/kata-cli/compare/v1.3.0...v1.3.1 248 | [1.3.0]: https://github.com/kata-ai/kata-cli/compare/v1.2.4...v1.3.0 249 | [1.2.4]: https://github.com/kata-ai/kata-cli/compare/v1.2.3...v1.2.4 250 | [1.2.3]: https://github.com/kata-ai/kata-cli/compare/v1.2.1...v1.2.3 251 | [1.2.1]: https://github.com/kata-ai/kata-cli/compare/v1.2.0...v1.2.1 252 | [1.2.0]: https://github.com/kata-ai/kata-cli/compare/v1.1.0...v1.2.0 253 | [1.1.0]: https://github.com/kata-ai/kata-cli/compare/v1.0.0...v1.1.0 254 | -------------------------------------------------------------------------------- /lib/components/scripts/tester.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | Object.defineProperty(exports, "__esModule", { value: true }); 12 | const merapi_1 = require("merapi"); 13 | let colors = require("colors/safe"); 14 | class Tester extends merapi_1.Component { 15 | constructor(config, helper) { 16 | super(); 17 | this.config = config; 18 | this.helper = helper; 19 | } 20 | execIntentTest(yaml, botApi, botId, print = function (text) { }) { 21 | return __awaiter(this, void 0, void 0, function* () { 22 | let flow = yaml.flow; 23 | let context = yaml.context || {}; 24 | let data = yaml.data || {}; 25 | let result = {}; 26 | print(yaml.desc); 27 | let tab = " "; 28 | for (let name in yaml.test) { 29 | let test = yaml.test[name]; 30 | let testIntent = test.intent || null; 31 | let testContext = Object.assign({}, context, test.context || {}); 32 | let testData = Object.assign({}, data, test.data || {}); 33 | let testMessage = Object.assign({ type: "text", content: "", payload: {} }, test.message || {}); 34 | let body = { 35 | flow, 36 | "intent": testIntent, 37 | "message": testMessage, 38 | "context": testContext, 39 | "data": testData 40 | }; 41 | let execResult = yield this.helper.toPromise(botApi, botApi.botsBotIdExecObjectPost, botId, "intent", body); 42 | result[name] = this.helper.compareTestResult(execResult.data, test.expect); 43 | print(tab + name + ": " + (result[name] && !result[name].length ? colors.green("passed") : colors.red("not passing"))); 44 | } 45 | return result; 46 | }); 47 | } 48 | execStateTest(yaml, botApi, botId, print = function (text) { }) { 49 | return __awaiter(this, void 0, void 0, function* () { 50 | let flow = yaml.flow; 51 | let context = yaml.context || {}; 52 | let data = yaml.data || {}; 53 | let result = {}; 54 | print(yaml.desc); 55 | let tab = " "; 56 | for (let name in yaml.test) { 57 | let test = yaml.test[name]; 58 | let testState = test.state; 59 | let testContext = Object.assign({}, context, test.context || {}); 60 | let testData = Object.assign({}, data, test.data || {}); 61 | let testMessage = Object.assign({ type: "text", content: "", payload: {} }, test.message || {}, { intent: test.intent, attributes: test.attributes }); 62 | let body = { 63 | flow, 64 | state: testState, 65 | message: testMessage, 66 | context: testContext, 67 | data: testData 68 | }; 69 | let execResult = yield this.helper.toPromise(botApi, botApi.botsBotIdExecObjectPost, botId, "state-mapper", body); 70 | result[name] = this.helper.compareTestResult(execResult.data, test.expect); 71 | print(tab + name + ": " + (result[name] && !result[name].length ? colors.green("passed") : colors.red("not passing"))); 72 | } 73 | return result; 74 | }); 75 | } 76 | execActionsTest(yaml, botApi, botId, print = function (text) { }) { 77 | return __awaiter(this, void 0, void 0, function* () { 78 | let flow = yaml.flow; 79 | let context = yaml.context || {}; 80 | let data = yaml.data || {}; 81 | let result = {}; 82 | print(yaml.desc); 83 | let tab = " "; 84 | for (let name in yaml.test) { 85 | let test = yaml.test[name]; 86 | let testState = test.state; 87 | let testContext = Object.assign({}, context, test.context || {}); 88 | let testData = Object.assign({}, data, test.data || {}); 89 | let testMessage = Object.assign({ type: "text", content: "", payload: {} }, test.message || {}, { intent: test.intent, attributes: test.attributes }); 90 | let body = { 91 | flow, 92 | state: testState, 93 | message: testMessage, 94 | context: testContext, 95 | data: testData 96 | }; 97 | let execResult = yield this.helper.toPromise(botApi, botApi.botsBotIdExecObjectPost, botId, "action", body); 98 | result[name] = this.helper.compareTestResult(execResult.data, test.expect); 99 | print(tab + name + ": " + (result[name] && !result[name].length ? colors.green("passed") : colors.red("not passing"))); 100 | } 101 | return result; 102 | }); 103 | } 104 | execFlowTest(yaml, botApi, botId, print = function (text) { }) { 105 | return __awaiter(this, void 0, void 0, function* () { 106 | let flow = yaml.flow; 107 | let state = yaml.state || null; 108 | let context = yaml.context || {}; 109 | let data = yaml.data || {}; 110 | let result = {}; 111 | print(yaml.desc); 112 | let tab = " "; 113 | for (let name in yaml.test) { 114 | let test = yaml.test[name]; 115 | let message = Object.assign({ type: "text", content: "", payload: {} }, test.message || {}, { intent: test.intent, attributes: test.attributes }); 116 | let body = { flow, state, message, context, data }; 117 | let execResult = yield this.helper.toPromise(botApi, botApi.botsBotIdExecObjectPost, botId, "flow", body); 118 | result[name] = this.helper.compareTestResult(execResult.data, test.expect); 119 | print(tab + name + ": " + (result[name] && !result[name].length ? colors.green("passed") : colors.red("not passing"))); 120 | if (!execResult) 121 | break; 122 | state = execResult.data.state; 123 | context = execResult.data.context; 124 | data = execResult.data; 125 | } 126 | return result; 127 | }); 128 | } 129 | } 130 | exports.default = Tester; 131 | //# sourceMappingURL=tester.js.map -------------------------------------------------------------------------------- /components/bots/environment.ts: -------------------------------------------------------------------------------- 1 | 2 | import { IHelper } from "interfaces/main"; 3 | import inquirer = require("inquirer"); 4 | const Table = require("cli-table"); 5 | 6 | export default class Environment { 7 | constructor( 8 | private helper: IHelper, 9 | private api: any, 10 | ) { } 11 | 12 | public async create(slug: string) { 13 | const projectId = this.helper.getProjectId(); 14 | 15 | if (slug.length > 20) { 16 | console.error("error: Namespace length can not exceed 20 characters"); 17 | return; 18 | } 19 | 20 | try { 21 | const deployment = await this.getLatestDeployment(); 22 | if (!deployment) { 23 | throw Error("Deployment not found"); 24 | } 25 | 26 | const name = await this.askPredefinedEnvironmentName(); 27 | 28 | const existEnvs = await this.listEnvironment(); 29 | for (const existEnv of existEnvs) { 30 | if ((existEnv.name as string).toLowerCase() === name.toLowerCase()) { 31 | throw new Error( 32 | "Can not create environment with same name. " + 33 | "Please use command update-environment."); 34 | } 35 | } 36 | 37 | const postBody = { 38 | depId: projectId, 39 | depVersion: deployment.version, 40 | name, slug, 41 | }; 42 | 43 | const { response: { body } } = await this.helper.toPromise( 44 | this.api.deploymentApi, this.api.deploymentApi.projectsProjectIdEnvironmentsPost, 45 | projectId, postBody, 46 | ); 47 | 48 | console.log(body); 49 | console.log("ENVIRONMENT CREATED SUCCESSFULLY"); 50 | } catch (e) { 51 | console.log(this.helper.wrapError(e)); 52 | } 53 | } 54 | 55 | 56 | public async list() { 57 | try { 58 | const environmentList = await this.listEnvironment(); 59 | 60 | const table = new Table({ 61 | head: ["Environment Name", "Environment Slug", "Environment ID", "Deployment Version"], 62 | colWidths: [30, 30, 42, 30] 63 | }); 64 | environmentList.forEach((environment) => { 65 | table.push([environment.name, String(environment.slug), environment.id, environment.depVersion]); 66 | }); 67 | 68 | console.log(table.toString()); 69 | } catch (e) { 70 | console.log(this.helper.wrapError(e)); 71 | } 72 | } 73 | 74 | private async askDeploymentId(prop: { message?: string } = {}): Promise { 75 | const projectId = this.helper.getProjectId(); 76 | 77 | let page = 1; 78 | while (true) { 79 | const { response: { body } } = await this.helper.toPromise( 80 | this.api.deploymentApi, this.api.deploymentApi.projectsProjectIdDeploymentVersionsGet, projectId, { 81 | page 82 | } 83 | ); 84 | const choices = body.data.map((row: any) => ({ name: `(${row.version})`, value: row.version })); 85 | 86 | if (body.total > body.page * body.limit) { 87 | choices.push({name: "Load More", value: -1}); 88 | } 89 | 90 | const { deploymentVersion } = await inquirer.prompt([ 91 | { 92 | type: "list", 93 | name: "deploymentVersion", 94 | message: prop.message || "Select Deployment:", 95 | paginated: false, 96 | choices 97 | }, 98 | ]); 99 | if (deploymentVersion === -1) { 100 | page++; 101 | continue; 102 | } 103 | return deploymentVersion; 104 | } 105 | } 106 | 107 | public async update(newDeploymentVersion: string) { 108 | const projectId = this.helper.getProjectId(); 109 | if (!newDeploymentVersion) { 110 | newDeploymentVersion = await this.askDeploymentId(); 111 | } 112 | if (!/[0-9]+\.[0-9]+\.[0-9]+/.test(newDeploymentVersion)) { 113 | console.error("error: Deployment version must be in the format of <0-9>.<0-9>.<0-9>"); 114 | return; 115 | } 116 | const { response: { body } } = await this.helper.toPromise( 117 | this.api.deploymentApi, this.api.deploymentApi.projectsProjectIdDeploymentVersionsGet, projectId, { limit: 1000000 } 118 | ); 119 | if (!body.data.find((data: any) => data.version === newDeploymentVersion)) { 120 | console.error(`error: There are no deployment with version ${newDeploymentVersion}`); 121 | return; 122 | } 123 | 124 | try { 125 | const environmentId = await this.askEnvironmentId({ 126 | message: "Select which Environment to update: " 127 | }); 128 | 129 | const { response: { body } } = await this.helper.toPromise( 130 | this.api.deploymentApi, this.api.deploymentApi.projectsProjectIdEnvironmentsEnvironmentIdPut, 131 | environmentId, projectId, { depVersion: newDeploymentVersion } 132 | ); 133 | 134 | if (!body) { 135 | throw Error("Error updating Environment"); 136 | } 137 | 138 | return console.log(body); 139 | } catch (error) { 140 | console.log(this.helper.wrapError(error)); 141 | } 142 | } 143 | 144 | public async askEnvironmentId(prop: { message?: string } = {}): Promise { 145 | const environmentList = await this.listEnvironment(); 146 | const choices = environmentList.map((row: any) => ({ name: `${row.name} (${row.depVersion})`, value: row.id })); 147 | 148 | const { environmentId } = await inquirer.prompt([ 149 | { 150 | type: "list", 151 | name: "environmentId", 152 | message: prop.message || "Select Environment:", 153 | paginated: false, 154 | choices 155 | }, 156 | ]); 157 | return environmentId; 158 | } 159 | 160 | private async askPredefinedEnvironmentName(): Promise { 161 | const environmentNames = ["Development", "Production", "Staging"]; 162 | const choices = environmentNames.map((name) => ({ name, value: name })); 163 | 164 | const { environmentName } = await inquirer.prompt([ 165 | { 166 | type: "list", 167 | name: "environmentName", 168 | message: "Which Environment to create?", 169 | paginated: false, 170 | choices 171 | }, 172 | ]); 173 | 174 | return environmentName; 175 | } 176 | 177 | private async getLatestDeployment() { 178 | const projectId = this.helper.getProjectId(); 179 | 180 | const { response } = await this.helper.toPromise( 181 | this.api.projectApi, this.api.projectApi.projectsProjectIdDeploymentGet, projectId 182 | ); 183 | 184 | return response && response.body; 185 | } 186 | 187 | private async listEnvironment(): Promise { 188 | const projectId = this.helper.getProp("projectId"); 189 | const { response: { body } } = await this.helper.toPromise( 190 | this.api.deploymentApi, this.api.deploymentApi.projectsProjectIdEnvironmentsGet, projectId, {} 191 | ); 192 | if (!body || !body.data) { 193 | throw Error("Failed to list environments."); 194 | } 195 | return body.data; 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /components/bots/deployment.ts: -------------------------------------------------------------------------------- 1 | 2 | import { IHelper } from "interfaces/main"; 3 | import { JsonObject } from "merapi"; 4 | const Table = require("cli-table"); 5 | import inquirer = require("inquirer"); 6 | 7 | export default class Deployment { 8 | constructor(private helper: IHelper, private api: any) { 9 | } 10 | 11 | public async create(versionType?: string) { 12 | const projectId = this.helper.getProjectId(); 13 | const { response: { body: project } } = await this.helper.toPromise( 14 | this.api.projectApi, this.api.projectApi.projectsProjectIdGet, 15 | projectId 16 | ); 17 | // TODO: {page:1, limit:1} 18 | let botRevision: string; 19 | let nluRevision: string; 20 | let cmsRevision: string; 21 | 22 | try { 23 | const { response: { body: data } } = await this.helper.toPromise( 24 | this.api.projectApi, 25 | this.api.projectApi.projectsProjectIdBotGet, projectId 26 | ); 27 | 28 | if (data.revision) { 29 | botRevision = data.revision; 30 | } 31 | } catch (e) { 32 | console.error("Error"); 33 | console.log(this.helper.wrapError(e)); 34 | } 35 | 36 | try { 37 | const { response: { body: data } } = await this.helper.toPromise(this.api.projectApi, 38 | this.api.projectApi.projectsProjectIdNluRevisionsGet, projectId); 39 | 40 | if (data.data[0] && data.data[0].revision) { 41 | nluRevision = data.data[0].revision; 42 | } 43 | } catch (e) { 44 | console.error("Error"); 45 | console.log(this.helper.wrapError(e)); 46 | } 47 | 48 | try { 49 | const { response: { body: data } } = await this.helper.toPromise(this.api.projectApi, 50 | this.api.projectApi.projectsProjectIdCmsGet, projectId); 51 | if (data.revision) { 52 | cmsRevision = data.revision; 53 | } 54 | } catch (e) { 55 | console.error("Error"); 56 | console.log(this.helper.wrapError(e)); 57 | } 58 | 59 | let targetVersion; 60 | 61 | try { 62 | // get previous deployment version 63 | const { response: { body: latestDeployment } } = await this.helper.toPromise(this.api.projectApi, 64 | this.api.projectApi.projectsProjectIdDeploymentGet, projectId); 65 | const prevVersion = latestDeployment.version; 66 | let [major, minor, patch] = prevVersion.split("."); 67 | 68 | if (versionType === "major") { 69 | major = Number(major) + 1; 70 | minor = 0; 71 | patch = 0; 72 | } else if (versionType === "minor") { 73 | minor = Number(minor) + 1; 74 | patch = 0; 75 | } else { 76 | patch = Number(patch) + 1; 77 | } 78 | targetVersion = `${major}.${minor}.${patch}`; 79 | 80 | } catch (e) { 81 | targetVersion = "0.0.1"; 82 | } 83 | 84 | try { 85 | const postBody = { 86 | version: targetVersion, 87 | botId: projectId, 88 | botRevision, 89 | nluId: projectId, 90 | nluRevision, 91 | cmsId: projectId, 92 | cmsRevision, 93 | modules: (null as any), 94 | }; 95 | 96 | const { response: { body } } = await this.helper.toPromise( 97 | this.api.deploymentApi, this.api.deploymentApi.projectsProjectIdDeploymentVersionsPost, 98 | postBody, projectId, 99 | ); 100 | 101 | 102 | console.log(`Bot Revision: ${botRevision.substring(0, 7)}`); 103 | console.log(`NLU Revision: ${nluRevision.substring(0, 7)}`); 104 | console.log(`CMS Revision: ${cmsRevision.substring(0, 7)}`); 105 | console.log(`Succesfully create Deployment to version ${targetVersion}`); 106 | } catch (e) { 107 | console.error("Error"); 108 | console.log(this.helper.wrapError(e)); 109 | } 110 | } 111 | 112 | public async list(options: JsonObject) { 113 | const projectId = this.helper.getProjectId(); 114 | 115 | try { 116 | const { response: { body } } = await this.helper.toPromise( 117 | this.api.deploymentApi, this.api.deploymentApi.projectsProjectIdDeploymentVersionsGet, projectId, {} 118 | ); 119 | 120 | if (body && body.data) { 121 | const table = new Table({ 122 | head: ["Deployment Name", "Deployment Version", "Bot Revision"], 123 | colWidths: [30, 30, 42] 124 | }); 125 | body.data.forEach((deployment: JsonObject) => { 126 | table.push([deployment.name, deployment.version, deployment.botRevision]); 127 | }); 128 | 129 | console.log(table.toString()); 130 | } 131 | } catch (e) { 132 | console.error("Error"); 133 | console.log(this.helper.wrapError(e)); 134 | } 135 | } 136 | 137 | public async rollback(version: string) { 138 | try { 139 | const projectId = this.helper.getProjectId(); 140 | const author = this.helper.getProp("current_login"); 141 | 142 | const { response: { body } } = await this.helper.toPromise( 143 | this.api.deploymentApi, this.api.deploymentApi.projectsProjectIdDeploymentVersionsGet, 144 | projectId, 145 | { 146 | limit: 1000000, 147 | page: 1 148 | } 149 | ); 150 | 151 | if (body && body.data) { 152 | const deployments: object[] = body.data; 153 | const versionData: any = deployments.find((deployment: any) => deployment.version === version); 154 | if (!versionData) { 155 | throw new Error("Version not found"); 156 | } 157 | 158 | const { changelog, confirm } = await inquirer.prompt([ 159 | { 160 | type: "text", 161 | name: "changelog", 162 | message: "Changelog:", 163 | }, 164 | { 165 | type: "confirm", 166 | name: "confirm", 167 | message: `IMPORTANT: Existing NL training data will also be rolled back to version ${versionData.version}`, 168 | default: true, 169 | } 170 | ]); 171 | 172 | const requestBody = { 173 | author, 174 | changelog, 175 | version: versionData.version 176 | }; 177 | 178 | if (confirm) { 179 | const { response } = await this.helper.toPromise( 180 | this.api.deploymentApi, this.api.deploymentApi.deploymentsDeploymentIdRollbackPost, 181 | versionData.id, 182 | requestBody 183 | ); 184 | 185 | if (response && response.body) { 186 | console.log(`Successfully rolled back to version ${versionData.version}`); 187 | } else { 188 | console.log(`Error when trying to rollback to version ${versionData.version}`); 189 | } 190 | } 191 | } 192 | } catch (e) { 193 | console.log(this.helper.wrapError(e)); 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /lib/components/bots/deployment.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | Object.defineProperty(exports, "__esModule", { value: true }); 12 | const Table = require("cli-table"); 13 | const inquirer = require("inquirer"); 14 | class Deployment { 15 | constructor(helper, api) { 16 | this.helper = helper; 17 | this.api = api; 18 | } 19 | create(versionType) { 20 | return __awaiter(this, void 0, void 0, function* () { 21 | const projectId = this.helper.getProjectId(); 22 | const { response: { body: project } } = yield this.helper.toPromise(this.api.projectApi, this.api.projectApi.projectsProjectIdGet, projectId); 23 | // TODO: {page:1, limit:1} 24 | let botRevision; 25 | let nluRevision; 26 | let cmsRevision; 27 | try { 28 | const { response: { body: data } } = yield this.helper.toPromise(this.api.projectApi, this.api.projectApi.projectsProjectIdBotGet, projectId); 29 | if (data.revision) { 30 | botRevision = data.revision; 31 | } 32 | } 33 | catch (e) { 34 | console.error("Error"); 35 | console.log(this.helper.wrapError(e)); 36 | } 37 | try { 38 | const { response: { body: data } } = yield this.helper.toPromise(this.api.projectApi, this.api.projectApi.projectsProjectIdNluRevisionsGet, projectId); 39 | if (data.data[0] && data.data[0].revision) { 40 | nluRevision = data.data[0].revision; 41 | } 42 | } 43 | catch (e) { 44 | console.error("Error"); 45 | console.log(this.helper.wrapError(e)); 46 | } 47 | try { 48 | const { response: { body: data } } = yield this.helper.toPromise(this.api.projectApi, this.api.projectApi.projectsProjectIdCmsGet, projectId); 49 | if (data.revision) { 50 | cmsRevision = data.revision; 51 | } 52 | } 53 | catch (e) { 54 | console.error("Error"); 55 | console.log(this.helper.wrapError(e)); 56 | } 57 | let targetVersion; 58 | try { 59 | // get previous deployment version 60 | const { response: { body: latestDeployment } } = yield this.helper.toPromise(this.api.projectApi, this.api.projectApi.projectsProjectIdDeploymentGet, projectId); 61 | const prevVersion = latestDeployment.version; 62 | let [major, minor, patch] = prevVersion.split("."); 63 | if (versionType === "major") { 64 | major = Number(major) + 1; 65 | minor = 0; 66 | patch = 0; 67 | } 68 | else if (versionType === "minor") { 69 | minor = Number(minor) + 1; 70 | patch = 0; 71 | } 72 | else { 73 | patch = Number(patch) + 1; 74 | } 75 | targetVersion = `${major}.${minor}.${patch}`; 76 | } 77 | catch (e) { 78 | targetVersion = "0.0.1"; 79 | } 80 | try { 81 | const postBody = { 82 | version: targetVersion, 83 | botId: projectId, 84 | botRevision, 85 | nluId: projectId, 86 | nluRevision, 87 | cmsId: projectId, 88 | cmsRevision, 89 | modules: null, 90 | }; 91 | const { response: { body } } = yield this.helper.toPromise(this.api.deploymentApi, this.api.deploymentApi.projectsProjectIdDeploymentVersionsPost, postBody, projectId); 92 | console.log(`Bot Revision: ${botRevision.substring(0, 7)}`); 93 | console.log(`NLU Revision: ${nluRevision.substring(0, 7)}`); 94 | console.log(`CMS Revision: ${cmsRevision.substring(0, 7)}`); 95 | console.log(`Succesfully create Deployment to version ${targetVersion}`); 96 | } 97 | catch (e) { 98 | console.error("Error"); 99 | console.log(this.helper.wrapError(e)); 100 | } 101 | }); 102 | } 103 | list(options) { 104 | return __awaiter(this, void 0, void 0, function* () { 105 | const projectId = this.helper.getProjectId(); 106 | try { 107 | const { response: { body } } = yield this.helper.toPromise(this.api.deploymentApi, this.api.deploymentApi.projectsProjectIdDeploymentVersionsGet, projectId, {}); 108 | if (body && body.data) { 109 | const table = new Table({ 110 | head: ["Deployment Name", "Deployment Version", "Bot Revision"], 111 | colWidths: [30, 30, 42] 112 | }); 113 | body.data.forEach((deployment) => { 114 | table.push([deployment.name, deployment.version, deployment.botRevision]); 115 | }); 116 | console.log(table.toString()); 117 | } 118 | } 119 | catch (e) { 120 | console.error("Error"); 121 | console.log(this.helper.wrapError(e)); 122 | } 123 | }); 124 | } 125 | rollback(version) { 126 | return __awaiter(this, void 0, void 0, function* () { 127 | try { 128 | const projectId = this.helper.getProjectId(); 129 | const author = this.helper.getProp("current_login"); 130 | const { response: { body } } = yield this.helper.toPromise(this.api.deploymentApi, this.api.deploymentApi.projectsProjectIdDeploymentVersionsGet, projectId, { 131 | limit: 1000000, 132 | page: 1 133 | }); 134 | if (body && body.data) { 135 | const deployments = body.data; 136 | const versionData = deployments.find((deployment) => deployment.version === version); 137 | if (!versionData) { 138 | throw new Error("Version not found"); 139 | } 140 | const { changelog, confirm } = yield inquirer.prompt([ 141 | { 142 | type: "text", 143 | name: "changelog", 144 | message: "Changelog:", 145 | }, 146 | { 147 | type: "confirm", 148 | name: "confirm", 149 | message: `IMPORTANT: Existing NL training data will also be rolled back to version ${versionData.version}`, 150 | default: true, 151 | } 152 | ]); 153 | const requestBody = { 154 | author, 155 | changelog, 156 | version: versionData.version 157 | }; 158 | if (confirm) { 159 | const { response } = yield this.helper.toPromise(this.api.deploymentApi, this.api.deploymentApi.deploymentsDeploymentIdRollbackPost, versionData.id, requestBody); 160 | if (response && response.body) { 161 | console.log(`Successfully rolled back to version ${versionData.version}`); 162 | } 163 | else { 164 | console.log(`Error when trying to rollback to version ${versionData.version}`); 165 | } 166 | } 167 | } 168 | } 169 | catch (e) { 170 | console.log(this.helper.wrapError(e)); 171 | } 172 | }); 173 | } 174 | } 175 | exports.default = Deployment; 176 | //# sourceMappingURL=deployment.js.map -------------------------------------------------------------------------------- /lib/components/bots/environment.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | Object.defineProperty(exports, "__esModule", { value: true }); 12 | const inquirer = require("inquirer"); 13 | const Table = require("cli-table"); 14 | class Environment { 15 | constructor(helper, api) { 16 | this.helper = helper; 17 | this.api = api; 18 | } 19 | create(slug) { 20 | return __awaiter(this, void 0, void 0, function* () { 21 | const projectId = this.helper.getProjectId(); 22 | if (slug.length > 20) { 23 | console.error("error: Namespace length can not exceed 20 characters"); 24 | return; 25 | } 26 | try { 27 | const deployment = yield this.getLatestDeployment(); 28 | if (!deployment) { 29 | throw Error("Deployment not found"); 30 | } 31 | const name = yield this.askPredefinedEnvironmentName(); 32 | const existEnvs = yield this.listEnvironment(); 33 | for (const existEnv of existEnvs) { 34 | if (existEnv.name.toLowerCase() === name.toLowerCase()) { 35 | throw new Error("Can not create environment with same name. " + 36 | "Please use command update-environment."); 37 | } 38 | } 39 | const postBody = { 40 | depId: projectId, 41 | depVersion: deployment.version, 42 | name, slug, 43 | }; 44 | const { response: { body } } = yield this.helper.toPromise(this.api.deploymentApi, this.api.deploymentApi.projectsProjectIdEnvironmentsPost, projectId, postBody); 45 | console.log(body); 46 | console.log("ENVIRONMENT CREATED SUCCESSFULLY"); 47 | } 48 | catch (e) { 49 | console.log(this.helper.wrapError(e)); 50 | } 51 | }); 52 | } 53 | list() { 54 | return __awaiter(this, void 0, void 0, function* () { 55 | try { 56 | const environmentList = yield this.listEnvironment(); 57 | const table = new Table({ 58 | head: ["Environment Name", "Environment Slug", "Environment ID", "Deployment Version"], 59 | colWidths: [30, 30, 42, 30] 60 | }); 61 | environmentList.forEach((environment) => { 62 | table.push([environment.name, String(environment.slug), environment.id, environment.depVersion]); 63 | }); 64 | console.log(table.toString()); 65 | } 66 | catch (e) { 67 | console.log(this.helper.wrapError(e)); 68 | } 69 | }); 70 | } 71 | askDeploymentId(prop = {}) { 72 | return __awaiter(this, void 0, void 0, function* () { 73 | const projectId = this.helper.getProjectId(); 74 | let page = 1; 75 | while (true) { 76 | const { response: { body } } = yield this.helper.toPromise(this.api.deploymentApi, this.api.deploymentApi.projectsProjectIdDeploymentVersionsGet, projectId, { 77 | page 78 | }); 79 | const choices = body.data.map((row) => ({ name: `(${row.version})`, value: row.version })); 80 | if (body.total > body.page * body.limit) { 81 | choices.push({ name: "Load More", value: -1 }); 82 | } 83 | const { deploymentVersion } = yield inquirer.prompt([ 84 | { 85 | type: "list", 86 | name: "deploymentVersion", 87 | message: prop.message || "Select Deployment:", 88 | paginated: false, 89 | choices 90 | }, 91 | ]); 92 | if (deploymentVersion === -1) { 93 | page++; 94 | continue; 95 | } 96 | return deploymentVersion; 97 | } 98 | }); 99 | } 100 | update(newDeploymentVersion) { 101 | return __awaiter(this, void 0, void 0, function* () { 102 | const projectId = this.helper.getProjectId(); 103 | if (!newDeploymentVersion) { 104 | newDeploymentVersion = yield this.askDeploymentId(); 105 | } 106 | if (!/[0-9]+\.[0-9]+\.[0-9]+/.test(newDeploymentVersion)) { 107 | console.error("error: Deployment version must be in the format of <0-9>.<0-9>.<0-9>"); 108 | return; 109 | } 110 | const { response: { body } } = yield this.helper.toPromise(this.api.deploymentApi, this.api.deploymentApi.projectsProjectIdDeploymentVersionsGet, projectId, { limit: 1000000 }); 111 | if (!body.data.find((data) => data.version === newDeploymentVersion)) { 112 | console.error(`error: There are no deployment with version ${newDeploymentVersion}`); 113 | return; 114 | } 115 | try { 116 | const environmentId = yield this.askEnvironmentId({ 117 | message: "Select which Environment to update: " 118 | }); 119 | const { response: { body } } = yield this.helper.toPromise(this.api.deploymentApi, this.api.deploymentApi.projectsProjectIdEnvironmentsEnvironmentIdPut, environmentId, projectId, { depVersion: newDeploymentVersion }); 120 | if (!body) { 121 | throw Error("Error updating Environment"); 122 | } 123 | return console.log(body); 124 | } 125 | catch (error) { 126 | console.log(this.helper.wrapError(error)); 127 | } 128 | }); 129 | } 130 | askEnvironmentId(prop = {}) { 131 | return __awaiter(this, void 0, void 0, function* () { 132 | const environmentList = yield this.listEnvironment(); 133 | const choices = environmentList.map((row) => ({ name: `${row.name} (${row.depVersion})`, value: row.id })); 134 | const { environmentId } = yield inquirer.prompt([ 135 | { 136 | type: "list", 137 | name: "environmentId", 138 | message: prop.message || "Select Environment:", 139 | paginated: false, 140 | choices 141 | }, 142 | ]); 143 | return environmentId; 144 | }); 145 | } 146 | askPredefinedEnvironmentName() { 147 | return __awaiter(this, void 0, void 0, function* () { 148 | const environmentNames = ["Development", "Production", "Staging"]; 149 | const choices = environmentNames.map((name) => ({ name, value: name })); 150 | const { environmentName } = yield inquirer.prompt([ 151 | { 152 | type: "list", 153 | name: "environmentName", 154 | message: "Which Environment to create?", 155 | paginated: false, 156 | choices 157 | }, 158 | ]); 159 | return environmentName; 160 | }); 161 | } 162 | getLatestDeployment() { 163 | return __awaiter(this, void 0, void 0, function* () { 164 | const projectId = this.helper.getProjectId(); 165 | const { response } = yield this.helper.toPromise(this.api.projectApi, this.api.projectApi.projectsProjectIdDeploymentGet, projectId); 166 | return response && response.body; 167 | }); 168 | } 169 | listEnvironment() { 170 | return __awaiter(this, void 0, void 0, function* () { 171 | const projectId = this.helper.getProp("projectId"); 172 | const { response: { body } } = yield this.helper.toPromise(this.api.deploymentApi, this.api.deploymentApi.projectsProjectIdEnvironmentsGet, projectId, {}); 173 | if (!body || !body.data) { 174 | throw Error("Failed to list environments."); 175 | } 176 | return body.data; 177 | }); 178 | } 179 | } 180 | exports.default = Environment; 181 | //# sourceMappingURL=environment.js.map -------------------------------------------------------------------------------- /test/users/team.spec.ts: -------------------------------------------------------------------------------- 1 | import { IConfigReader, IConfig, Config } from "merapi"; 2 | import { suite, test } from "mocha-typescript"; 3 | import { spy, stub, assert } from "sinon"; 4 | import { IHelper } from "../../interfaces/main"; 5 | import { readFileSync } from "fs"; 6 | import { safeLoad } from "js-yaml"; 7 | import Helper from "../../components/scripts/helper"; 8 | import Zaun from "../../components/api/zaun"; 9 | import Api from "../../components/api/api"; 10 | import Team from "../../components/users/team"; 11 | 12 | @suite class TeamTest { 13 | private config: IConfig; 14 | private helper: IHelper; 15 | private api: any; 16 | private session: any; 17 | private team: any; 18 | 19 | private userTokenObj = { 20 | id: '84330d2b-f2f8-499c-adee-99d2c533e7d5', 21 | type: 'user', 22 | label: 'dashboard', 23 | userId: '5c0a78c2-3a2c-447a-80d2-069761be3ef9', 24 | teamId: null, 25 | botId: null, 26 | roleId: 'ccd9b640-93c4-43b6-8523-f324f89faad6', 27 | expire: 1503789662 28 | }; 29 | private teamTokenObj = { 30 | id: 'ea43b3b6-3c7f-4bd8-98f3-e58ee699b5b3', 31 | type: 'team', 32 | label: null, 33 | userId: '8a62686d-695c-44f0-a57c-4ca19f5bf78b', 34 | teamId: 'e563ec87-dae9-45f0-a2b3-515a069fb2b0', 35 | botId: null, 36 | roleId: '81b179bc-fa56-4c27-92db-a1602abe48c2', 37 | expire: 1506907823 38 | }; 39 | private userObj = { 40 | id: '9ad603b7-5c33-432c-9752-2f4816b6bd9f', 41 | type: 'user2', 42 | label: 'dashboard', 43 | userId: '05c7f7a7-fba6-437a-8e6d-72c9f71c6352', 44 | roleId: 'b3941608-1494-4983-9b64-af746b601190', 45 | expire: 1506915325 46 | } 47 | private teamObj = { 48 | id: 'e563ec87-dae9-45f0-a2b3-515a069fb2b0', 49 | username: 'team1', 50 | password: 'password', 51 | type: 'team', 52 | email: null, 53 | profile: null, 54 | roleId: '55f7d797-a938-4f59-9c6a-cf6cd8a01d08', 55 | created_at: '2017-06-18T01:46:13.000Z', 56 | updated_at: '2017-06-18T01:46:13.000Z' 57 | } 58 | 59 | constructor() { 60 | let zaun = Zaun(); 61 | let configJson = safeLoad(readFileSync("./service.yml", "utf8")); 62 | this.config = Config.create(configJson); 63 | this.helper = new Helper(this.config); 64 | this.api = new Api(this.helper, zaun); 65 | this.team = new Team(this.helper, this.api); 66 | } 67 | 68 | @test async "function add member should add member successfully"() { 69 | let consoleLogStub = stub(console, "log"); 70 | let setPropStub = stub(this.helper, "setProp"); 71 | let getPropStub = stub(this.helper, "getProp"); 72 | let getUserInfoStub = stub(this.api.userApi, "usersUserIdGet"); 73 | let teamPostStub = stub(this.api.teamApi, "teamsTeamIdUsersUserIdPost"); 74 | let teamGetStub = stub(this.api.teamApi, "teamsTeamIdUsersGet"); 75 | 76 | getPropStub.withArgs("current_login").returns("team1"); 77 | getPropStub.withArgs("current_user_type").returns("team"); 78 | getUserInfoStub.withArgs("team1").callsFake((userId, callback) => { 79 | callback(null, this.teamObj, { body: this.teamObj }); 80 | }); 81 | getUserInfoStub.withArgs("user2").callsFake((userId, callback) => { 82 | callback(null, this.userObj, { body: this.userObj }); 83 | }); 84 | teamGetStub.callsFake((teamId, callback) => { 85 | callback(null, null, {body: [{userId: "anotherUserId"}]}); 86 | }); 87 | teamPostStub.callsFake((teamId, userId, roleId, callback) => { 88 | callback(null, null, {body: {team: this.teamObj}}); 89 | }) 90 | 91 | await this.team.addMember("user2", "roleId-test"); 92 | 93 | setPropStub.restore(); 94 | getPropStub.restore(); 95 | getUserInfoStub.restore(); 96 | teamGetStub.restore(); 97 | teamPostStub.restore(); 98 | consoleLogStub.restore(); 99 | assert.calledWith(consoleLogStub, "Success register user2 to team1"); 100 | } 101 | 102 | @test async "function add member should throw error when current login is user"() { 103 | let consoleLogStub = stub(console, "log"); 104 | let setPropStub = stub(this.helper, "setProp"); 105 | let getPropStub = stub(this.helper, "getProp"); 106 | 107 | getPropStub.withArgs("current_login").returns("user1"); 108 | getPropStub.withArgs("current_user_type").returns("user"); 109 | 110 | await this.team.addMember("user2", "roleId-test"); 111 | 112 | setPropStub.restore(); 113 | getPropStub.restore(); 114 | consoleLogStub.restore(); 115 | assert.calledWith(consoleLogStub, "Must be on team to do this operation"); 116 | } 117 | 118 | @test async "function add member should throw error when username is already in team"() { 119 | let consoleLogStub = stub(console, "log"); 120 | let setPropStub = stub(this.helper, "setProp"); 121 | let getPropStub = stub(this.helper, "getProp"); 122 | let getUserInfoStub = stub(this.api.userApi, "usersUserIdGet"); 123 | let teamGetStub = stub(this.api.teamApi, "teamsTeamIdUsersGet"); 124 | 125 | getPropStub.withArgs("current_login").returns("team1"); 126 | getPropStub.withArgs("current_user_type").returns("team"); 127 | getUserInfoStub.withArgs("team1").callsFake((userId, callback) => { 128 | callback(null, this.teamObj, { body: this.teamObj }); 129 | }); 130 | getUserInfoStub.withArgs("user2").callsFake((userId, callback) => { 131 | callback(null, this.userObj, { body: this.userObj }); 132 | }); 133 | teamGetStub.callsFake((teamId, callback) => { 134 | callback(null, null, {body: [{userId: this.userObj.id}]}); 135 | }); 136 | 137 | await this.team.addMember("user2", "roleId-test"); 138 | 139 | setPropStub.restore(); 140 | getPropStub.restore(); 141 | getUserInfoStub.restore(); 142 | teamGetStub.restore(); 143 | consoleLogStub.restore(); 144 | assert.calledWith(consoleLogStub, "User user2 already on this team"); 145 | } 146 | 147 | @test async "function remove member should remove member successfully"() { 148 | let consoleLogStub = stub(console, "log"); 149 | let setPropStub = stub(this.helper, "setProp"); 150 | let getPropStub = stub(this.helper, "getProp"); 151 | let getUserInfoStub = stub(this.api.userApi, "usersUserIdGet"); 152 | let teamDeleteStub = stub(this.api.teamApi, "teamsTeamIdUsersUserIdDelete"); 153 | let teamGetStub = stub(this.api.teamApi, "teamsTeamIdUsersGet"); 154 | let inquirerPromptStub = stub(this.helper, "inquirerPrompt").returns({ confirmation: true }) 155 | 156 | getPropStub.withArgs("current_login").returns("team1"); 157 | getPropStub.withArgs("current_user_type").returns("team"); 158 | getUserInfoStub.withArgs("team1").callsFake((userId, callback) => { 159 | callback(null, this.teamObj, { body: this.teamObj }); 160 | }); 161 | getUserInfoStub.withArgs("user2").callsFake((userId, callback) => { 162 | callback(null, this.userObj, { body: this.userObj }); 163 | }); 164 | teamGetStub.callsFake((teamId, callback) => { 165 | callback(null, null, {body: [{userId: this.userObj.id}]}); 166 | }); 167 | teamDeleteStub.callsFake((teamId, userId, callback) => { 168 | callback(null, null, {body: {team: this.teamObj}}); 169 | }) 170 | 171 | await this.team.removeMember("user2"); 172 | 173 | setPropStub.restore(); 174 | getPropStub.restore(); 175 | getUserInfoStub.restore(); 176 | teamGetStub.restore(); 177 | teamDeleteStub.restore(); 178 | consoleLogStub.restore(); 179 | inquirerPromptStub.restore(); 180 | assert.calledWith(consoleLogStub, "Success remove user2 from team1"); 181 | } 182 | 183 | @test async "function remove member should throw error when current login is user"() { 184 | let consoleLogStub = stub(console, "log"); 185 | let setPropStub = stub(this.helper, "setProp"); 186 | let getPropStub = stub(this.helper, "getProp"); 187 | let inquirerPromptStub = stub(this.helper, "inquirerPrompt").returns({ confirmation: true }) 188 | 189 | getPropStub.withArgs("current_login").returns("user1"); 190 | getPropStub.withArgs("current_user_type").returns("user"); 191 | 192 | await this.team.removeMember("user2"); 193 | 194 | setPropStub.restore(); 195 | getPropStub.restore(); 196 | consoleLogStub.restore(); 197 | inquirerPromptStub.restore(); 198 | assert.calledWith(consoleLogStub, "Must be on team to do this operation"); 199 | } 200 | 201 | @test async "function remove member should throw error when username is not member of the team"() { 202 | let consoleLogStub = stub(console, "log"); 203 | let setPropStub = stub(this.helper, "setProp"); 204 | let getPropStub = stub(this.helper, "getProp"); 205 | let getUserInfoStub = stub(this.api.userApi, "usersUserIdGet"); 206 | let teamGetStub = stub(this.api.teamApi, "teamsTeamIdUsersGet"); 207 | let inquirerPromptStub = stub(this.helper, "inquirerPrompt").returns({ confirmation: true }) 208 | 209 | getPropStub.withArgs("current_login").returns("team1"); 210 | getPropStub.withArgs("current_user_type").returns("team"); 211 | getUserInfoStub.withArgs("team1").callsFake((userId, callback) => { 212 | callback(null, this.teamObj, { body: this.teamObj }); 213 | }); 214 | getUserInfoStub.withArgs("user2").callsFake((userId, callback) => { 215 | callback(null, this.userObj, { body: this.userObj }); 216 | }); 217 | teamGetStub.callsFake((teamId, callback) => { 218 | callback(null, null, {body: [{userId: "anotherUserId"}]}); 219 | }); 220 | 221 | await this.team.removeMember("user2"); 222 | 223 | setPropStub.restore(); 224 | getPropStub.restore(); 225 | getUserInfoStub.restore(); 226 | teamGetStub.restore(); 227 | consoleLogStub.restore(); 228 | inquirerPromptStub.restore(); 229 | assert.calledWith(consoleLogStub, "User user2 not a member of this team"); 230 | } 231 | 232 | } -------------------------------------------------------------------------------- /interfaces/main.ts: -------------------------------------------------------------------------------- 1 | import { Json, IConfig, IHash, JsonObject } from "merapi"; 2 | 3 | export interface CommandOptions { 4 | noHelp?: boolean; 5 | isDefault?: boolean; 6 | } 7 | 8 | export interface ParseOptionsResult { 9 | args: string[]; 10 | unknown: string[]; 11 | } 12 | 13 | export interface Option { 14 | flags: string; 15 | required: boolean; 16 | optional: boolean; 17 | bool: boolean; 18 | short?: string; 19 | long: string; 20 | description: string; 21 | } 22 | 23 | export interface Command extends NodeJS.EventEmitter { 24 | args: string[]; 25 | 26 | /** 27 | * Set the program version to `str`. 28 | * 29 | * This method auto-registers the "-V, --version" flag 30 | * which will print the version number when passed. 31 | * 32 | * @param {string} str 33 | * @param {string} [flags] 34 | * @returns {Command} for chaining 35 | */ 36 | version(str: string, flags?: string): Command; 37 | 38 | /** 39 | * Add command `name`. 40 | * 41 | * The `.action()` callback is invoked when the 42 | * command `name` is specified via __ARGV__, 43 | * and the remaining arguments are applied to the 44 | * function for access. 45 | * 46 | * When the `name` is "*" an un-matched command 47 | * will be passed as the first arg, followed by 48 | * the rest of __ARGV__ remaining. 49 | * 50 | * @example 51 | * program 52 | * .version('0.0.1') 53 | * .option('-C, --chdir ', 'change the working directory') 54 | * .option('-c, --config ', 'set config path. defaults to ./deploy.conf') 55 | * .option('-T, --no-tests', 'ignore test hook') 56 | * 57 | * program 58 | * .command('setup') 59 | * .description('run remote setup commands') 60 | * .action(function() { 61 | * console.log('setup'); 62 | * }); 63 | * 64 | * program 65 | * .command('exec ') 66 | * .description('run the given remote command') 67 | * .action(function(cmd) { 68 | * console.log('exec "%s"', cmd); 69 | * }); 70 | * 71 | * program 72 | * .command('teardown [otherDirs...]') 73 | * .description('run teardown commands') 74 | * .action(function(dir, otherDirs) { 75 | * console.log('dir "%s"', dir); 76 | * if (otherDirs) { 77 | * otherDirs.forEach(function (oDir) { 78 | * console.log('dir "%s"', oDir); 79 | * }); 80 | * } 81 | * }); 82 | * 83 | * program 84 | * .command('*') 85 | * .description('deploy the given env') 86 | * .action(function(env) { 87 | * console.log('deploying "%s"', env); 88 | * }); 89 | * 90 | * program.parse(process.argv); 91 | * 92 | * @param {string} name 93 | * @param {string} [desc] for git-style sub-commands 94 | * @param {CommandOptions} [opts] command options 95 | * @returns {Command} the new command 96 | */ 97 | command(name: string, desc?: string, opts?: CommandOptions): Command; 98 | 99 | 100 | /** 101 | * Define argument syntax for the top-level command. 102 | * 103 | * @param {string} desc 104 | * @returns {Command} for chaining 105 | */ 106 | arguments(desc: string): Command; 107 | 108 | /** 109 | * Parse expected `args`. 110 | * 111 | * For example `["[type]"]` becomes `[{ required: false, name: 'type' }]`. 112 | * 113 | * @param {string[]} args 114 | * @returns {Command} for chaining 115 | */ 116 | parseExpectedArgs(args: string[]): Command; 117 | /** 118 | * Register callback `fn` for the command. 119 | * 120 | * @example 121 | * program 122 | * .command('help') 123 | * .description('display verbose help') 124 | * .action(function() { 125 | * // output help here 126 | * }); 127 | * 128 | * @param {(...args: any[]) => void} fn 129 | * @returns {Command} for chaining 130 | */ 131 | action(fn: (...args: any[]) => void): Command; 132 | 133 | /** 134 | * Define option with `flags`, `description` and optional 135 | * coercion `fn`. 136 | * 137 | * The `flags` string should contain both the short and long flags, 138 | * separated by comma, a pipe or space. The following are all valid 139 | * all will output this way when `--help` is used. 140 | * 141 | * "-p, --pepper" 142 | * "-p|--pepper" 143 | * "-p --pepper" 144 | * 145 | *@example 146 | * // simple boolean defaulting to false 147 | * program.option('-p, --pepper', 'add pepper'); 148 | * 149 | * --pepper 150 | * program.pepper 151 | * // => Boolean 152 | * 153 | * // simple boolean defaulting to true 154 | * program.option('-C, --no-cheese', 'remove cheese'); 155 | * 156 | * program.cheese 157 | * // => true 158 | * 159 | * --no-cheese 160 | * program.cheese 161 | * // => false 162 | * 163 | * // required argument 164 | * program.option('-C, --chdir ', 'change the working directory'); 165 | * 166 | * --chdir /tmp 167 | * program.chdir 168 | * // => "/tmp" 169 | * 170 | * // optional argument 171 | * program.option('-c, --cheese [type]', 'add cheese [marble]'); 172 | * 173 | * @param {string} flags 174 | * @param {string} [description] 175 | * @param {((arg1: any, arg2: any) => void) | RegExp} [fn] function or default 176 | * @param {*} [defaultValue] 177 | * @returns {Command} for chaining 178 | */ 179 | option(flags: string, description?: string, fn?: ((arg1: any, arg2: any) => void) | RegExp, defaultValue?: any): Command; 180 | option(flags: string, description?: string, defaultValue?: any): Command; 181 | 182 | /** 183 | * Allow unknown options on the command line. 184 | * 185 | * @param {boolean} [arg] if `true` or omitted, no error will be thrown for unknown options. 186 | * @returns {Command} for chaining 187 | */ 188 | allowUnknownOption(arg?: boolean): Command; 189 | 190 | /** 191 | * Parse `argv`, settings options and invoking commands when defined. 192 | * 193 | * @param {string[]} argv 194 | * @returns {Command} for chaining 195 | */ 196 | parse(argv: string[]): Command; 197 | 198 | /** 199 | * Parse options from `argv` returning `argv` void of these options. 200 | * 201 | * @param {string[]} argv 202 | * @returns {ParseOptionsResult} 203 | */ 204 | parseOptions(argv: string[]): ParseOptionsResult; 205 | 206 | /** 207 | * Return an object containing options as key-value pairs 208 | * 209 | * @returns {{[key: string]: string}} 210 | */ 211 | opts(): { [key: string]: string }; 212 | 213 | /** 214 | * Set the description to `str`. 215 | * 216 | * @param {string} str 217 | * @return {(Command | string)} 218 | */ 219 | description(str: string): Command; 220 | description(): string; 221 | 222 | /** 223 | * Set an alias for the command. 224 | * 225 | * @param {string} alias 226 | * @return {(Command | string)} 227 | */ 228 | alias(alias: string): Command; 229 | alias(): string; 230 | 231 | /** 232 | * Set or get the command usage. 233 | * 234 | * @param {string} str 235 | * @return {(Command | string)} 236 | */ 237 | usage(str: string): Command; 238 | usage(): string; 239 | 240 | /** 241 | * Get the name of the command. 242 | * 243 | * @return {string} 244 | */ 245 | name(): string; 246 | 247 | /** 248 | * Output help information for this command. 249 | * 250 | * @param {() => void} [cb] Callback method 251 | */ 252 | outputHelp(cb?: () => void): void; 253 | 254 | /** Output help information and exit. */ 255 | help(): void; 256 | } 257 | 258 | export interface CommandList { 259 | [name: string] : CommandDescriptor; 260 | } 261 | 262 | export interface CommandDescriptor { 263 | type?: "group" | "command" | "alias"; 264 | desc?: string; 265 | middleware?: string[]; 266 | subcommands?: CommandList; 267 | args?: string; 268 | params?: { 269 | [param: string]: { 270 | type: string, 271 | desc?: string, 272 | short?: string, 273 | bool?: boolean, 274 | value?: Json 275 | } 276 | }; 277 | handler?: string; 278 | alias?: string; 279 | } 280 | 281 | export interface ICompile { 282 | execDirectives(config: IConfig, basepath: string) : IConfig; 283 | execDirective(name: string, directive: any, dict: IHash, basepath: string) : void 284 | } 285 | 286 | export interface IHelper { 287 | getFiles(dir: string, ending: string) : string[]; 288 | loadYaml(file: string) : JsonObject; 289 | dumpYaml(file: string, object: JsonObject) : void; 290 | compareTestResult(result: Json, expect: Json) : {field: string, expect: any, result: any}[]; 291 | setProp(prop: string, value: Json, options?: JsonObject) : void; 292 | getProp(prop: string, options?: JsonObject): Json; 293 | toPromise(ctx: any, func: any, ...args: any[]): Promise; 294 | getBotId(): string; 295 | getProjectId(): string; 296 | createDirectory(path: string, mode?: number) : void; 297 | getCurrentToken() : JsonObject; 298 | loadYamlOrJsonFile(filePath: string) : JsonObject; 299 | inquirerPrompt(questions: JsonObject[]): Promise; 300 | delete() : Boolean; 301 | deleteKeyToken(username: string): JsonObject; 302 | wrapError(error : any) : string; 303 | difference(object: any, base: any): Object; 304 | checkNotificationStatus() : Boolean; 305 | addCommandSession(command:string): void; 306 | getCommandSession(): JsonObject[]; 307 | clearCommandSession(): void; 308 | sendGoogleAnalytics(event:string, action:string, data:string): void; 309 | } 310 | 311 | export interface ITester { 312 | execIntentTest(yaml: any, botApi: any, botId: string, print: (text: string) => void) : Promise>; 313 | execStateTest(yaml: any, botApi: any, botId: string, print: (text: string) => void) : Promise>; 314 | execActionsTest(yaml: any, botApi: any, botId: string, print: (text: string) => void) : Promise>; 315 | execFlowTest(yaml: any, botApi: any, botId: string, print: (text: string) => void) : Promise>; 316 | } 317 | -------------------------------------------------------------------------------- /test/bots/deployment.spec.ts: -------------------------------------------------------------------------------- 1 | import { IConfigReader, ILogger, Config, IConfig } from "merapi"; 2 | import { suite, test } from "mocha-typescript"; 3 | import { stub, spy, assert } from "sinon"; 4 | import { IHelper } from "../../interfaces/main"; 5 | import { readFileSync } from "fs"; 6 | import { safeLoad } from "js-yaml"; 7 | import Helper from "../../components/scripts/helper"; 8 | import Api from "../../components/api/api"; 9 | import Deployment from "../../components/bots/deployment"; 10 | import Zaun from "../../components/api/zaun"; 11 | import { v4 as uuid } from "uuid"; 12 | const Table = require("cli-table"); 13 | 14 | @suite class DeploymentTest { 15 | private config: IConfig; 16 | private helper: IHelper; 17 | private api: any; 18 | private deployment: any; 19 | private emptyDeploymentObj = { 20 | name: "test", 21 | botId: "739b5e9f-d5e1-44b1-93a8-954d291df170", 22 | botVersion: "1.0.5", 23 | channels: {} 24 | }; 25 | private deploymentObj = { 26 | name: "test", 27 | botId: "739b5e9f-d5e1-44b1-93a8-954d291df170", 28 | botVersion: "1.0.5", 29 | channels: { 30 | fb: "a22867dd-ce49-4afe-b7d1-3199c01e1c51", 31 | line: "b02eb207-8f8a-480d-8c32-606b0fa7dfe7" 32 | } 33 | }; 34 | private channelObj = { 35 | name: "fb", 36 | id: this.deploymentObj.channels.fb, 37 | type: "messenger", 38 | token: "tokenChannel", 39 | refreshToken: "refreshToken", 40 | secret: "secretKey", 41 | url: "http://url", 42 | webhook: "https://urlwebhook" 43 | }; 44 | 45 | private channelObjWithOptions = { 46 | name: "fb", 47 | id: this.deploymentObj.channels.fb, 48 | type: "messenger", 49 | token: "tokenChannel", 50 | refreshToken: "refreshToken", 51 | secret: "secretKey", 52 | url: "http://url", 53 | additionalOptions: { botEmail: "test@test.com" }, 54 | webhook: "https://urlwebhook" 55 | }; 56 | private webhook = "https://kanal.katalabs.io"; 57 | 58 | constructor() { 59 | const configJson = safeLoad(readFileSync("./service.yml", "utf8")); 60 | const zaun = Zaun(); 61 | this.config = Config.create(configJson); 62 | this.helper = new Helper(this.config); 63 | this.api = new Api(this.helper, zaun); 64 | this.deployment = new Deployment(this.helper, this.api); 65 | } 66 | 67 | @test public async "function create deployment should create deployment successfully"() { 68 | const projectIdStub = stub(this.helper, "getProjectId").returns("projectId1"); 69 | const projectId = "projectId1"; 70 | const projectApiStub = stub(this.api.projectApi, "projectsProjectIdGet").callsFake((projectId, callback) => { 71 | callback(null, null, { body: { project: { nluRevision: "nlurev1", botRevision: "botrev1", cmsRevision: "cmsrev1", } } }) 72 | }); 73 | const botRevApiStub = stub(this.api.botApi, "projectsProjectIdBotRevisionsGet").callsFake((projectId, callback) => { 74 | callback(null, null, { body: { data: [ {revision: "botrev1"} ] } }); 75 | }); 76 | const nluRevApiStub = stub(this.api.projectApi, "projectsProjectIdNluRevisionsGet").callsFake((projectId, callback) => { 77 | callback(null, null, { body: { data: [ {revision: "nlurev1"} ] } }); 78 | }); 79 | const cmsRevApiStub = stub(this.api.projectApi, "projectsProjectIdCmsRevisionsGet").callsFake((projectId, callback) => { 80 | callback(null, null, { body: { data: [ {revision: "cmsrev1"} ] } }); 81 | }); 82 | const prevDeplymentStub = stub(this.api.projectApi, "projectsProjectIdDeploymentGet").callsFake((projectId, callback) => { 83 | callback(null, null, { body: { version: "0.0.1" } }); 84 | }); 85 | const deploymentApiStub = stub(this.api.deploymentApi, "projectsProjectIdDeploymentVersionsPost").callsFake(({}, projectId, callback) => { 86 | callback(null, null, { body: { result: "ok" }}); 87 | }); 88 | 89 | const consoleLogStub = stub(console, "log"); 90 | 91 | await this.deployment.create(); 92 | 93 | consoleLogStub.restore(); 94 | assert.calledWith(consoleLogStub, "Succesfully create Deployment to version 0.0.2"); 95 | } 96 | 97 | @test public async "function create deployment with patch version should create deployment successfully"() { 98 | const projectIdStub = stub(this.helper, "getProjectId").returns("projectId1"); 99 | const projectId = "projectId1"; 100 | const projectApiStub = stub(this.api.projectApi, "projectsProjectIdGet").callsFake((projectId, callback) => { 101 | callback(null, null, { body: { project: { nluRevision: "nlurev1", botRevision: "botrev1", cmsRevision: "cmsrev1", } } }) 102 | }); 103 | const botRevApiStub = stub(this.api.botApi, "projectsProjectIdBotRevisionsGet").callsFake((projectId, callback) => { 104 | callback(null, null, { body: { data: [ {revision: "botrev1"} ] } }); 105 | }); 106 | const nluRevApiStub = stub(this.api.projectApi, "projectsProjectIdNluRevisionsGet").callsFake((projectId, callback) => { 107 | callback(null, null, { body: { data: [ {revision: "nlurev1"} ] } }); 108 | }); 109 | const cmsRevApiStub = stub(this.api.projectApi, "projectsProjectIdCmsRevisionsGet").callsFake((projectId, callback) => { 110 | callback(null, null, { body: { data: [ {revision: "cmsrev1"} ] } }); 111 | }); 112 | const prevDeplymentStub = stub(this.api.projectApi, "projectsProjectIdDeploymentGet").callsFake((projectId, callback) => { 113 | callback(null, null, { body: { version: "0.0.1" } }); 114 | }); 115 | const deploymentApiStub = stub(this.api.deploymentApi, "projectsProjectIdDeploymentVersionsPost").callsFake(({}, projectId, callback) => { 116 | callback(null, null, { body: { result: "ok" }}); 117 | }); 118 | 119 | const consoleLogStub = stub(console, "log"); 120 | 121 | await this.deployment.create("patch"); 122 | 123 | consoleLogStub.restore(); 124 | assert.calledWith(consoleLogStub, "Succesfully create Deployment to version 0.0.2"); 125 | } 126 | 127 | @test public async "function create deployment with minor version should create deployment successfully"() { 128 | const projectIdStub = stub(this.helper, "getProjectId").returns("projectId1"); 129 | const projectId = "projectId1"; 130 | const projectApiStub = stub(this.api.projectApi, "projectsProjectIdGet").callsFake((projectId, callback) => { 131 | callback(null, null, { body: { project: { nluRevision: "nlurev1", botRevision: "botrev1", cmsRevision: "cmsrev1", } } }) 132 | }); 133 | const botRevApiStub = stub(this.api.botApi, "projectsProjectIdBotRevisionsGet").callsFake((projectId, callback) => { 134 | callback(null, null, { body: { data: [ {revision: "botrev1"} ] } }); 135 | }); 136 | const nluRevApiStub = stub(this.api.projectApi, "projectsProjectIdNluRevisionsGet").callsFake((projectId, callback) => { 137 | callback(null, null, { body: { data: [ {revision: "nlurev1"} ] } }); 138 | }); 139 | const cmsRevApiStub = stub(this.api.projectApi, "projectsProjectIdCmsRevisionsGet").callsFake((projectId, callback) => { 140 | callback(null, null, { body: { data: [ {revision: "cmsrev1"} ] } }); 141 | }); 142 | const prevDeplymentStub = stub(this.api.projectApi, "projectsProjectIdDeploymentGet").callsFake((projectId, callback) => { 143 | callback(null, null, { body: { version: "0.0.1" } }); 144 | }); 145 | const deploymentApiStub = stub(this.api.deploymentApi, "projectsProjectIdDeploymentVersionsPost").callsFake(({}, projectId, callback) => { 146 | callback(null, null, { body: { result: "ok" }}); 147 | }); 148 | 149 | const consoleLogStub = stub(console, "log"); 150 | 151 | await this.deployment.create("minor"); 152 | 153 | consoleLogStub.restore(); 154 | assert.calledWith(consoleLogStub, "Succesfully create Deployment to version 0.1.0"); 155 | } 156 | 157 | @test public async "function create deployment with major version should create deployment successfully"() { 158 | const projectIdStub = stub(this.helper, "getProjectId").returns("projectId1"); 159 | const projectId = "projectId1"; 160 | const projectApiStub = stub(this.api.projectApi, "projectsProjectIdGet").callsFake((projectId, callback) => { 161 | callback(null, null, { body: { project: { nluRevision: "nlurev1", botRevision: "botrev1", cmsRevision: "cmsrev1", } } }) 162 | }); 163 | const botRevApiStub = stub(this.api.botApi, "projectsProjectIdBotRevisionsGet").callsFake((projectId, callback) => { 164 | callback(null, null, { body: { data: [ {revision: "botrev1"} ] } }); 165 | }); 166 | const nluRevApiStub = stub(this.api.projectApi, "projectsProjectIdNluRevisionsGet").callsFake((projectId, callback) => { 167 | callback(null, null, { body: { data: [ {revision: "nlurev1"} ] } }); 168 | }); 169 | const cmsRevApiStub = stub(this.api.projectApi, "projectsProjectIdCmsRevisionsGet").callsFake((projectId, callback) => { 170 | callback(null, null, { body: { data: [ {revision: "cmsrev1"} ] } }); 171 | }); 172 | const prevDeplymentStub = stub(this.api.projectApi, "projectsProjectIdDeploymentGet").callsFake((projectId, callback) => { 173 | callback(null, null, { body: { version: "0.0.1" } }); 174 | }); 175 | const deploymentApiStub = stub(this.api.deploymentApi, "projectsProjectIdDeploymentVersionsPost").callsFake(({}, projectId, callback) => { 176 | callback(null, null, { body: { result: "ok" }}); 177 | }); 178 | 179 | const consoleLogStub = stub(console, "log"); 180 | 181 | await this.deployment.create("major"); 182 | 183 | consoleLogStub.restore(); 184 | assert.calledWith(consoleLogStub, "Succesfully create Deployment to version 1.0.0"); 185 | } 186 | 187 | @test public async "function list deployment should list deployment successfully"() { 188 | const projectIdStub = stub(this.helper, "getProjectId").returns("projectId1"); 189 | const projectId = "projectId1"; 190 | const projectApiStub = stub(this.api.deploymentApi, "projectsProjectIdDeploymentVersionsGet").callsFake((projectId, {}, callback) => { 191 | callback(null, null, { body: { data: [{ name: "depl01", version: "0.0.1", botRevision: "ah42das", }] } }) 192 | }); 193 | 194 | const consoleErrorStub = stub(console, "error"); 195 | const consoleLogStub = stub(console, "log"); 196 | await this.deployment.list(); 197 | consoleErrorStub.restore(); 198 | consoleLogStub.restore(); 199 | assert.notCalled(consoleErrorStub); 200 | } 201 | 202 | 203 | } 204 | -------------------------------------------------------------------------------- /components/scripts/helper.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Component, JsonObject, Json, IConfig } from "merapi"; 3 | import * as _ from "lodash"; 4 | 5 | const Catch = require("catch-decorator"); 6 | const yaml = require("js-yaml"); 7 | const fs = require("fs"); 8 | const os = require("os"); 9 | const path = require("path"); 10 | const inquirer = require("inquirer"); 11 | const analytics = require("universal-analytics"); 12 | 13 | function wrapError(error: any) { 14 | let errorMessage; 15 | 16 | if (error.response && error.response.body && error.response.body.message) { 17 | errorMessage = error.response.body.message; 18 | } else if (error.response && error.response.body) { 19 | errorMessage = error.response.body; 20 | } else { 21 | errorMessage = error.message; 22 | } 23 | 24 | return errorMessage; 25 | } 26 | 27 | export const CatchError = Catch(Error, (error: any) => { 28 | console.log("Error"); 29 | console.error(wrapError(error)); 30 | }); 31 | 32 | 33 | export default class Helper extends Component { 34 | constructor(private config : IConfig) { 35 | super(); 36 | } 37 | 38 | public getFiles(dir : string, ending : string) : string[] { 39 | const fileList = fs.readdirSync(dir); 40 | const res = []; 41 | for (let i = 0; i < fileList.length; i++) { 42 | const stat = fs.statSync(path.resolve(dir, fileList[i])); 43 | if (stat.isDirectory()) { 44 | res.push(...this.getFiles(dir + "/" + fileList[i], ending)); 45 | } else if (stat.isFile() && fileList[i].endsWith(ending)) { 46 | res.push(dir + "/" + fileList[i]); 47 | } 48 | } 49 | return res; 50 | } 51 | 52 | public loadYaml(file : string) : JsonObject { 53 | return yaml.safeLoad(fs.readFileSync(file, "utf8")); 54 | } 55 | 56 | public dumpYaml(file : string, object : JsonObject) : void { 57 | fs.writeFileSync(file, yaml.safeDump(object, { indent: 4, lineWidth: 150 }), "utf8"); 58 | } 59 | 60 | public compareTestResult(result : Json, expect : Json) : { field : string, expect : any, result : any }[] { 61 | if (!result) { return null; } 62 | const errors = []; 63 | const expected = this.config.create(expect).flatten(); 64 | const res = this.config.create(result); 65 | for (const i in expected) { 66 | const value = res.get(i); 67 | if (value !== expected[i]) { 68 | errors.push({ field: i, expect: expected[i], result: value }); 69 | } 70 | } 71 | return errors; 72 | } 73 | 74 | public setProp(prop : string, value : string, options? : JsonObject) : void { 75 | const jsonPath = `${os.homedir()}/.katajson`; 76 | let jsonProp; 77 | 78 | if (fs.existsSync(jsonPath)) { 79 | jsonProp = JSON.parse(fs.readFileSync(jsonPath, "utf8")); 80 | } else { 81 | jsonProp = {}; 82 | } 83 | 84 | jsonProp[prop] = value; 85 | 86 | fs.writeFileSync(jsonPath, JSON.stringify(jsonProp), "utf8"); 87 | } 88 | 89 | public getProp(prop : string, options? : JsonObject) : Json { 90 | const jsonPath = `${os.homedir()}/.katajson`; 91 | let jsonProp; 92 | 93 | if (fs.existsSync(jsonPath)) { 94 | jsonProp = JSON.parse(fs.readFileSync(jsonPath, "utf8")); 95 | } else { 96 | jsonProp = {}; 97 | } 98 | 99 | return jsonProp[prop]; 100 | } 101 | 102 | public delete() : Boolean { 103 | const jsonPath = `${os.homedir()}/.katajson`; 104 | 105 | if (fs.existsSync(jsonPath)) { 106 | fs.unlinkSync(jsonPath); 107 | return true; 108 | } 109 | 110 | return false; 111 | } 112 | 113 | public deleteKeyToken(userName: string) { 114 | const jsonPath = `${os.homedir()}/.katajson`; 115 | let jsonProp; 116 | 117 | if (fs.existsSync(jsonPath)) { 118 | jsonProp = JSON.parse(fs.readFileSync(jsonPath, "utf8")); 119 | // if userName token exist 120 | if (userName in jsonProp.token) { 121 | jsonProp.current_login = "admin"; 122 | delete jsonProp.token[userName]; 123 | delete jsonProp.projectId; 124 | delete jsonProp.projectName; 125 | delete jsonProp.isImpersonate; 126 | } else { 127 | return new Error(`Failed to unimpersonate ${(userName)}`); 128 | } 129 | } else { 130 | jsonProp = {}; 131 | } 132 | fs.writeFileSync(jsonPath, JSON.stringify(jsonProp), "utf8"); 133 | return jsonProp; 134 | } 135 | 136 | public toPromise(ctx : any, func : any, ...args : any[]) : Promise { 137 | return new Promise((resolve, reject) => { 138 | args.push((error : Error, data : any, response : Response) => { 139 | if (error) { 140 | reject(error); 141 | } else { 142 | resolve({ data, response }); 143 | } 144 | }); 145 | 146 | func.apply(ctx, args); 147 | }); 148 | } 149 | 150 | public getBotId() : string { 151 | const desc = this.loadYaml("./bot.yml"); 152 | 153 | return desc.id as string; 154 | } 155 | 156 | public getProjectId(): string { 157 | return this.getProp("projectId") as string; 158 | } 159 | 160 | public createDirectory(dirPath: string, mode?: number) { 161 | if (!fs.existsSync(dirPath)) { 162 | fs.mkdirSync(dirPath, mode); 163 | } 164 | } 165 | 166 | public getCurrentToken() : JsonObject { 167 | const currentLogin = this.getProp("current_login") as string || ""; 168 | const tokenProp = (this.getProp("token") || {}) as JsonObject; 169 | 170 | return { 171 | currentLogin, 172 | token: tokenProp[currentLogin] 173 | }; 174 | } 175 | public loadYamlOrJsonFile(filePath : string) { 176 | if (!fs.existsSync(filePath)) { 177 | return new Error("FILE NOT FOUND"); 178 | } 179 | 180 | const fileExt = path.extname(filePath); 181 | 182 | if (fileExt === ".json") { 183 | return JSON.parse(fs.readFileSync(filePath, "utf8")); 184 | } else if (fileExt === ".yml" || fileExt === ".yaml") { 185 | return this.loadYaml(filePath); 186 | } else if (fileExt === ".txt") { 187 | return fs.readFileSync(filePath, "utf8"); 188 | } else { 189 | return new Error("UNSUPPORTED FILE TYPE"); 190 | } 191 | } 192 | 193 | public async inquirerPrompt(questions : JsonObject[]) : Promise { 194 | return inquirer.prompt(questions); 195 | } 196 | 197 | public wrapError(error: any) { 198 | const errorMessage: string = wrapError(error); 199 | 200 | const commands: JsonObject[] = this.getCommandSession(); 201 | // Note: error might happen even after clearCommandSession is called 202 | // this might results in an empty command, we do not want to track empty command error 203 | if (commands.length > 0) { 204 | const lastCommand: string = commands[commands.length - 1].command as string; 205 | const mainCommand: string = lastCommand.split(" ")[0]; 206 | 207 | this.sendGoogleAnalytics("debug", mainCommand, lastCommand, commands, errorMessage); 208 | this.clearCommandSession(); 209 | } 210 | 211 | return errorMessage; 212 | } 213 | 214 | public difference(object: any, base: any) { 215 | function changes(object: any, base: any) { 216 | return _.transform(object, function(result: any, value, key) { 217 | if (!_.isEqual(value, base[key])) { 218 | result[key] = (_.isObject(value) && _.isObject(base[key])) ? changes(value, base[key]) : value; 219 | } 220 | }); 221 | } 222 | 223 | return changes(object, base); 224 | } 225 | 226 | public viewConfig() : void { 227 | const jsonPath = `${os.homedir()}/.katajson`; 228 | let jsonProp; 229 | 230 | if (fs.existsSync(jsonPath)) { 231 | jsonProp = JSON.parse(fs.readFileSync(jsonPath, "utf8")); 232 | delete jsonProp.first_login; 233 | } else { 234 | jsonProp = ".katajson file not found"; 235 | } 236 | 237 | console.log(jsonProp); 238 | } 239 | 240 | public checkNotificationStatus(): Boolean { 241 | const jsonPath = `${os.homedir()}/.katanotif`; 242 | 243 | if (fs.existsSync(jsonPath)) { 244 | return true 245 | } else { 246 | fs.writeFileSync(jsonPath, "true", "utf8"); 247 | return false 248 | } 249 | } 250 | 251 | public addCommandSession(command:string): void { 252 | const jsonPath = `${os.homedir()}/.katacommand`; 253 | let jsonData:JsonObject[] = []; 254 | 255 | if (fs.existsSync(jsonPath)) jsonData = JSON.parse(fs.readFileSync(jsonPath, "utf8")); 256 | 257 | if (jsonData.length > 0) { 258 | const lastData:JsonObject = jsonData[jsonData.length - 1] 259 | const diff:number = Math.abs(Number(lastData.timestamp) - new Date().getTime()) / 36e5; 260 | 261 | if (diff >= 1) jsonData = [] //Lebih dari 1 jam ? 262 | } 263 | 264 | jsonData.push({ timestamp: new Date().getTime(), command: command }) 265 | 266 | fs.writeFileSync(jsonPath, JSON.stringify(jsonData), "utf8"); 267 | } 268 | 269 | public getCommandSession(): JsonObject[] { 270 | const jsonPath = `${os.homedir()}/.katacommand`; 271 | let jsonData:JsonObject[] = []; 272 | 273 | if (fs.existsSync(jsonPath)) jsonData = JSON.parse(fs.readFileSync(jsonPath, "utf8")); 274 | 275 | return jsonData 276 | } 277 | 278 | public clearCommandSession(): void { 279 | const jsonPath = `${os.homedir()}/.katacommand`; 280 | 281 | fs.writeFileSync(jsonPath, "[]", "utf8"); 282 | } 283 | 284 | public sendGoogleAnalytics(event:string, action:string, command:string, lastSession?:JsonObject[], errorMessage?:string): void { 285 | let firstLogin = this.getProp("first_login") as JsonObject; 286 | let projectId = this.getProp("projectId") as string; 287 | let projectName = this.getProp("projectName") as string; 288 | 289 | if (!firstLogin) firstLogin = { id: null, username: null, type: null } 290 | if (!projectId) projectId = null 291 | if (!projectName) projectName = null 292 | 293 | const version = this.config.default("version", "1.0.0") 294 | const google = analytics(this.config.default("config.trackingId", "UA-131926842-1"), firstLogin.id); 295 | 296 | const data:JsonObject = { 297 | userId: firstLogin.id, 298 | username: firstLogin.username, 299 | currentUserType: firstLogin.type, 300 | activeProjectId: projectId, 301 | activeProjectName: projectName, 302 | command: command, 303 | versionCLI: version, 304 | timestamp: new Date().getTime() 305 | } 306 | 307 | if (lastSession) data.lastSession = lastSession 308 | if (errorMessage) data.errorMessage = errorMessage 309 | 310 | google.event(event, action, JSON.stringify(data), (err:any) => { 311 | if (err) console.log(this.wrapError(err)); 312 | }) 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /lib/components/scripts/helper.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | Object.defineProperty(exports, "__esModule", { value: true }); 12 | exports.CatchError = void 0; 13 | const merapi_1 = require("merapi"); 14 | const _ = require("lodash"); 15 | const Catch = require("catch-decorator"); 16 | const yaml = require("js-yaml"); 17 | const fs = require("fs"); 18 | const os = require("os"); 19 | const path = require("path"); 20 | const inquirer = require("inquirer"); 21 | const analytics = require("universal-analytics"); 22 | function wrapError(error) { 23 | let errorMessage; 24 | if (error.response && error.response.body && error.response.body.message) { 25 | errorMessage = error.response.body.message; 26 | } 27 | else if (error.response && error.response.body) { 28 | errorMessage = error.response.body; 29 | } 30 | else { 31 | errorMessage = error.message; 32 | } 33 | return errorMessage; 34 | } 35 | exports.CatchError = Catch(Error, (error) => { 36 | console.log("Error"); 37 | console.error(wrapError(error)); 38 | }); 39 | class Helper extends merapi_1.Component { 40 | constructor(config) { 41 | super(); 42 | this.config = config; 43 | } 44 | getFiles(dir, ending) { 45 | const fileList = fs.readdirSync(dir); 46 | const res = []; 47 | for (let i = 0; i < fileList.length; i++) { 48 | const stat = fs.statSync(path.resolve(dir, fileList[i])); 49 | if (stat.isDirectory()) { 50 | res.push(...this.getFiles(dir + "/" + fileList[i], ending)); 51 | } 52 | else if (stat.isFile() && fileList[i].endsWith(ending)) { 53 | res.push(dir + "/" + fileList[i]); 54 | } 55 | } 56 | return res; 57 | } 58 | loadYaml(file) { 59 | return yaml.safeLoad(fs.readFileSync(file, "utf8")); 60 | } 61 | dumpYaml(file, object) { 62 | fs.writeFileSync(file, yaml.safeDump(object, { indent: 4, lineWidth: 150 }), "utf8"); 63 | } 64 | compareTestResult(result, expect) { 65 | if (!result) { 66 | return null; 67 | } 68 | const errors = []; 69 | const expected = this.config.create(expect).flatten(); 70 | const res = this.config.create(result); 71 | for (const i in expected) { 72 | const value = res.get(i); 73 | if (value !== expected[i]) { 74 | errors.push({ field: i, expect: expected[i], result: value }); 75 | } 76 | } 77 | return errors; 78 | } 79 | setProp(prop, value, options) { 80 | const jsonPath = `${os.homedir()}/.katajson`; 81 | let jsonProp; 82 | if (fs.existsSync(jsonPath)) { 83 | jsonProp = JSON.parse(fs.readFileSync(jsonPath, "utf8")); 84 | } 85 | else { 86 | jsonProp = {}; 87 | } 88 | jsonProp[prop] = value; 89 | fs.writeFileSync(jsonPath, JSON.stringify(jsonProp), "utf8"); 90 | } 91 | getProp(prop, options) { 92 | const jsonPath = `${os.homedir()}/.katajson`; 93 | let jsonProp; 94 | if (fs.existsSync(jsonPath)) { 95 | jsonProp = JSON.parse(fs.readFileSync(jsonPath, "utf8")); 96 | } 97 | else { 98 | jsonProp = {}; 99 | } 100 | return jsonProp[prop]; 101 | } 102 | delete() { 103 | const jsonPath = `${os.homedir()}/.katajson`; 104 | if (fs.existsSync(jsonPath)) { 105 | fs.unlinkSync(jsonPath); 106 | return true; 107 | } 108 | return false; 109 | } 110 | deleteKeyToken(userName) { 111 | const jsonPath = `${os.homedir()}/.katajson`; 112 | let jsonProp; 113 | if (fs.existsSync(jsonPath)) { 114 | jsonProp = JSON.parse(fs.readFileSync(jsonPath, "utf8")); 115 | // if userName token exist 116 | if (userName in jsonProp.token) { 117 | jsonProp.current_login = "admin"; 118 | delete jsonProp.token[userName]; 119 | delete jsonProp.projectId; 120 | delete jsonProp.projectName; 121 | delete jsonProp.isImpersonate; 122 | } 123 | else { 124 | return new Error(`Failed to unimpersonate ${(userName)}`); 125 | } 126 | } 127 | else { 128 | jsonProp = {}; 129 | } 130 | fs.writeFileSync(jsonPath, JSON.stringify(jsonProp), "utf8"); 131 | return jsonProp; 132 | } 133 | toPromise(ctx, func, ...args) { 134 | return new Promise((resolve, reject) => { 135 | args.push((error, data, response) => { 136 | if (error) { 137 | reject(error); 138 | } 139 | else { 140 | resolve({ data, response }); 141 | } 142 | }); 143 | func.apply(ctx, args); 144 | }); 145 | } 146 | getBotId() { 147 | const desc = this.loadYaml("./bot.yml"); 148 | return desc.id; 149 | } 150 | getProjectId() { 151 | return this.getProp("projectId"); 152 | } 153 | createDirectory(dirPath, mode) { 154 | if (!fs.existsSync(dirPath)) { 155 | fs.mkdirSync(dirPath, mode); 156 | } 157 | } 158 | getCurrentToken() { 159 | const currentLogin = this.getProp("current_login") || ""; 160 | const tokenProp = (this.getProp("token") || {}); 161 | return { 162 | currentLogin, 163 | token: tokenProp[currentLogin] 164 | }; 165 | } 166 | loadYamlOrJsonFile(filePath) { 167 | if (!fs.existsSync(filePath)) { 168 | return new Error("FILE NOT FOUND"); 169 | } 170 | const fileExt = path.extname(filePath); 171 | if (fileExt === ".json") { 172 | return JSON.parse(fs.readFileSync(filePath, "utf8")); 173 | } 174 | else if (fileExt === ".yml" || fileExt === ".yaml") { 175 | return this.loadYaml(filePath); 176 | } 177 | else if (fileExt === ".txt") { 178 | return fs.readFileSync(filePath, "utf8"); 179 | } 180 | else { 181 | return new Error("UNSUPPORTED FILE TYPE"); 182 | } 183 | } 184 | inquirerPrompt(questions) { 185 | return __awaiter(this, void 0, void 0, function* () { 186 | return inquirer.prompt(questions); 187 | }); 188 | } 189 | wrapError(error) { 190 | const errorMessage = wrapError(error); 191 | const commands = this.getCommandSession(); 192 | // Note: error might happen even after clearCommandSession is called 193 | // this might results in an empty command, we do not want to track empty command error 194 | if (commands.length > 0) { 195 | const lastCommand = commands[commands.length - 1].command; 196 | const mainCommand = lastCommand.split(" ")[0]; 197 | this.sendGoogleAnalytics("debug", mainCommand, lastCommand, commands, errorMessage); 198 | this.clearCommandSession(); 199 | } 200 | return errorMessage; 201 | } 202 | difference(object, base) { 203 | function changes(object, base) { 204 | return _.transform(object, function (result, value, key) { 205 | if (!_.isEqual(value, base[key])) { 206 | result[key] = (_.isObject(value) && _.isObject(base[key])) ? changes(value, base[key]) : value; 207 | } 208 | }); 209 | } 210 | return changes(object, base); 211 | } 212 | viewConfig() { 213 | const jsonPath = `${os.homedir()}/.katajson`; 214 | let jsonProp; 215 | if (fs.existsSync(jsonPath)) { 216 | jsonProp = JSON.parse(fs.readFileSync(jsonPath, "utf8")); 217 | delete jsonProp.first_login; 218 | } 219 | else { 220 | jsonProp = ".katajson file not found"; 221 | } 222 | console.log(jsonProp); 223 | } 224 | checkNotificationStatus() { 225 | const jsonPath = `${os.homedir()}/.katanotif`; 226 | if (fs.existsSync(jsonPath)) { 227 | return true; 228 | } 229 | else { 230 | fs.writeFileSync(jsonPath, "true", "utf8"); 231 | return false; 232 | } 233 | } 234 | addCommandSession(command) { 235 | const jsonPath = `${os.homedir()}/.katacommand`; 236 | let jsonData = []; 237 | if (fs.existsSync(jsonPath)) 238 | jsonData = JSON.parse(fs.readFileSync(jsonPath, "utf8")); 239 | if (jsonData.length > 0) { 240 | const lastData = jsonData[jsonData.length - 1]; 241 | const diff = Math.abs(Number(lastData.timestamp) - new Date().getTime()) / 36e5; 242 | if (diff >= 1) 243 | jsonData = []; //Lebih dari 1 jam ? 244 | } 245 | jsonData.push({ timestamp: new Date().getTime(), command: command }); 246 | fs.writeFileSync(jsonPath, JSON.stringify(jsonData), "utf8"); 247 | } 248 | getCommandSession() { 249 | const jsonPath = `${os.homedir()}/.katacommand`; 250 | let jsonData = []; 251 | if (fs.existsSync(jsonPath)) 252 | jsonData = JSON.parse(fs.readFileSync(jsonPath, "utf8")); 253 | return jsonData; 254 | } 255 | clearCommandSession() { 256 | const jsonPath = `${os.homedir()}/.katacommand`; 257 | fs.writeFileSync(jsonPath, "[]", "utf8"); 258 | } 259 | sendGoogleAnalytics(event, action, command, lastSession, errorMessage) { 260 | let firstLogin = this.getProp("first_login"); 261 | let projectId = this.getProp("projectId"); 262 | let projectName = this.getProp("projectName"); 263 | if (!firstLogin) 264 | firstLogin = { id: null, username: null, type: null }; 265 | if (!projectId) 266 | projectId = null; 267 | if (!projectName) 268 | projectName = null; 269 | const version = this.config.default("version", "1.0.0"); 270 | const google = analytics(this.config.default("config.trackingId", "UA-131926842-1"), firstLogin.id); 271 | const data = { 272 | userId: firstLogin.id, 273 | username: firstLogin.username, 274 | currentUserType: firstLogin.type, 275 | activeProjectId: projectId, 276 | activeProjectName: projectName, 277 | command: command, 278 | versionCLI: version, 279 | timestamp: new Date().getTime() 280 | }; 281 | if (lastSession) 282 | data.lastSession = lastSession; 283 | if (errorMessage) 284 | data.errorMessage = errorMessage; 285 | google.event(event, action, JSON.stringify(data), (err) => { 286 | if (err) 287 | console.log(this.wrapError(err)); 288 | }); 289 | } 290 | } 291 | exports.default = Helper; 292 | //# sourceMappingURL=helper.js.map -------------------------------------------------------------------------------- /components/projects/project.ts: -------------------------------------------------------------------------------- 1 | 2 | import Api from "components/api/api"; 3 | import { IHelper } from "interfaces/main"; 4 | import { JsonObject } from "merapi"; 5 | import inquirer = require("inquirer"); 6 | const Table = require("cli-table"); 7 | const colors = require("colors/safe"); 8 | 9 | export default class Project { 10 | constructor( 11 | private api: Api, 12 | private helper: IHelper, 13 | ) { } 14 | 15 | public async create() { 16 | let projectData: Record = { name: "" }; 17 | while (true) { 18 | projectData = await this.helper.inquirerPrompt([ 19 | { 20 | type: "text", 21 | name: "name", 22 | message: "Project name:", 23 | }, 24 | { 25 | type: "text", 26 | name: "description", 27 | message: "Project description:", 28 | }, 29 | ]); 30 | if (projectData.name.length > 20) { 31 | console.error("error: Project name length can not exceed 20 characters"); 32 | } 33 | else if (!/^[A-Za-z][A-Za-z0-9_-]*[A-Za-z0-9]$/.test(projectData.name)) { 34 | console.error("error: Project name must start with alphabet characters and contains only aplhanumeric character, dash, or underscore"); 35 | } 36 | else { 37 | break; 38 | } 39 | } 40 | const inquiredOptions = await this.helper.inquirerPrompt([ 41 | { 42 | type: "number", 43 | name: "timezone", 44 | message: "Timezone (UTC)", 45 | default: 7, 46 | }, 47 | ]); 48 | inquiredOptions.timezone = Number(inquiredOptions.timezone) 49 | const options = { bot: true, cms: true, nlu: true, ...inquiredOptions }; 50 | 51 | let nluOptions: any = {}; 52 | if (options.nlu) { 53 | nluOptions = await this.helper.inquirerPrompt([ 54 | { 55 | type: "text", 56 | name: "nluLang", 57 | message: "NLU Language", 58 | default: "id", 59 | }, 60 | { 61 | type: "confirm", 62 | name: "privateNlu", 63 | message: "Is private Nlu?", 64 | default: true, 65 | }, 66 | ]); 67 | nluOptions.nluVisibility = nluOptions.privateNlu ? "private" : "public"; 68 | delete nluOptions.privateNlu; 69 | } 70 | 71 | const requestBody = { ...projectData, options: { ...options, ...nluOptions } }; 72 | 73 | try { 74 | const { response } = await this.helper.toPromise( 75 | this.api.projectApi, this.api.projectApi.projectsPost, requestBody 76 | ); 77 | 78 | if (response && response.body && response.body.id) { 79 | const project = response.body; 80 | const projectId = project.id; 81 | this.helper.setProp("projectId", projectId); 82 | console.log(colors.green(`Project "${project.name}" (${projectId}) is successfully created`)); 83 | return; 84 | } 85 | 86 | } catch (e) { 87 | console.error(this.helper.wrapError(e)); 88 | } 89 | } 90 | 91 | public async update(projectName?: string) { 92 | let chosen = null 93 | 94 | if (projectName) { 95 | chosen = await this.getDataByName(projectName) 96 | } else { 97 | chosen = await this.choose() 98 | } 99 | 100 | if (chosen) { 101 | const { description, privateNlu } = await this.helper.inquirerPrompt([ 102 | { 103 | type: "text", 104 | name: "description", 105 | message: "Project description:", 106 | }, 107 | { 108 | type: "confirm", 109 | name: "privateNlu", 110 | message: "Is private Nlu?", 111 | default: true, 112 | } 113 | ]); 114 | 115 | const nluVisibility = privateNlu ? 'private' : 'public'; 116 | 117 | const requestBody = { 118 | id: chosen.id, 119 | name: chosen.name, 120 | description: description, 121 | options: { 122 | timezone: chosen.options.timezone, 123 | nluLang: chosen.options.nluLang, 124 | nluVisibility: nluVisibility, 125 | nluId: chosen.options.nluId 126 | } 127 | } 128 | 129 | const { response } = await this.helper.toPromise(this.api.projectApi, this.api.projectApi.projectsProjectIdPut, chosen.id, requestBody); 130 | if (response && response.body) { 131 | console.log(`Project ${chosen.name} has been updated.`) 132 | } else { 133 | console.log("Failed when trying update project") 134 | } 135 | } else { 136 | console.log(`Project ${projectName} is not found`) 137 | } 138 | } 139 | 140 | public async list() { 141 | try { 142 | const { response } = await this.helper.toPromise(this.api.projectApi, this.api.projectApi.projectsGet, {}); 143 | 144 | if (response && response.body && response.body.data) { 145 | const table = new Table({ 146 | head: ["Project ID", "Project Name"], 147 | colWidths: [38, 32] 148 | }); 149 | response.body.data.forEach((project: JsonObject) => { 150 | table.push([project.id, project.name]); 151 | }); 152 | console.log(table.toString()); 153 | } 154 | } catch (e) { 155 | console.error(this.helper.wrapError(e)); 156 | } 157 | } 158 | 159 | private async getDataByName(projectName: string) { 160 | const { response } = await this.helper.toPromise(this.api.projectApi, this.api.projectApi.projectsGetProjectByNameGet, { name: projectName }); 161 | if (response && response.body && response.body.id) { 162 | return response.body; 163 | } 164 | } 165 | 166 | private async choose() { 167 | try { 168 | let page = 1; 169 | const pageLimit = 10; 170 | while (true) { 171 | const { response } = await this.helper.toPromise( 172 | this.api.projectApi, 173 | this.api.projectApi.projectsGet, 174 | { limit: pageLimit, page } 175 | ); 176 | 177 | if (response && response.body && response.body.data) { 178 | 179 | const maxPage = Math.ceil(response.body.total / pageLimit); 180 | 181 | const projectList: object[] = response.body.data; 182 | const choices = projectList.map((projectRow: any) => ({ 183 | name: projectRow.name, 184 | value: projectRow 185 | })); 186 | 187 | const body = response.body; 188 | 189 | if (body.total > body.page * body.limit) { 190 | choices.push({ name: "(Load More)", value: -1 }); 191 | } 192 | 193 | const { project } = await inquirer.prompt([ 194 | { 195 | type: "list", 196 | name: "project", 197 | message: `Select project (page ${page} / ${maxPage})`, 198 | paginated: false, 199 | pageSize: pageLimit + 1, 200 | choices 201 | }, 202 | ]); 203 | 204 | if (project === -1) { 205 | page++; 206 | continue; 207 | } 208 | 209 | return project; 210 | } 211 | console.error("Failed to list projects"); 212 | } 213 | } catch (e) { 214 | console.error(this.helper.wrapError(e)); 215 | } 216 | } 217 | 218 | public async select(projectName?: string) { 219 | let chosen: any; 220 | if (typeof projectName === "string") { 221 | chosen = await this.getDataByName(projectName); 222 | if (chosen !== undefined) { 223 | // project name found 224 | this.helper.setProp("projectId", chosen.id); 225 | this.helper.setProp("projectName", chosen.name); 226 | } else { 227 | // project name not found, select through inquirer 228 | console.log( 229 | `Project with name ${colors.green(projectName)} is not found. ` + 230 | `Please choose listed project name below:`); 231 | chosen = await this.choose() 232 | if (chosen) { 233 | this.helper.setProp("projectId", chosen.id); 234 | this.helper.setProp("projectName", chosen.name); 235 | } 236 | } 237 | } else { 238 | // projectName is empty 239 | chosen = await this.choose() 240 | if (chosen) { 241 | this.helper.setProp("projectId", chosen.id); 242 | this.helper.setProp("projectName", chosen.name); 243 | } 244 | } 245 | console.log(colors.green(`Project "${chosen.name}" (${chosen.id}) is successfully selected`)); 246 | 247 | if (!chosen || chosen === undefined) { 248 | chosen = await this.choose() 249 | if (chosen) { 250 | this.helper.setProp("projectId", chosen.id); 251 | this.helper.setProp("projectName", chosen.name); 252 | console.log(colors.green(`Project "${chosen.name}" (${chosen.id}) is successfully selected`)); 253 | } 254 | } 255 | 256 | 257 | } 258 | 259 | public async delete(projectName?: string) { 260 | try { 261 | const chosen = projectName ? await this.getDataByName(projectName) : await this.choose() 262 | if (chosen) { 263 | const { yes } = await inquirer.prompt([ 264 | { 265 | type: "confirm", 266 | name: "yes", 267 | message: "Are you sure want to delete this project ?", 268 | default: true, 269 | } 270 | ]); 271 | 272 | if (yes) { 273 | const deleteProject = await this.helper.toPromise(this.api.projectApi, this.api.projectApi.projectsProjectIdDelete, chosen.id); 274 | if (deleteProject && deleteProject.response && deleteProject.response.body) { 275 | if (deleteProject.response.body) { 276 | console.log("Project has been deleted.") 277 | } else { 278 | console.log("Failed when trying delete project") 279 | } 280 | } 281 | } 282 | } else { 283 | console.log(`Project ${projectName} is not found`) 284 | } 285 | } catch (e) { 286 | console.error(this.helper.wrapError(e)); 287 | } 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /test/bots/bot.spec.ts: -------------------------------------------------------------------------------- 1 | import { IConfig, Config } from "merapi"; 2 | import { suite, test } from "mocha-typescript"; 3 | import { safeLoad } from "js-yaml"; 4 | import { readFileSync } from "fs"; 5 | import { execSync } from "child_process"; 6 | import { IHelper, ICompile, ITester } from "../../interfaces/main"; 7 | import { stub, SinonStub, assert } from "sinon"; 8 | import { deepEqual } from "assert"; 9 | import Api from "../../components/api/api"; 10 | import Bot from "../../components/bots/bot"; 11 | import Helper from "../../components/scripts/helper"; 12 | import Compile from "../../components/scripts/compile"; 13 | import Tester from "../../components/scripts/tester"; 14 | import Zaun from "../../components/api/zaun"; 15 | 16 | @suite class BotTest { 17 | private config : IConfig; 18 | private helper : IHelper; 19 | private compile : ICompile; 20 | private tester : ITester; 21 | private api : any; 22 | private bot : any; 23 | private botDesc = { 24 | schema: "kata.ai/schema/kata-ml/1.0", 25 | name: "Bot Name", 26 | desc: "My First Bot", 27 | flows: { 28 | hello: { 29 | fallback: true, 30 | intents: { 31 | greeting: { 32 | initial: true, 33 | condition: "content == 'hi'" 34 | }, 35 | fallback: { 36 | fallback: true 37 | } 38 | }, 39 | states: { 40 | init: { 41 | initial: true, 42 | transitions: { 43 | greet: { 44 | condition: "intent == \"greeting\"" 45 | }, 46 | other: { 47 | fallback: true 48 | } 49 | } 50 | }, 51 | greet: { 52 | end: true, 53 | action: { 54 | name: "text", 55 | options: { 56 | text: "hi!" 57 | } 58 | } 59 | }, 60 | other: { 61 | end: true, 62 | action: { 63 | name: "text", 64 | options: { 65 | text: "sorry!" 66 | } 67 | } 68 | } 69 | } 70 | } 71 | }, 72 | id: "botId" 73 | }; 74 | 75 | constructor() { 76 | const configJson = safeLoad(readFileSync("./service.yml", "utf8")); 77 | const zaun = Zaun(); 78 | this.config = Config.create(configJson); 79 | this.helper = new Helper(this.config); 80 | this.compile = new Compile(this.helper); 81 | this.tester = new Tester(this.config, this.helper); 82 | this.api = new Api(this.helper, zaun); 83 | this.bot = new Bot(this.compile, this.helper, this.tester, this.api); 84 | } 85 | 86 | @test public async "should call init bot successfully"() { 87 | const createDirStub = stub(this.helper, "createDirectory"); 88 | const dumpYamlStub = stub(this.helper, "dumpYaml"); 89 | const consoleLogStub = stub(console, "log"); 90 | 91 | this.bot.init(this.botDesc.name, {}); 92 | 93 | const bot = Object.assign({}, this.botDesc); 94 | delete bot.id; 95 | 96 | createDirStub.restore(); 97 | dumpYamlStub.restore(); 98 | consoleLogStub.restore(); 99 | assert.callCount(dumpYamlStub, 1); 100 | assert.calledWith(dumpYamlStub, "./bot.yml", bot); 101 | assert.calledWith(consoleLogStub, "Initialized Bot Name successfully"); 102 | } 103 | 104 | 105 | @test public async "should call bot revisions successfully"() { 106 | const consoleLogStub = stub(console, "log"); 107 | const botApiVersions = stub(this.api.botApi, "projectsProjectIdBotRevisionsGet").callsFake(function fakeFn(botId, callback) { 108 | callback(); 109 | }); 110 | 111 | await this.bot.revisions(); 112 | 113 | consoleLogStub.restore(); 114 | botApiVersions.restore(); 115 | assert.calledOnce(botApiVersions); 116 | } 117 | 118 | // TODO 119 | @test public async "function test should call intents, states, actions, and flow test api"() { 120 | const getFileStub = stub(this.helper, "getFiles").returns(["data1"]); 121 | let loadYamlStub = stub(this.helper, "loadYaml").returns({ schema: "kata.ai/schema/kata-ml/1.0/test/intents" }); 122 | const getBotIdStub = stub(this.helper, "getBotId").returns("testBotId"); 123 | 124 | const execIntentTestStub = stub(this.tester, "execIntentTest").returns({ results: { intents: { field: "intentsA", expect: "true", result: "true" } } }); 125 | await this.bot.test(); 126 | 127 | loadYamlStub.restore(); 128 | loadYamlStub = stub(this.helper, "loadYaml").returns({ schema: "kata.ai/schema/kata-ml/1.0/test/states" }); 129 | const execStateTestStub = stub(this.tester, "execStateTest").returns({ results: { intents: { field: "statesA", expect: "true", result: "true" } } }); 130 | await this.bot.test(); 131 | 132 | loadYamlStub.restore(); 133 | loadYamlStub = stub(this.helper, "loadYaml").returns({ schema: "kata.ai/schema/kata-ml/1.0/test/actions" }); 134 | const execActionsTestStub = stub(this.tester, "execActionsTest").returns({ results: { intents: { field: "actionsA", expect: "true", result: "true" } } }); 135 | await this.bot.test(); 136 | 137 | loadYamlStub.restore(); 138 | loadYamlStub = stub(this.helper, "loadYaml").returns({ schema: "kata.ai/schema/kata-ml/1.0/test/flow" }); 139 | const execFlowTestStub = stub(this.tester, "execFlowTest").returns({ results: { intents: { field: "flowsA", expect: "true", result: "true" } } }); 140 | await this.bot.test(); 141 | 142 | getBotIdStub.restore(); 143 | getFileStub.restore(); 144 | loadYamlStub.restore(); 145 | execIntentTestStub.restore(); 146 | execStateTestStub.restore(); 147 | execActionsTestStub.restore(); 148 | execFlowTestStub.restore(); 149 | 150 | 151 | assert.calledWith(getFileStub, "./test", ".spec.yml"); 152 | assert.calledOnce(execIntentTestStub); 153 | assert.calledOnce(execStateTestStub); 154 | assert.calledOnce(execActionsTestStub); 155 | assert.calledOnce(execFlowTestStub); 156 | } 157 | 158 | // @test public async "should call list bots"() { 159 | // const botsGetStub = stub(this.api.botApi, "botsGet").callsFake(function fakeFn(opts, callback) { 160 | // callback(); 161 | // }); 162 | // const consoleLogStub = stub(console, "log"); 163 | 164 | // await this.bot.list(); 165 | 166 | // botsGetStub.restore(); 167 | // consoleLogStub.restore(); 168 | // assert.calledOnce(botsGetStub); 169 | // } 170 | 171 | @test public async "function bot push should create bot"() { 172 | const bot = Object.assign({}, this.botDesc); 173 | delete bot.id; 174 | 175 | const botObj = { 176 | resolve: () => { }, 177 | get: () => bot 178 | }; 179 | const configCreateStub = stub(Config, "create").returns({}); 180 | const loadYamlStub = stub(this.helper, "loadYaml").returns(bot); 181 | const getProjectStubn = stub(this.helper, "getProp").returns(bot); 182 | const execDirectiveStub = stub(this.compile, "execDirectives").returns(botObj); 183 | const createBotStub = stub(this.api.botApi, "projectsProjectIdBotRevisionsPost").callsFake(function fakeFn(body, callback) { 184 | callback(null, { revision: "aaaabbbbccccdddddeeee" }); 185 | }); 186 | const dumpYamlStub = stub(this.helper, "dumpYaml"); 187 | const consoleLogStub = stub(console, "log"); 188 | 189 | await this.bot.push({}); 190 | 191 | configCreateStub.restore(); 192 | loadYamlStub.restore(); 193 | execDirectiveStub.restore(); 194 | createBotStub.restore(); 195 | dumpYamlStub.restore(); 196 | consoleLogStub.restore(); 197 | assert.calledOnce(createBotStub); 198 | assert.calledWith(createBotStub, bot); 199 | assert.calledWith(dumpYamlStub, "./bot.yml", bot); 200 | } 201 | 202 | // @test public async "function bot update should call update bot api"() { 203 | // const botObj = { 204 | // resolve: () => { }, 205 | // get: () => this.botDesc 206 | // }; 207 | // const updatedVersion = "2.0.0"; 208 | // const configCreateStub = stub(Config, "create").returns({}); 209 | // const loadYamlStub = stub(this.helper, "loadYaml").returns(this.botDesc); 210 | // const execDirectiveStub = stub(this.compile, "execDirectives").returns(botObj); 211 | // const updateBotStub = stub(this.api.botApi, "botsBotIdPut").callsFake((botId, body, opts, callback) => { 212 | // callback(null, { version: updatedVersion }); 213 | // }); 214 | // const dumpYamlStub = stub(this.helper, "dumpYaml"); 215 | // const consoleLogStub = stub(console, "log"); 216 | 217 | // await this.bot.update({ rev: "major" }); 218 | 219 | // configCreateStub.restore(); 220 | // loadYamlStub.restore(); 221 | // execDirectiveStub.restore(); 222 | // updateBotStub.restore(); 223 | // dumpYamlStub.restore(); 224 | // consoleLogStub.restore(); 225 | // assert.calledOnce(updateBotStub); 226 | // assert.calledWith(configCreateStub, { ...this.botDesc, version: updatedVersion }); 227 | // assert.calledWith(updateBotStub, this.botDesc.id, this.botDesc, {}); 228 | // assert.calledWith(dumpYamlStub, "./bot.yml", { ...this.botDesc, version: updatedVersion }); 229 | // } 230 | 231 | // @test public async "function bot delete should call delete bot api"() { 232 | // const inquirerPromptStub = stub(this.helper, "inquirerPrompt").returns({ confirmation: true }); 233 | // const getBotIdStub = stub(this.helper, "getBotId").returns(this.botDesc.id); 234 | // const deleteBotStub = stub(this.api.botApi, "botsBotIdDelete").callsFake((botId, callback) => { 235 | // callback(null, {}); 236 | // }); 237 | // const consoleLogStub = stub(console, "log"); 238 | 239 | // await this.bot.delete(); 240 | 241 | // inquirerPromptStub.restore(); 242 | // getBotIdStub.restore(); 243 | // deleteBotStub.restore(); 244 | // consoleLogStub.restore(); 245 | // assert.calledOnce(deleteBotStub); 246 | // assert.calledWith(deleteBotStub, this.botDesc.id); 247 | // } 248 | 249 | // @test public async "should pull bot with valid bot name"() { 250 | // const dumpYamlStub = stub(this.helper, "dumpYaml"); 251 | // const botsGetStub = stub(this.api.botApi, "botsGet").callsFake((body, callback) => { 252 | // callback(null, { items: [this.botDesc] }); 253 | // }); 254 | // const botsBotIdGet = stub(this.api.botApi, "botsBotIdGet").callsFake((botId, callback) => { 255 | // callback(null, this.botDesc); 256 | // }); 257 | // const consoleLogStub = stub(console, "log"); 258 | 259 | // await this.bot.pull(this.botDesc.name, this.botDesc.version, {}); 260 | 261 | // dumpYamlStub.restore(); 262 | // botsGetStub.restore(); 263 | // botsBotIdGet.restore(); 264 | // consoleLogStub.restore(); 265 | // assert.calledOnce(botsGetStub); 266 | // assert.callCount(dumpYamlStub, 1); 267 | // assert.calledWith(dumpYamlStub, "./bot.yml", this.botDesc); 268 | // assert.calledWith(consoleLogStub, `SUCCESS PULL BOT ${this.botDesc.name} WITH VERSION ${this.botDesc.version}`); 269 | // } 270 | } 271 | -------------------------------------------------------------------------------- /components/bots/channel.ts: -------------------------------------------------------------------------------- 1 | 2 | import { IHelper } from "interfaces/main"; 3 | import { Config, JsonObject } from "merapi"; 4 | import Environment from "./environment"; 5 | import inquirer = require("inquirer"); 6 | const Table = require("cli-table"); 7 | 8 | export default class Channel { 9 | constructor( 10 | private helper: IHelper, 11 | private api: any, 12 | private config: Config, 13 | private environment: Environment, 14 | ) { } 15 | 16 | public async addChannel(channelName: string, options: JsonObject) { 17 | const projectId = this.helper.getProjectId(); 18 | const environmentId = await this.environment.askEnvironmentId(); 19 | 20 | try { 21 | const { response: { body: channelsBody } } = await this.helper.toPromise( 22 | this.api.deploymentApi, 23 | this.api.deploymentApi.projectsProjectIdEnvironmentsEnvironmentIdChannelsGet, 24 | projectId, environmentId, {}, 25 | ); 26 | 27 | const channels: { name: string; id: string }[] = channelsBody; 28 | const channelWithSameName = channels.find((row) => row.name === channelName); 29 | if (channelWithSameName) { 30 | throw new Error("CHANNEL NAME HAS BEEN USED"); 31 | } 32 | 33 | if (!options.data) { 34 | options.data = "{}"; 35 | } 36 | 37 | let channelData = JSON.parse(options.data as string) as JsonObject; 38 | channelData.name = channelName; 39 | channelData = await this.inquireChannelData(channelData); 40 | 41 | const result = await this.helper.toPromise( 42 | this.api.deploymentApi, 43 | this.api.deploymentApi.projectsProjectIdEnvironmentsEnvironmentIdChannelsPost, 44 | projectId, 45 | environmentId, 46 | channelData, 47 | ); 48 | console.log(result.response.body); 49 | const channel = result.response.body; 50 | 51 | 52 | console.log("CHANNEL ADDED SUCCESSFULLY"); 53 | console.log(`Paste this url to ${channelData.type} webhook : ${channel.webhook}`); 54 | if (channelData.type === "fbmessenger") { 55 | const channelOptions = JSON.parse(channel.options as string) as JsonObject; 56 | console.log(`And also this token : ${channelOptions.challenge} to your FB Challenge token.`); 57 | } 58 | 59 | } catch (e) { 60 | console.log(this.helper.wrapError(e)); 61 | } 62 | } 63 | 64 | 65 | public async list() { 66 | const projectId = this.helper.getProjectId(); 67 | const environmentId = await this.environment.askEnvironmentId(); 68 | 69 | try { 70 | const { response: { body } } = await this.helper.toPromise( 71 | this.api.deploymentApi, this.api.deploymentApi.projectsProjectIdEnvironmentsEnvironmentIdChannelsGet, 72 | projectId, environmentId, {} 73 | ); 74 | 75 | if (!body) { 76 | throw Error("Failed to list Channels for this environment."); 77 | } 78 | 79 | const table = new Table({ 80 | head: ["Channel Name", "Channel Type", "Channel ID"], 81 | colWidths: [30, 30, 42] 82 | }); 83 | 84 | body.forEach((channel: JsonObject) => { 85 | table.push([channel.name, channel.type, channel.id]); 86 | }); 87 | console.log(table.toString()); 88 | 89 | } catch (e) { 90 | console.log(this.helper.wrapError(e)); 91 | } 92 | } 93 | 94 | 95 | public async removeChannel( 96 | channelName: string, options: JsonObject 97 | ) { 98 | const projectId = this.helper.getProjectId(); 99 | const environmentId = await this.environment.askEnvironmentId(); 100 | 101 | try { 102 | const { response: { body } } = await this.helper.toPromise( 103 | this.api.deploymentApi, this.api.deploymentApi.projectsProjectIdEnvironmentsEnvironmentIdGet, 104 | environmentId, projectId, 105 | ); 106 | const deployment = body; 107 | const channels: { name: string; id: string }[] = deployment.channels; 108 | 109 | const channel = channels.find((row) => row.name === channelName); 110 | if (!channel) { 111 | throw new Error("CHANNEL NOT FOUND"); 112 | } 113 | console.log(channel); 114 | 115 | await this.helper.toPromise( 116 | this.api.deploymentApi, this.api.deploymentApi.projectsProjectIdEnvironmentsEnvironmentIdChannelsChannelIdDelete, 117 | projectId, environmentId, channel.id 118 | ); 119 | 120 | console.log("CHANNEL REMOVED SUCCESSFULLY"); 121 | } catch (e) { 122 | console.log(this.helper.wrapError(e)); 123 | } 124 | } 125 | 126 | private async inquireChannelData(data: JsonObject): Promise { 127 | const { id, name, type, token, refreshToken, secret, url, additionalOptions } = data; 128 | const channelType = this.config.default("config.channels.type", []); 129 | const channelUrl = this.config.default("config.channels.url", []); 130 | const answer = await inquirer.prompt([ 131 | { 132 | type: "list", 133 | name: "type", 134 | message: `channel type : `, 135 | choices: channelType, 136 | when: () => !type, 137 | validate: (type: string) => { 138 | if (!type) { 139 | return "Channel type cannot be empty"; 140 | } 141 | return true; 142 | }, 143 | filter: (type: string) => { 144 | return type.toLowerCase(); 145 | } 146 | }, 147 | { 148 | type: "input", 149 | name: "options.token", 150 | message: "channel token: ", 151 | when: () => !token, 152 | filter(token: string) { 153 | if (!token || token.length === 0) { 154 | return null; 155 | } 156 | 157 | return token; 158 | } 159 | }, 160 | { 161 | type: "input", 162 | name: "options.refreshToken", 163 | message: "channel refresh token: ", 164 | when: () => !refreshToken, 165 | filter(refreshToken: string) { 166 | if (!refreshToken || refreshToken.length === 0) { 167 | return null; 168 | } 169 | 170 | return refreshToken; 171 | } 172 | }, 173 | { 174 | type: "input", 175 | name: "options.secret", 176 | message: "channel secret key: ", 177 | when() { return !secret; }, 178 | filter(secret: string) { 179 | if (!secret || secret.length === 0) { 180 | return null; 181 | } 182 | 183 | return secret; 184 | } 185 | }, 186 | { 187 | type: "input", 188 | name: "additionalOptions", 189 | message: "channel additional options: ", 190 | when() { return !additionalOptions; }, 191 | filter(additionalOptions: string): JsonObject { 192 | if (!additionalOptions || additionalOptions.length === 0) { 193 | return null; 194 | } 195 | try { 196 | const result = JSON.parse(additionalOptions); 197 | if (typeof result === "object") { 198 | return result; 199 | } else { 200 | return { error: true }; 201 | } 202 | } catch (error) { 203 | return { error }; 204 | } 205 | }, 206 | validate(additionalOptions: JsonObject) { 207 | if (!additionalOptions) { 208 | return true; 209 | } 210 | if (additionalOptions.error) { 211 | return "Channel options must be a JSON Format"; 212 | } else { 213 | return true; 214 | } 215 | } 216 | }, 217 | { 218 | type: "input", 219 | name: "url", 220 | message(answer: JsonObject) { 221 | if (answer.type !== "generic") { 222 | return `channel api url (default: ${channelUrl[answer.type as any]}) :`; 223 | } 224 | 225 | return "channel api url : "; 226 | }, 227 | when() { return !url; }, 228 | validate(url: string, answer: JsonObject) { 229 | if (!url && answer.type === "generic") { 230 | return "Channel api url cannot be empty"; 231 | } 232 | 233 | return true; 234 | }, 235 | default: (answer: JsonObject) => { 236 | return channelUrl[answer.type as any]; 237 | } 238 | } 239 | 240 | ]); 241 | 242 | let options = { token, refreshToken, secret, }; 243 | 244 | if (additionalOptions) { 245 | options = { ...options, ...additionalOptions as JsonObject }; 246 | } 247 | 248 | const res = { id, name, type, options, url }; 249 | try { 250 | answer.options = Object.assign(answer.options, answer.additionalOptions); 251 | answer.additionalOptions = undefined; 252 | } catch (error) { 253 | // 254 | } 255 | return { ...res, ...answer }; 256 | } 257 | 258 | public async updateChannel(channelName: string, options: JsonObject) { 259 | try { 260 | const projectId = this.helper.getProjectId(); 261 | if (!projectId) { 262 | throw new Error("Please select project first"); 263 | } 264 | 265 | const environmentId = await this.environment.askEnvironmentId(); 266 | 267 | const { response: { body: channelsBody } } = await this.helper.toPromise( 268 | this.api.deploymentApi, 269 | this.api.deploymentApi.projectsProjectIdEnvironmentsEnvironmentIdChannelsGet, 270 | projectId, environmentId, {}, 271 | ); 272 | 273 | const channels: { name: string; id: string }[] = channelsBody; 274 | if (channels.length == 0) { 275 | throw new Error("Channel not found"); 276 | } 277 | 278 | const channelFound = channels.find((row) => row.name === channelName); 279 | if (!channelFound) { 280 | throw new Error("Channel not found"); 281 | } 282 | 283 | if (!options.data) { 284 | options.data = "{}"; 285 | } 286 | 287 | let channelData = JSON.parse(options.data as string) as JsonObject; 288 | channelData.name = channelName; 289 | channelData = await this.inquireChannelData(channelData); 290 | 291 | const result = await this.helper.toPromise( 292 | this.api.deploymentApi, 293 | this.api.deploymentApi.projectsProjectIdEnvironmentsEnvironmentIdChannelsChannelIdPut, 294 | projectId, 295 | environmentId, 296 | channelFound.id, 297 | channelData, 298 | ); 299 | console.log(result.response.body); 300 | const channel = result.response.body; 301 | 302 | console.log("Channel added successfully"); 303 | console.log(`Paste this url to ${channelData.type} webhook : ${channel.webhook}`); 304 | if (channelData.type === "fbmessenger") { 305 | const channelOptions = JSON.parse(channel.options as string) as JsonObject; 306 | console.log(`And also this token : ${channelOptions.challenge} to your FB Challenge token.`); 307 | } 308 | } catch (e) { 309 | console.log(this.helper.wrapError(e)); 310 | } 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /lib/components/bots/channel.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | Object.defineProperty(exports, "__esModule", { value: true }); 12 | const inquirer = require("inquirer"); 13 | const Table = require("cli-table"); 14 | class Channel { 15 | constructor(helper, api, config, environment) { 16 | this.helper = helper; 17 | this.api = api; 18 | this.config = config; 19 | this.environment = environment; 20 | } 21 | addChannel(channelName, options) { 22 | return __awaiter(this, void 0, void 0, function* () { 23 | const projectId = this.helper.getProjectId(); 24 | const environmentId = yield this.environment.askEnvironmentId(); 25 | try { 26 | const { response: { body: channelsBody } } = yield this.helper.toPromise(this.api.deploymentApi, this.api.deploymentApi.projectsProjectIdEnvironmentsEnvironmentIdChannelsGet, projectId, environmentId, {}); 27 | const channels = channelsBody; 28 | const channelWithSameName = channels.find((row) => row.name === channelName); 29 | if (channelWithSameName) { 30 | throw new Error("CHANNEL NAME HAS BEEN USED"); 31 | } 32 | if (!options.data) { 33 | options.data = "{}"; 34 | } 35 | let channelData = JSON.parse(options.data); 36 | channelData.name = channelName; 37 | channelData = yield this.inquireChannelData(channelData); 38 | const result = yield this.helper.toPromise(this.api.deploymentApi, this.api.deploymentApi.projectsProjectIdEnvironmentsEnvironmentIdChannelsPost, projectId, environmentId, channelData); 39 | console.log(result.response.body); 40 | const channel = result.response.body; 41 | console.log("CHANNEL ADDED SUCCESSFULLY"); 42 | console.log(`Paste this url to ${channelData.type} webhook : ${channel.webhook}`); 43 | if (channelData.type === "fbmessenger") { 44 | const channelOptions = JSON.parse(channel.options); 45 | console.log(`And also this token : ${channelOptions.challenge} to your FB Challenge token.`); 46 | } 47 | } 48 | catch (e) { 49 | console.log(this.helper.wrapError(e)); 50 | } 51 | }); 52 | } 53 | list() { 54 | return __awaiter(this, void 0, void 0, function* () { 55 | const projectId = this.helper.getProjectId(); 56 | const environmentId = yield this.environment.askEnvironmentId(); 57 | try { 58 | const { response: { body } } = yield this.helper.toPromise(this.api.deploymentApi, this.api.deploymentApi.projectsProjectIdEnvironmentsEnvironmentIdChannelsGet, projectId, environmentId, {}); 59 | if (!body) { 60 | throw Error("Failed to list Channels for this environment."); 61 | } 62 | const table = new Table({ 63 | head: ["Channel Name", "Channel Type", "Channel ID"], 64 | colWidths: [30, 30, 42] 65 | }); 66 | body.forEach((channel) => { 67 | table.push([channel.name, channel.type, channel.id]); 68 | }); 69 | console.log(table.toString()); 70 | } 71 | catch (e) { 72 | console.log(this.helper.wrapError(e)); 73 | } 74 | }); 75 | } 76 | removeChannel(channelName, options) { 77 | return __awaiter(this, void 0, void 0, function* () { 78 | const projectId = this.helper.getProjectId(); 79 | const environmentId = yield this.environment.askEnvironmentId(); 80 | try { 81 | const { response: { body } } = yield this.helper.toPromise(this.api.deploymentApi, this.api.deploymentApi.projectsProjectIdEnvironmentsEnvironmentIdGet, environmentId, projectId); 82 | const deployment = body; 83 | const channels = deployment.channels; 84 | const channel = channels.find((row) => row.name === channelName); 85 | if (!channel) { 86 | throw new Error("CHANNEL NOT FOUND"); 87 | } 88 | console.log(channel); 89 | yield this.helper.toPromise(this.api.deploymentApi, this.api.deploymentApi.projectsProjectIdEnvironmentsEnvironmentIdChannelsChannelIdDelete, projectId, environmentId, channel.id); 90 | console.log("CHANNEL REMOVED SUCCESSFULLY"); 91 | } 92 | catch (e) { 93 | console.log(this.helper.wrapError(e)); 94 | } 95 | }); 96 | } 97 | inquireChannelData(data) { 98 | return __awaiter(this, void 0, void 0, function* () { 99 | const { id, name, type, token, refreshToken, secret, url, additionalOptions } = data; 100 | const channelType = this.config.default("config.channels.type", []); 101 | const channelUrl = this.config.default("config.channels.url", []); 102 | const answer = yield inquirer.prompt([ 103 | { 104 | type: "list", 105 | name: "type", 106 | message: `channel type : `, 107 | choices: channelType, 108 | when: () => !type, 109 | validate: (type) => { 110 | if (!type) { 111 | return "Channel type cannot be empty"; 112 | } 113 | return true; 114 | }, 115 | filter: (type) => { 116 | return type.toLowerCase(); 117 | } 118 | }, 119 | { 120 | type: "input", 121 | name: "options.token", 122 | message: "channel token: ", 123 | when: () => !token, 124 | filter(token) { 125 | if (!token || token.length === 0) { 126 | return null; 127 | } 128 | return token; 129 | } 130 | }, 131 | { 132 | type: "input", 133 | name: "options.refreshToken", 134 | message: "channel refresh token: ", 135 | when: () => !refreshToken, 136 | filter(refreshToken) { 137 | if (!refreshToken || refreshToken.length === 0) { 138 | return null; 139 | } 140 | return refreshToken; 141 | } 142 | }, 143 | { 144 | type: "input", 145 | name: "options.secret", 146 | message: "channel secret key: ", 147 | when() { return !secret; }, 148 | filter(secret) { 149 | if (!secret || secret.length === 0) { 150 | return null; 151 | } 152 | return secret; 153 | } 154 | }, 155 | { 156 | type: "input", 157 | name: "additionalOptions", 158 | message: "channel additional options: ", 159 | when() { return !additionalOptions; }, 160 | filter(additionalOptions) { 161 | if (!additionalOptions || additionalOptions.length === 0) { 162 | return null; 163 | } 164 | try { 165 | const result = JSON.parse(additionalOptions); 166 | if (typeof result === "object") { 167 | return result; 168 | } 169 | else { 170 | return { error: true }; 171 | } 172 | } 173 | catch (error) { 174 | return { error }; 175 | } 176 | }, 177 | validate(additionalOptions) { 178 | if (!additionalOptions) { 179 | return true; 180 | } 181 | if (additionalOptions.error) { 182 | return "Channel options must be a JSON Format"; 183 | } 184 | else { 185 | return true; 186 | } 187 | } 188 | }, 189 | { 190 | type: "input", 191 | name: "url", 192 | message(answer) { 193 | if (answer.type !== "generic") { 194 | return `channel api url (default: ${channelUrl[answer.type]}) :`; 195 | } 196 | return "channel api url : "; 197 | }, 198 | when() { return !url; }, 199 | validate(url, answer) { 200 | if (!url && answer.type === "generic") { 201 | return "Channel api url cannot be empty"; 202 | } 203 | return true; 204 | }, 205 | default: (answer) => { 206 | return channelUrl[answer.type]; 207 | } 208 | } 209 | ]); 210 | let options = { token, refreshToken, secret, }; 211 | if (additionalOptions) { 212 | options = Object.assign(Object.assign({}, options), additionalOptions); 213 | } 214 | const res = { id, name, type, options, url }; 215 | try { 216 | answer.options = Object.assign(answer.options, answer.additionalOptions); 217 | answer.additionalOptions = undefined; 218 | } 219 | catch (error) { 220 | // 221 | } 222 | return Object.assign(Object.assign({}, res), answer); 223 | }); 224 | } 225 | updateChannel(channelName, options) { 226 | return __awaiter(this, void 0, void 0, function* () { 227 | try { 228 | const projectId = this.helper.getProjectId(); 229 | if (!projectId) { 230 | throw new Error("Please select project first"); 231 | } 232 | const environmentId = yield this.environment.askEnvironmentId(); 233 | const { response: { body: channelsBody } } = yield this.helper.toPromise(this.api.deploymentApi, this.api.deploymentApi.projectsProjectIdEnvironmentsEnvironmentIdChannelsGet, projectId, environmentId, {}); 234 | const channels = channelsBody; 235 | if (channels.length == 0) { 236 | throw new Error("Channel not found"); 237 | } 238 | const channelFound = channels.find((row) => row.name === channelName); 239 | if (!channelFound) { 240 | throw new Error("Channel not found"); 241 | } 242 | if (!options.data) { 243 | options.data = "{}"; 244 | } 245 | let channelData = JSON.parse(options.data); 246 | channelData.name = channelName; 247 | channelData = yield this.inquireChannelData(channelData); 248 | const result = yield this.helper.toPromise(this.api.deploymentApi, this.api.deploymentApi.projectsProjectIdEnvironmentsEnvironmentIdChannelsChannelIdPut, projectId, environmentId, channelFound.id, channelData); 249 | console.log(result.response.body); 250 | const channel = result.response.body; 251 | console.log("Channel added successfully"); 252 | console.log(`Paste this url to ${channelData.type} webhook : ${channel.webhook}`); 253 | if (channelData.type === "fbmessenger") { 254 | const channelOptions = JSON.parse(channel.options); 255 | console.log(`And also this token : ${channelOptions.challenge} to your FB Challenge token.`); 256 | } 257 | } 258 | catch (e) { 259 | console.log(this.helper.wrapError(e)); 260 | } 261 | }); 262 | } 263 | } 264 | exports.default = Channel; 265 | //# sourceMappingURL=channel.js.map -------------------------------------------------------------------------------- /lib/components/projects/project.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | Object.defineProperty(exports, "__esModule", { value: true }); 12 | const inquirer = require("inquirer"); 13 | const Table = require("cli-table"); 14 | const colors = require("colors/safe"); 15 | class Project { 16 | constructor(api, helper) { 17 | this.api = api; 18 | this.helper = helper; 19 | } 20 | create() { 21 | return __awaiter(this, void 0, void 0, function* () { 22 | let projectData = { name: "" }; 23 | while (true) { 24 | projectData = yield this.helper.inquirerPrompt([ 25 | { 26 | type: "text", 27 | name: "name", 28 | message: "Project name:", 29 | }, 30 | { 31 | type: "text", 32 | name: "description", 33 | message: "Project description:", 34 | }, 35 | ]); 36 | if (projectData.name.length > 20) { 37 | console.error("error: Project name length can not exceed 20 characters"); 38 | } 39 | else if (!/^[A-Za-z][A-Za-z0-9_-]*[A-Za-z0-9]$/.test(projectData.name)) { 40 | console.error("error: Project name must start with alphabet characters and contains only aplhanumeric character, dash, or underscore"); 41 | } 42 | else { 43 | break; 44 | } 45 | } 46 | const inquiredOptions = yield this.helper.inquirerPrompt([ 47 | { 48 | type: "number", 49 | name: "timezone", 50 | message: "Timezone (UTC)", 51 | default: 7, 52 | }, 53 | ]); 54 | inquiredOptions.timezone = Number(inquiredOptions.timezone); 55 | const options = Object.assign({ bot: true, cms: true, nlu: true }, inquiredOptions); 56 | let nluOptions = {}; 57 | if (options.nlu) { 58 | nluOptions = yield this.helper.inquirerPrompt([ 59 | { 60 | type: "text", 61 | name: "nluLang", 62 | message: "NLU Language", 63 | default: "id", 64 | }, 65 | { 66 | type: "confirm", 67 | name: "privateNlu", 68 | message: "Is private Nlu?", 69 | default: true, 70 | }, 71 | ]); 72 | nluOptions.nluVisibility = nluOptions.privateNlu ? "private" : "public"; 73 | delete nluOptions.privateNlu; 74 | } 75 | const requestBody = Object.assign(Object.assign({}, projectData), { options: Object.assign(Object.assign({}, options), nluOptions) }); 76 | try { 77 | const { response } = yield this.helper.toPromise(this.api.projectApi, this.api.projectApi.projectsPost, requestBody); 78 | if (response && response.body && response.body.id) { 79 | const project = response.body; 80 | const projectId = project.id; 81 | this.helper.setProp("projectId", projectId); 82 | console.log(colors.green(`Project "${project.name}" (${projectId}) is successfully created`)); 83 | return; 84 | } 85 | } 86 | catch (e) { 87 | console.error(this.helper.wrapError(e)); 88 | } 89 | }); 90 | } 91 | update(projectName) { 92 | return __awaiter(this, void 0, void 0, function* () { 93 | let chosen = null; 94 | if (projectName) { 95 | chosen = yield this.getDataByName(projectName); 96 | } 97 | else { 98 | chosen = yield this.choose(); 99 | } 100 | if (chosen) { 101 | const { description, privateNlu } = yield this.helper.inquirerPrompt([ 102 | { 103 | type: "text", 104 | name: "description", 105 | message: "Project description:", 106 | }, 107 | { 108 | type: "confirm", 109 | name: "privateNlu", 110 | message: "Is private Nlu?", 111 | default: true, 112 | } 113 | ]); 114 | const nluVisibility = privateNlu ? 'private' : 'public'; 115 | const requestBody = { 116 | id: chosen.id, 117 | name: chosen.name, 118 | description: description, 119 | options: { 120 | timezone: chosen.options.timezone, 121 | nluLang: chosen.options.nluLang, 122 | nluVisibility: nluVisibility, 123 | nluId: chosen.options.nluId 124 | } 125 | }; 126 | const { response } = yield this.helper.toPromise(this.api.projectApi, this.api.projectApi.projectsProjectIdPut, chosen.id, requestBody); 127 | if (response && response.body) { 128 | console.log(`Project ${chosen.name} has been updated.`); 129 | } 130 | else { 131 | console.log("Failed when trying update project"); 132 | } 133 | } 134 | else { 135 | console.log(`Project ${projectName} is not found`); 136 | } 137 | }); 138 | } 139 | list() { 140 | return __awaiter(this, void 0, void 0, function* () { 141 | try { 142 | const { response } = yield this.helper.toPromise(this.api.projectApi, this.api.projectApi.projectsGet, {}); 143 | if (response && response.body && response.body.data) { 144 | const table = new Table({ 145 | head: ["Project ID", "Project Name"], 146 | colWidths: [38, 32] 147 | }); 148 | response.body.data.forEach((project) => { 149 | table.push([project.id, project.name]); 150 | }); 151 | console.log(table.toString()); 152 | } 153 | } 154 | catch (e) { 155 | console.error(this.helper.wrapError(e)); 156 | } 157 | }); 158 | } 159 | getDataByName(projectName) { 160 | return __awaiter(this, void 0, void 0, function* () { 161 | const { response } = yield this.helper.toPromise(this.api.projectApi, this.api.projectApi.projectsGetProjectByNameGet, { name: projectName }); 162 | if (response && response.body && response.body.id) { 163 | return response.body; 164 | } 165 | }); 166 | } 167 | choose() { 168 | return __awaiter(this, void 0, void 0, function* () { 169 | try { 170 | let page = 1; 171 | const pageLimit = 10; 172 | while (true) { 173 | const { response } = yield this.helper.toPromise(this.api.projectApi, this.api.projectApi.projectsGet, { limit: pageLimit, page }); 174 | if (response && response.body && response.body.data) { 175 | const maxPage = Math.ceil(response.body.total / pageLimit); 176 | const projectList = response.body.data; 177 | const choices = projectList.map((projectRow) => ({ 178 | name: projectRow.name, 179 | value: projectRow 180 | })); 181 | const body = response.body; 182 | if (body.total > body.page * body.limit) { 183 | choices.push({ name: "(Load More)", value: -1 }); 184 | } 185 | const { project } = yield inquirer.prompt([ 186 | { 187 | type: "list", 188 | name: "project", 189 | message: `Select project (page ${page} / ${maxPage})`, 190 | paginated: false, 191 | pageSize: pageLimit + 1, 192 | choices 193 | }, 194 | ]); 195 | if (project === -1) { 196 | page++; 197 | continue; 198 | } 199 | return project; 200 | } 201 | console.error("Failed to list projects"); 202 | } 203 | } 204 | catch (e) { 205 | console.error(this.helper.wrapError(e)); 206 | } 207 | }); 208 | } 209 | select(projectName) { 210 | return __awaiter(this, void 0, void 0, function* () { 211 | let chosen; 212 | if (typeof projectName === "string") { 213 | chosen = yield this.getDataByName(projectName); 214 | if (chosen !== undefined) { 215 | // project name found 216 | this.helper.setProp("projectId", chosen.id); 217 | this.helper.setProp("projectName", chosen.name); 218 | } 219 | else { 220 | // project name not found, select through inquirer 221 | console.log(`Project with name ${colors.green(projectName)} is not found. ` + 222 | `Please choose listed project name below:`); 223 | chosen = yield this.choose(); 224 | if (chosen) { 225 | this.helper.setProp("projectId", chosen.id); 226 | this.helper.setProp("projectName", chosen.name); 227 | } 228 | } 229 | } 230 | else { 231 | // projectName is empty 232 | chosen = yield this.choose(); 233 | if (chosen) { 234 | this.helper.setProp("projectId", chosen.id); 235 | this.helper.setProp("projectName", chosen.name); 236 | } 237 | } 238 | console.log(colors.green(`Project "${chosen.name}" (${chosen.id}) is successfully selected`)); 239 | if (!chosen || chosen === undefined) { 240 | chosen = yield this.choose(); 241 | if (chosen) { 242 | this.helper.setProp("projectId", chosen.id); 243 | this.helper.setProp("projectName", chosen.name); 244 | console.log(colors.green(`Project "${chosen.name}" (${chosen.id}) is successfully selected`)); 245 | } 246 | } 247 | }); 248 | } 249 | delete(projectName) { 250 | return __awaiter(this, void 0, void 0, function* () { 251 | try { 252 | const chosen = projectName ? yield this.getDataByName(projectName) : yield this.choose(); 253 | if (chosen) { 254 | const { yes } = yield inquirer.prompt([ 255 | { 256 | type: "confirm", 257 | name: "yes", 258 | message: "Are you sure want to delete this project ?", 259 | default: true, 260 | } 261 | ]); 262 | if (yes) { 263 | const deleteProject = yield this.helper.toPromise(this.api.projectApi, this.api.projectApi.projectsProjectIdDelete, chosen.id); 264 | if (deleteProject && deleteProject.response && deleteProject.response.body) { 265 | if (deleteProject.response.body) { 266 | console.log("Project has been deleted."); 267 | } 268 | else { 269 | console.log("Failed when trying delete project"); 270 | } 271 | } 272 | } 273 | } 274 | else { 275 | console.log(`Project ${projectName} is not found`); 276 | } 277 | } 278 | catch (e) { 279 | console.error(this.helper.wrapError(e)); 280 | } 281 | }); 282 | } 283 | } 284 | exports.default = Project; 285 | //# sourceMappingURL=project.js.map --------------------------------------------------------------------------------