diff --git a/lib/typescript/botskills/.gitignore b/lib/typescript/botskills/.gitignore new file mode 100644 index 0000000000..27077059da --- /dev/null +++ b/lib/typescript/botskills/.gitignore @@ -0,0 +1,4 @@ +lib/** +bin/** +typings/** +export/** \ No newline at end of file diff --git a/lib/typescript/botskills/README.md b/lib/typescript/botskills/README.md new file mode 100644 index 0000000000..66803b7ee5 --- /dev/null +++ b/lib/typescript/botskills/README.md @@ -0,0 +1,40 @@ +# Botskills Command Line tool +The Botskills tool is a command line tool to manage the skills connected to your assistant solution. + +## Prerequisite +- [Node.js](https://nodejs.org/) version 10.8 or higher + +## Installation +To install using npm +```bash +npm install -g botskills +``` +This will install botskills into your global path. +To uninstall using npm +```bash +npm uninstall -g botskills +``` + +## Botskills functionality +- [Connect](./docs/connect-disconnect.md) a Skill to your assistant +- [Disconnect](./docs/connect-disconnect.md) a Skill from your assistant +- [List](./docs/list.md) all Skills connected to your assistant + +## Nightly builds +Nightly builds are based on the latest development code which means they may or may not be stable and probably won't be documented. These builds are better suited for more experienced users and developers although everyone is welcome to give them a shot and provide feedback. + +You can get the latest nightly build of Botskills from the [BotBuilder MyGet]() feed. To install the nightly +```bash +npm config set registry https://botbuilder.myget.org/F/aitemplates/npm/ +``` +Install using npm +```bash +npm install -g botskills +``` +To reset registry +```bash +npm config set registry https://registry.npmjs.org/ +``` + +## Further Reading +- [Create and customize Skills for your assistant](https://github.com/Microsoft/AI/blob/master/docs/skills/typescript/create.md). diff --git a/lib/typescript/botskills/docs/connect-disconnect.md b/lib/typescript/botskills/docs/connect-disconnect.md new file mode 100644 index 0000000000..7bf4e017c2 --- /dev/null +++ b/lib/typescript/botskills/docs/connect-disconnect.md @@ -0,0 +1,66 @@ +# Connect a Skill to your assistant + +To connect a Skill to your assistant: + +```bash +botskills connect [options] +``` + +### Options + +| Option | Description | +|-------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------| +| -b, --botName \ | Name of your assistant bot | +| -l, --localManifest \ | Path to local Skill Manifest file | +| -r, --remoteManifest \ | URL to remote Skill Manifest | +| --dispatchName [name] | (OPTIONAL) Name of your assistant's '.dispatch' file (defaults to the name displayed in your Cognitive Models file) | +| --language [language] | (OPTIONAL) Locale used for LUIS culture (defaults to 'en-us') | +| --luisFolder [path] | (OPTIONAL) Path to the folder containing your Skills' '.lu' files (defaults to './deployment/resources/skills/en' inside your assistant folder) | +| --dispatchFolder [path] | (OPTIONAL) Path to the folder containing your assistant's '.dispatch' file (defaults to './deployment/resources/dispatch/en' inside your assistant folder) | +| --outFolder [path] | (OPTIONAL) Path for any output file that may be generated (defaults to your assistant's root folder') | +| --lgOutFolder [path] | (OPTIONAL) Path for the LuisGen output (defaults to a 'service' folder inside your assistant's folder') | +| --skillsFile [path] | (OPTIONAL) Path to your assistant Skills configuration file (defaults to the 'skills.json' inside your assistant's folder) | +| --resourceGroup [name] | (OPTIONAL) Name of your assistant's resource group in Azure (defaults to your assistant's bot name) | +| --appSettingsFile [path] | (OPTIONAL) Path to your app settings file (defaults to 'appsettings.json' inside your assistant's folder) | +| --cognitiveModelsFile [path] | (OPTIONAL) Path to your Cognitive Models file (defaults to 'cognitivemodels.json' inside your assistant's folder) | +| --verbose | (OPTIONAL) Output detailed information about the processing of the tool | +| -h, --help | Output usage information | + +An example on how to use it with a local Skill manifest file: + +```bash +botskills connect --localManifest "./skills/customSkill/customSkillManifest.json" --assistantSkills "./skills.json" --verbose +``` + +> **Note:** The paths to both the Skill Manifest and the assistant Skills configuration file can be relative or absolute paths equally, and should be explicitly a `.json` file. + +An example on how to use it with a remote Skill manifest: + +```bash +botskills connect --remoteManifest "http://.azurewebsites.net/api/skill/manifest?inlineTriggerUtterances=false" --assistantSkills "./skills.json" --verbose +``` + +# Disconnect a Skill from your assistant + +To disconnect a Skill from your assistant: + +```bash +botskills disconnect [option] +``` + +### Options + +| Option | Description | +|--------------------------|-------------------------------------------------------------------------| +| -n, --skillName | Name or id of the skill to remove from your assistant (case sensitive) | +| -f, --skillsFile | Path to the assistant Skills configuration file | +| --verbose | (OPTIONAL) Output detailed information about the processing of the tool | +| -h, --help | Output usage information | + +An example on how to use it: + +```bash +botskills disconnect --skillName "customSkill" --assistantSkills "./skills.json" --verbose +``` + +> **Note:** The path to the assistant Skills configuration file can be relative or absolute path, and should be explicitly a `.json` file. \ No newline at end of file diff --git a/lib/typescript/botskills/docs/list.md b/lib/typescript/botskills/docs/list.md new file mode 100644 index 0000000000..b79564a77b --- /dev/null +++ b/lib/typescript/botskills/docs/list.md @@ -0,0 +1,14 @@ +# Listing connected skills + +To acces the list of connected skills: +```bash +botskills list [options] +``` + +### Options + +| Option | Description | +|-------------------------------|-------------------------------------------------------------------------| +| -a, --assistantSkills | Path to the assistant Skills configuration file | +| --verbose | (OPTIONAL) Output detailed information about the processing of the tool | +| -h, --help | Output usage information | diff --git a/lib/typescript/botskills/package-lock.json b/lib/typescript/botskills/package-lock.json new file mode 100644 index 0000000000..62d8458a9b --- /dev/null +++ b/lib/typescript/botskills/package-lock.json @@ -0,0 +1,883 @@ +{ + "name": "botskills", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/caseless": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", + "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==", + "dev": true + }, + "@types/form-data": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.2.1.tgz", + "integrity": "sha512-JAMFhOaHIciYVh8fb5/83nmuO/AHwmto+Hq7a9y8FzLDcC1KCU344XDOMEmahnrTFlHjgh4L0WJFczNIX2GxnQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/get-stdin": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/get-stdin/-/get-stdin-5.0.1.tgz", + "integrity": "sha1-Rq+8rwnpT+Alr6B66ZSsMWitvfM=", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/node": { + "version": "10.14.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.4.tgz", + "integrity": "sha512-DT25xX/YgyPKiHFOpNuANIQIVvYEwCWXgK2jYYwqgaMrYE6+tq+DtmMwlD3drl6DJbUwtlIDnn0d7tIn/EbXBg==", + "dev": true + }, + "@types/request": { + "version": "2.48.1", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.1.tgz", + "integrity": "sha512-ZgEZ1TiD+KGA9LiAAPPJL68Id2UWfeSO62ijSXZjFJArVV+2pKcsVHmrcu+1oiE3q6eDGiFiSolRc4JHoerBBg==", + "dev": true, + "requires": { + "@types/caseless": "*", + "@types/form-data": "*", + "@types/node": "*", + "@types/tough-cookie": "*" + } + }, + "@types/request-promise-native": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@types/request-promise-native/-/request-promise-native-1.0.15.tgz", + "integrity": "sha512-uYPjTChD9TpjlvbBjNpZfNc64TBejBS52u7pbxhQLnlxw+5Em7wLb6DU2wdJVhJ2Mou7v50N0qgL4Gia5mmRYg==", + "dev": true, + "requires": { + "@types/request": "*" + } + }, + "@types/semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ==", + "dev": true + }, + "@types/tough-cookie": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.5.tgz", + "integrity": "sha512-SCcK7mvGi3+ZNz833RRjFIxrn4gI1PPR3NtuIS+6vMkvmsGjosqTJwRt5bAEFLRz+wtJMWv8+uOnZf2hi2QXTg==", + "dev": true + }, + "ajv": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", + "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "botdispatch": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/botdispatch/-/botdispatch-1.3.0.tgz", + "integrity": "sha512-oAV0lR++a2eg0jUPBx+tGAJRFxvBDqCFN4HYPAku9U9pQanFDxGn0s0c94f7l6bZCmj7wrC25ZQ3R5ci8AaDjg==", + "requires": { + "dotnet-2.1": "^2.1.0" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "combined-stream": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", + "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", + "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "dotnet-2.1": { + "version": "2.1.1003", + "resolved": "https://registry.npmjs.org/dotnet-2.1/-/dotnet-2.1-2.1.1003.tgz", + "integrity": "sha512-HIGBUzKkQrPuen31ZNq6qODAXlEA075RNa5fQvMH0rirjARGqvGmUHy7iSBDZkGIenaXR9jz9q9Tk1cYGBtZTQ==" + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" + }, + "mime-db": { + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz", + "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==" + }, + "mime-types": { + "version": "2.1.22", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz", + "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==", + "requires": { + "mime-db": "~1.38.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "mocha": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", + "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", + "dev": true, + "requires": { + "browser-stdout": "1.3.1", + "commander": "2.15.1", + "debug": "3.1.0", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.5", + "he": "1.1.1", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "supports-color": "5.4.0" + }, + "dependencies": { + "commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "psl": { + "version": "1.1.31", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz", + "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==" + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, + "request-promise-core": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.2.tgz", + "integrity": "sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag==", + "requires": { + "lodash": "^4.17.11" + } + }, + "request-promise-native": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.7.tgz", + "integrity": "sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w==", + "requires": { + "request-promise-core": "1.1.2", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + } + }, + "resolve": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.0.tgz", + "integrity": "sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "semver": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.0.0.tgz", + "integrity": "sha512-0UewU+9rFapKFnlbirLi3byoOuhrSsli/z/ihNnvM24vgF+8sNBiI1LZPBSH9wJKUwaUbw+s3hToDLCXkrghrQ==" + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=" + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + } + } + }, + "tslib": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", + "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", + "dev": true + }, + "tslint": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.15.0.tgz", + "integrity": "sha512-6bIEujKR21/3nyeoX2uBnE8s+tMXCQXhqMmaIPJpHmXJoBJPTLcI7/VHRtUwMhnLVdwLqqY3zmd8Dxqa5CVdJA==", + "dev": true, + "requires": { + "babel-code-frame": "^6.22.0", + "builtin-modules": "^1.1.1", + "chalk": "^2.3.0", + "commander": "^2.12.1", + "diff": "^3.2.0", + "glob": "^7.1.1", + "js-yaml": "^3.13.0", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "resolve": "^1.3.2", + "semver": "^5.3.0", + "tslib": "^1.8.0", + "tsutils": "^2.29.0" + }, + "dependencies": { + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + } + } + }, + "tslint-microsoft-contrib": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tslint-microsoft-contrib/-/tslint-microsoft-contrib-6.0.0.tgz", + "integrity": "sha512-R//efwn+34IUjTJeYgNDAJdzG0jyLWIehygPt/PHuZAieTolFVS56FgeFW7DOLap9ghXzMiFPTmDgm54qaL7QA==", + "dev": true, + "requires": { + "tsutils": "^2.27.2 <2.29.0" + }, + "dependencies": { + "tsutils": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.28.0.tgz", + "integrity": "sha512-bh5nAtW0tuhvOJnx1GLRn5ScraRLICGyJV5wJhtRWOLsxW70Kk5tZtpK3O/hW6LDnqKS9mlUMPZj9fEMJ0gxqA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + } + } + }, + "tsutils": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", + "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, + "typescript": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.4.1.tgz", + "integrity": "sha512-3NSMb2VzDQm8oBTLH6Nj55VVtUEpe/rgkIzMir0qVoLyjDZlnMBva0U6vDiV3IH+sl/Yu6oP5QwsAQtHPmDd2Q==", + "dev": true + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "requires": { + "punycode": "^2.1.0" + } + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + } + } +} diff --git a/lib/typescript/botskills/package.json b/lib/typescript/botskills/package.json new file mode 100644 index 0000000000..66b2ed2798 --- /dev/null +++ b/lib/typescript/botskills/package.json @@ -0,0 +1,58 @@ +{ + "name": "botskills", + "version": "1.0.0", + "description": "Skill command line tool for manipulating Microsoft Bot Framework skills", + "main": "lib/botskills.js", + "bin": { + "botskills": "lib/botskills.js" + }, + "scripts": { + "prebuild": "npm run lint", + "build": "tsc", + "lint": "tslint -t vso ./src/**/*.ts", + "lint-fix": "tslint --fix ./src/**/*.ts", + "test": "cd test/ && mocha --opts mocha.opts" + }, + "directories": { + "lib": "lib" + }, + "files": [ + "lib/" + ], + "engines": { + "node": ">=8.0" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Microsoft/AI.git" + }, + "author": "Microsoft", + "license": "MIT", + "bugs": { + "url": "https://github.com/Microsoft/AI/issues" + }, + "homepage": "https://github.com/Microsoft/AI#readme", + "dependencies": { + "botdispatch": "^1.3.0", + "chalk": "^2.4.2", + "commander": "^2.20.0", + "dotnet-2.1": "^2.1.1003", + "request": "^2.88.0", + "request-promise-native": "^1.0.7", + "semver": "^6.0.0" + }, + "devDependencies": { + "@types/get-stdin": "^5.0.1", + "@types/node": "^10.9.4", + "@types/request-promise-native": "^1.0.15", + "@types/semver": "^5.5.0", + "mocha": "^5.2.0", + "tslint": "^5.12.1", + "tslint-microsoft-contrib": "6.0.0", + "typescript": "^3.2.2" + }, + "env": { + "mocha": true, + "node": true + } +} diff --git a/lib/typescript/botskills/src/botskills-connect.ts b/lib/typescript/botskills/src/botskills-connect.ts new file mode 100644 index 0000000000..6175a2e259 --- /dev/null +++ b/lib/typescript/botskills/src/botskills-connect.ts @@ -0,0 +1,158 @@ +/** + * Copyright(c) Microsoft Corporation.All rights reserved. + * Licensed under the MIT License. + */ + +import * as program from 'commander'; +import { existsSync } from 'fs'; +import { extname, isAbsolute, join, resolve } from 'path'; +import { connectSkill } from './functionality'; +import { ConsoleLogger, ILogger } from './logger'; +import { ICognitiveModelFile, IConnectConfiguration } from './models'; +import { validatePairOfArgs } from './utils'; + +function showErrorHelp(): void { + program.outputHelp((str: string) => { + logger.error(str); + + return ''; + }); + process.exit(1); +} + +const logger: ILogger = new ConsoleLogger(); + +program.Command.prototype.unknownOption = (flag: string): void => { + logger.error(`Unknown arguments: ${flag}`); + showErrorHelp(); +}; + +// tslint:disable: max-line-length +program + .name('botskills connect') + .description('Connect a skill to your assistant bot. Only one of both path or URL to Skill is needed.') + .option('-b, --botName ', 'Name of your assistant bot') + .option('-l, --localManifest ', 'Path to local Skill Manifest file') + .option('-r, --remoteManifest ', 'URL to remote Skill Manifest') + .option('--dispatchName [name]', '[OPTIONAL] Name of your assistant\'s \'.dispatch\' file (defaults to the name displayed in your Cognitive Models file)') + .option('--language [language]', '[OPTIONAL] Locale used for LUIS culture (defaults to \'en-us\')') + .option('--luisFolder [path]', '[OPTIONAL] Path to the folder containing your Skills\' .lu files (defaults to \'./deployment/resources/skills/en\' inside your assistant folder)') + .option('--dispatchFolder [path]', '[OPTIONAL] Path to the folder containing your assistant\'s \'.dispatch\' file (defaults to \'./deployment/resources/dispatch/en\' inside your assistant folder)') + .option('--outFolder [path]', '[OPTIONAL] Path for any output file that may be generated (defaults to your assistant\'s root folder)') + .option('--lgOutFolder [path]', '[OPTIONAL] Path for the LuisGen output (defaults to a \'service\' folder inside your assistant\'s folder)') + .option('--skillsFile [path]', '[OPTIONAL] Path to your assistant Skills configuration file (defaults to the \'skills.json\' inside your assistant\'s folder)') + .option('--resourceGroup [path]', '[OPTIONAL] Name of your assistant\'s resource group in Azure (defaults to your assistant\'s bot name)') + .option('--appSettingsFile [path]', '[OPTIONAL] Path to your app settings file (defaults to \'appsettings.json\' inside your assistant\'s folder)') + .option('--cognitiveModelsFile [path]', '[OPTIONAL] Path to your Cognitive Models file (defaults to \'cognitivemodels.json\' inside your assistant\'s folder)') + .option('--cs', 'Determine your assistant project structure to be a CSharp-like structure') + .option('--ts', 'Determine your assistant project structure to be a TypeScript-like structure') + .option('--verbose', '[OPTIONAL] Output detailed information about the processing of the tool') + .action((cmd: program.Command, actions: program.Command) => undefined); + +const args: program.Command = program.parse(process.argv); + +if (process.argv.length < 3) { + program.help(); + process.exit(0); +} + +logger.isVerbose = args.verbose; +let projectLanguage: string = ''; + +// cs and ts validation +const csAndTsValidationResult: string = validatePairOfArgs(args.cs, args.ts); +if (csAndTsValidationResult) { + logger.error( + csAndTsValidationResult.replace('{0}', 'cs') + .replace('{1}', 'ts') + ); + process.exit(1); +} +projectLanguage = args.cs ? 'cs' : 'ts'; + +// Validation of arguments +// botName validation +if (!args.botName) { + logger.error(`The 'botName' argument should be provided.`); + process.exit(1); +} + +// localManifest && remoteManifest validation +const manifestValidationResult: string = validatePairOfArgs(args.localManifest, args.remoteManifest); +if (manifestValidationResult) { + logger.error( + manifestValidationResult.replace('{0}', 'localManifest') + .replace('{1}', 'remoteManifest') + ); + process.exit(1); +} +if (args.localManifest && extname(args.localManifest) !== '.json') { + logger.error(`The 'localManifest' argument should be a path to a JSON file.`); + process.exit(1); +} + +// Initialize an instance of IConnectConfiguration to send the needed arguments to the connectSkill function +const configuration: Partial = { + botName: args.botName, + localManifest: args.localManifest, + remoteManifest: args.remoteManifest, + lgLanguage: projectLanguage +}; + +// outFolder validation -- the const is needed for reassuring 'configuration.outFolder' is not undefined +const outFolder: string = args.outFolder || resolve('./'); +configuration.outFolder = outFolder; + +// skillsFile validation +if (!args.skillsFile) { + configuration.skillsFile = join(configuration.outFolder, (args.ts ? join('src', 'skills.json') : 'skills.json')); +} else if (extname(args.skillsFile) !== '.json') { + logger.error(`The 'skillsFile' argument should be a JSON file.`); + process.exit(1); +} else { + const skillsFilePath: string = isAbsolute(args.skillsFile) ? args.skillsFile : join(resolve('./'), args.skillsFile); + if (!existsSync(skillsFilePath)) { + logger.error(`The 'skillsFile' argument leads to a non-existing file. + Please make sure to provide a valid path to your Assistant Skills configuration file.`); + process.exit(1); + } + configuration.skillsFile = skillsFilePath; +} + +// resourceGroup validation +configuration.resourceGroup = args.resourceGroup || configuration.botName; + +// appSettingsFile validation +configuration.appSettingsFile = args.appSettingsFile || join(configuration.outFolder, (args.ts ? join('src', 'appsettings.json') : 'appsettings.json')); + +// cognitiveModelsFile validation +const cognitiveModelsFilePath: string = args.cognitiveModelsFile || join(configuration.outFolder, (args.ts ? join('src', 'cognitivemodels.json') : 'cognitivemodels.json')); +configuration.cognitiveModelsFile = cognitiveModelsFilePath; + +// language validation +const language: string = args.language || 'en-us'; +configuration.language = language; +const languageCode: string = (language.split('-'))[0]; + +// luisFolder validation +configuration.luisFolder = args.luisFolder || join(configuration.outFolder, 'Deployment', 'Resources', 'Skills', languageCode); + +// dispatchFolder validation +configuration.dispatchFolder = args.dispatchFolder || join(configuration.outFolder, 'Deployment', 'Resources', 'Dispatch', languageCode); + +// lgOutFolder validation +configuration.lgOutFolder = args.lgOutFolder || join(configuration.outFolder, (args.ts ? join('src', 'Services') : 'Services')); + +// dispatchName validation +if (!args.dispatchName) { + // try get the dispatch name from the cognitiveModels file + // tslint:disable-next-line + const cognitiveModelsFile: ICognitiveModelFile = require(cognitiveModelsFilePath); + configuration.dispatchName = cognitiveModelsFile.cognitiveModels[languageCode].dispatchModel.name; +} + +configuration.logger = logger; + +// End of arguments validation + +connectSkill( configuration); diff --git a/lib/typescript/botskills/src/botskills-disconnect.ts b/lib/typescript/botskills/src/botskills-disconnect.ts new file mode 100644 index 0000000000..44d5d16cd9 --- /dev/null +++ b/lib/typescript/botskills/src/botskills-disconnect.ts @@ -0,0 +1,125 @@ +/** + * Copyright(c) Microsoft Corporation.All rights reserved. + * Licensed under the MIT License. + */ + +import * as program from 'commander'; +import { existsSync } from 'fs'; +import { extname, isAbsolute, join, resolve } from 'path'; +import { disconnectSkill } from './functionality'; +import { ConsoleLogger, ILogger} from './logger'; +import { ICognitiveModelFile, IDisconnectConfiguration } from './models'; +import { validatePairOfArgs } from './utils'; + +function showErrorHelp(): void { + program.outputHelp((str: string) => { + logger.error(str); + + return ''; + }); + process.exit(1); +} + +const logger: ILogger = new ConsoleLogger(); + +program.Command.prototype.unknownOption = (flag: string): void => { + logger.error(`Unknown arguments: ${flag}`); + showErrorHelp(); +}; + +// tslint:disable: max-line-length +program + .name('botskills disconnect') + .description('Disconnect a specific skill from your assitant bot. Only one of both id or name of the Skill is needed.') + .option('-i, --skillId ', 'Id of the skill to remove from your assistant') + .option('--dispatchName [name]', '[OPTIONAL] Name of your assistant\'s \'.dispatch\' file (defaults to the name displayed in your Cognitive Models file)') + .option('--language [language]', '[OPTIONAL] Locale used for LUIS culture (defaults to \'en-us\')') + .option('--luisFolder [path]', '[OPTIONAL] Path to the folder containing your Skills\' .lu files (defaults to \'./deployment/resources/skills/en\' inside your assistant folder)') + .option('--dispatchFolder [path]', '[OPTIONAL] Path to the folder containing your assistant\'s \'.dispatch\' file (defaults to \'./deployment/resources/dispatch/en\' inside your assistant folder)') + .option('--outFolder [path]', '[OPTIONAL] Path for any output file that may be generated (defaults to your assistant\'s root folder)') + .option('--lgOutFolder [path]', '[OPTIONAL] Path for the LuisGen output (defaults to a \'service\' folder inside your assistant\'s folder)') + .option('--skillsFile [path]', '[OPTIONAL] Path to your assistant Skills configuration file (defaults to the \'skills.json\' inside your assistant\'s folder)') + .option('--cognitiveModelsFile [path]', '[OPTIONAL] Path to your Cognitive Models file (defaults to \'cognitivemodels.json\' inside your assistant\'s folder)') + .option('--cs', 'Determine your assistant project structure to be a CSharp-like structure') + .option('--ts', 'Determine your assistant project structure to be a TypeScript-like structure') + .option('--verbose', '[OPTIONAL] Output detailed information about the processing of the tool') + .action((cmd: program.Command, actions: program.Command) => undefined); +// tslint:enable: max-line-length +const args: program.Command = program.parse(process.argv); + +if (process.argv.length < 3) { + program.help(); +} + +logger.isVerbose = args.verbose; + +// Validation of arguments +// cs and ts validation +const csAndTsValidationResult: string = validatePairOfArgs(args.cs, args.ts); +if (csAndTsValidationResult) { + logger.error( + csAndTsValidationResult.replace('{0}', 'cs') + .replace('{1}', 'ts') + ); + process.exit(1); +} + +// skillId validation +if (!args.skillId) { + logger.error(`The 'skillId' argument should be provided.`); + process.exit(1); +} + +const configuration: Partial = { + skillId: args.skillId, + logger: logger +}; + +// outFolder validation -- the const is needed for reassuring 'configuration.outFolder' is not undefined +const outFolder: string = args.outFolder || resolve('./'); +configuration.outFolder = outFolder; + +// skillsFile validation +if (!args.skillsFile) { + configuration.skillsFile = join(configuration.outFolder, (args.ts ? join('src', 'skills.json') : 'skills.json')); +} else if (extname(args.skillsFile) !== '.json') { + logger.error(`The 'skillsFile' argument should be a JSON file.`); + process.exit(1); +} else { + const skillsFilePath: string = isAbsolute(args.skillsFile) ? args.skillsFile : join(resolve('./'), args.skillsFile); + if (!existsSync(skillsFilePath)) { + logger.error(`The 'skillsFile' argument leads to a non-existing file. + Please make sure to provide a valid path to your Assistant Skills configuration file.`); + process.exit(1); + } + configuration.skillsFile = skillsFilePath; +} + +// cognitiveModelsFile validation +const cognitiveModelsFilePath: string = args.cognitiveModelsFile || join( + configuration.outFolder, (args.ts ? join('src', 'cognitivemodels.json') : 'cognitivemodels.json')); +configuration.cognitiveModelsFile = cognitiveModelsFilePath; + +// language validation +const language: string = args.language || 'en-us'; +configuration.language = language; +const languageCode: string = (language.split('-'))[0]; + +// luisFolder validation +configuration.luisFolder = args.luisFolder || join(configuration.outFolder, 'Deployment', 'Resources', 'Skills', languageCode); + +// dispatchFolder validation +configuration.dispatchFolder = args.dispatchFolder || join(configuration.outFolder, 'Deployment', 'Resources', 'Dispatch', languageCode); + +// lgOutFolder validation +configuration.lgOutFolder = args.lgOutFolder || join(configuration.outFolder, (args.ts ? join('src', 'Services') : 'Services')); + +// dispatchName validation +if (!args.dispatchName) { + // try get the dispatch name from the cognitiveModels file + // tslint:disable-next-line + const cognitiveModelsFile: ICognitiveModelFile = require(cognitiveModelsFilePath); + configuration.dispatchName = cognitiveModelsFile.cognitiveModels[languageCode].dispatchModel.name; +} + +disconnectSkill( configuration); diff --git a/lib/typescript/botskills/src/botskills-list.ts b/lib/typescript/botskills/src/botskills-list.ts new file mode 100644 index 0000000000..0ca0c52f6f --- /dev/null +++ b/lib/typescript/botskills/src/botskills-list.ts @@ -0,0 +1,75 @@ +/** + * Copyright(c) Microsoft Corporation.All rights reserved. + * Licensed under the MIT License. + */ + +import * as program from 'commander'; +import { existsSync } from 'fs'; +import { extname, isAbsolute, join, resolve } from 'path'; +import { listSkill } from './functionality'; +import { ConsoleLogger, ILogger} from './logger'; +import { IListConfiguration } from './models'; +import { validatePairOfArgs } from './utils'; + +function showErrorHelp(): void { + program.outputHelp((str: string) => { + logger.error(str); + + return ''; + }); + process.exit(1); +} + +const logger: ILogger = new ConsoleLogger(); + +program.Command.prototype.unknownOption = (flag: string): void => { + logger.error(`Unknown arguments: ${flag}`); + showErrorHelp(); +}; + +program + .name('botskills list') + .description('List all the Skills connected to your assistant') + .option('-f, --skillsFile ', 'Path to assistant Skills configuration file') + .option('--cs', 'Determine your assistant project structure to be a CSharp-like structure') + .option('--ts', 'Determine your assistant project structure to be a TypeScript-like structure') + .option('--verbose', '[OPTIONAL] Output detailed information about the processing of the tool') + .action((cmd: program.Command, actions: program.Command) => undefined); + +const args: program.Command = program.parse(process.argv); + +logger.isVerbose = args.verbose; + +// cs and ts validation +const csAndTsValidationResult: string = validatePairOfArgs(args.cs, args.ts); +if (csAndTsValidationResult) { + logger.error( + csAndTsValidationResult.replace('{0}', 'cs') + .replace('{1}', 'ts') + ); + process.exit(1); +} + +// skillsFile validation +if (!args.skillsFile) { + args.skillsFile = args.ts ? join('src', 'skills.json') : 'skills.json'; +} else if (extname(args.skillsFile) !== '.json') { + logger.error(`The 'skillsFile' argument should be a JSON file.`); + process.exit(1); +} + +const skillsFilePath: string = isAbsolute(args.skillsFile) ? args.skillsFile : join(resolve('./'), args.skillsFile); +if (!existsSync(skillsFilePath)) { + logger.error(`The 'skillsFile' argument is absent or leads to a non-existing file. +Please make sure to provide a valid path to your Assistant Skills configuration file.`); + process.exit(1); +} + +const configuration: IListConfiguration = { + skillsFile: skillsFilePath, + logger: logger +}; + +listSkill(configuration); + +process.exit(0); diff --git a/lib/typescript/botskills/src/botskills.ts b/lib/typescript/botskills/src/botskills.ts new file mode 100644 index 0000000000..433a8087b2 --- /dev/null +++ b/lib/typescript/botskills/src/botskills.ts @@ -0,0 +1,63 @@ +#!/usr/bin/env node +/** + * Copyright(c) Microsoft Corporation.All rights reserved. + * Licensed under the MIT License. + */ + +// tslint:disable:no-object-literal-type-assertion +import * as program from 'commander'; +import * as process from 'process'; +import * as semver from 'semver'; +import { ConsoleLogger, ILogger} from './logger/logger'; + +const logger: ILogger = new ConsoleLogger(); + +// tslint:disable-next-line:no-var-requires no-require-imports +const pkg: IPackage = require('../package.json'); + +const requiredVersion: string = pkg.engines.node; +if (!semver.satisfies(process.version, requiredVersion)) { + logger.error(`Required node version ${requiredVersion} not satisfied with current version ${process.version}.`); + process.exit(1); +} + +program.Command.prototype.unknownOption = (flag: string): void => { + logger.error(`Unknown arguments: ${flag}`); + program.outputHelp((str: string) => { + logger.error(str); + + return ''; + }); + process.exit(1); +}; + +program + .version(pkg.version, '-v, --Version') + .description(`The skill program makes it easy to manipulate skills for Microsoft Bot Framework tools.`); + +program + .command('connect', 'connect any skill to your assistant bot') + .command('disconnect', 'disconnect a specific skill from your assitant bot') + .command('list', 'list the connected skills in the assistant'); + +const args: program.Command = program.parse(process.argv); +// args should be undefined is subcommand is executed +if (args) { + const unknownArgs: string[] = process.argv.slice(2); + logger.error(`Unknown arguments: ${unknownArgs.join(' ')}`); + program.outputHelp((str: string) => { + logger.error(str); + + return ''; + }); + process.exit(1); +} + +/** + * Interface for using typedef when requiring the package.json file + */ +interface IPackage { + name: string; + version: string; + engines: { node: string }; +} diff --git a/lib/typescript/botskills/src/functionality/connectSkill.ts b/lib/typescript/botskills/src/functionality/connectSkill.ts new file mode 100644 index 0000000000..9ffd36a279 --- /dev/null +++ b/lib/typescript/botskills/src/functionality/connectSkill.ts @@ -0,0 +1,189 @@ +/** + * Copyright(c) Microsoft Corporation.All rights reserved. + * Licensed under the MIT License. + */ + +import { existsSync, writeFileSync } from 'fs'; +import { isAbsolute, join, resolve } from 'path'; +import { get } from 'request-promise-native'; +import { ConsoleLogger, ILogger} from '../logger'; +import { IAction, IConnectConfiguration, ISkillFIle, ISkillManifest, IUtteranceSource } from '../models'; +import { authenticate, execute } from '../utils'; + +async function runCommand(command: string, description: string): Promise { + logger.command(description, command); + const parts: string[] = command.split(' '); + const cmd: string = parts[0]; + const commandArgs: string[] = parts.slice(1) + .filter((arg: string) => arg); + + try { + + return await execute(cmd, commandArgs); + } catch (err) { + + return err; + } +} + +async function getRemoteManifest(manifestUrl: string): Promise { + return get({ + uri: manifestUrl, + json: true + }); +} + +function getLocalManifest(manifestPath: string): ISkillManifest { + const skillManifestPath: string = isAbsolute(manifestPath) ? manifestPath : join(resolve('./'), manifestPath); + + if (!existsSync(skillManifestPath)) { + logger.error( + `The 'localManifest' argument leads to a non-existing file. Please make sure to provide a valid path to your Skill manifest.`); + process.exit(1); + } + + // tslint:disable-next-line: non-literal-require + return require(skillManifestPath); +} + +function validateManifestSchema(skillManifest: ISkillManifest): void { + if (!skillManifest.name) { + logger.error(`Missing property 'name' of the manifest`); + } + if (!skillManifest.id) { + logger.error(`Missing property 'id' of the manifest`); + } + if (!skillManifest.endpoint) { + logger.error(`Missing property 'endpoint' of the manifest`); + } + if (!skillManifest.authenticationConnections) { + logger.error(`Missing property 'authenticationConnections' of the manifest`); + } + if (!skillManifest.actions || !skillManifest.actions[0]) { + logger.error(`Missing property 'actions' of the manifest`); + } +} + +async function updateDispatch(configuration: IConnectConfiguration, manifest: ISkillManifest): Promise { + // Initializing variables for the updateDispatch scope + const dispatchFile: string = `${configuration.dispatchName}.dispatch`; + const dispatchJsonFile: string = `${configuration.dispatchName}.json`; + const intentName: string = manifest.id; + let luisDictionary: Map; + + logger.message('Getting intents for dispatch...'); + + luisDictionary = manifest.actions.filter((action: IAction) => action.definition.triggers.utteranceSources) + .reduce((acc: IUtteranceSource[], val: IAction) => acc.concat(val.definition.triggers.utteranceSources), []) + .reduce((acc: string[], val: IUtteranceSource) => acc.concat(val.source), []) + .reduce( + (acc: Map, val: string) => { + const luis: string[] = val.split('#'); + if (acc.has(luis[0])) { + const previous: string | undefined = acc.get(luis[0]); + acc.set(luis[0], previous + luis[1]); + } else { + acc.set(luis[0], luis[1]); + } + + return acc; + }, + new Map()); + + logger.message('Adding skill to Dispatch'); + + await Promise.all( + Array.from(luisDictionary.entries()) + .map(async(item: [string, string]) => { + const luisApp: string = item[0]; + const luFile: string = `${luisApp}.lu`; + const luisFile: string = `${luisApp}.luis`; + + // Parse LU file + logger.message(`Parsing ${luisApp} LU file...`); + let ludownParseCommand: string = `ludown parse toluis `; + ludownParseCommand += `--in ${join(configuration.luisFolder, luFile)} `; + ludownParseCommand += `--luis_culture ${configuration.language} `; + ludownParseCommand += `--out_folder ${configuration.luisFolder} `; //luisFolder should point to 'en' folder inside LUIS folder + ludownParseCommand += `--out "${luisApp}.luis"`; + + let dispatchAddCommand: string = `dispatch add `; + dispatchAddCommand += `--type file `; + dispatchAddCommand += `--name ${manifest.id} `; + dispatchAddCommand += `--filePath ${join(configuration.luisFolder, luisFile)} `; + dispatchAddCommand += `--intentName ${intentName} `; + dispatchAddCommand += `--dataFolder ${configuration.dispatchFolder} `; + dispatchAddCommand += `--dispatch ${join(configuration.dispatchFolder, dispatchFile)}`; + + logger.message(await runCommand(ludownParseCommand, `Parsing ${luisApp} LU file`)); + logger.message(await runCommand(dispatchAddCommand, `Executing dispatch add for the ${luisApp} LU file`)); + })); + + logger.message('Running dispatch refresh...'); + + let dispatchRefreshCommand: string = `dispatch refresh `; + dispatchRefreshCommand += `--dispatch ${join(configuration.dispatchFolder, dispatchFile)} `; + dispatchRefreshCommand += `--dataFolder ${configuration.dispatchFolder}`; + + await runCommand(dispatchRefreshCommand, `Executing dispatch refresh for the ${configuration.dispatchName} file`); + + logger.message('Running LuisGen...'); + + let luisgenCommand: string = `luisgen `; + luisgenCommand += `${join(configuration.dispatchFolder, dispatchJsonFile)} `; + luisgenCommand += `-${configuration.lgLanguage} "DispatchLuis" `; + luisgenCommand += `-o ${configuration.lgOutFolder}`; + + await runCommand(luisgenCommand, `Executing luisgen for the ${configuration.dispatchName} file`); +} + +export async function connectSkill(configuration: IConnectConfiguration): Promise { + + if (configuration.logger) { + logger = configuration.logger; + } + // Take skillManifest + const skillManifest: ISkillManifest = configuration.localManifest + ? getLocalManifest(configuration.localManifest) + : await getRemoteManifest(configuration.remoteManifest); + + // Manifest schema validation + validateManifestSchema(skillManifest); + + if (logger.isError) { + process.exit(1); + } + // End of manifest schema validation + + // Take VA Skills configurations + //tslint:disable-next-line: no-var-requires non-literal-require + const assistantSkillsFile: ISkillFIle = require(configuration.skillsFile); + const assistantSkills: ISkillManifest[] = assistantSkillsFile.skills; + + // Check if the skill is already connected to the assistant + if (assistantSkills.find((assistantSkill: ISkillManifest) => assistantSkill.id === skillManifest.id)) { + logger.warning(`The skill '${skillManifest.name}' is already registered.`); + process.exit(1); + } + + // Updating Dispatch + logger.message('Updating Dispatch'); + await updateDispatch(configuration, skillManifest); + + // Adding the skill manifest to the assistant skills array + logger.warning(`Appending '${skillManifest.name}' manifest to your assistant's skills configuration file.`); + assistantSkills.push(skillManifest); + + // Updating the assistant skills file's skills property with the assistant skills array + assistantSkillsFile.skills = assistantSkills; + + // Writing (and overriding) the assistant skills file + writeFileSync(configuration.skillsFile, JSON.stringify(assistantSkillsFile, undefined, 4)); + logger.success(`Successfully appended '${skillManifest.name}' manifest to your assistant's skills configuration file!`); + + // Configuring bot auth settings + logger.message('Configuring bot auth settings'); + await authenticate(configuration, skillManifest, logger); +} + +let logger: ILogger = new ConsoleLogger(); diff --git a/lib/typescript/botskills/src/functionality/disconnectSkill.ts b/lib/typescript/botskills/src/functionality/disconnectSkill.ts new file mode 100644 index 0000000000..260744cba3 --- /dev/null +++ b/lib/typescript/botskills/src/functionality/disconnectSkill.ts @@ -0,0 +1,101 @@ +/** + * Copyright(c) Microsoft Corporation.All rights reserved. + * Licensed under the MIT License. + */ + +import { existsSync, readFileSync, writeFileSync } from 'fs'; +import { join } from 'path'; +import { ConsoleLogger, ILogger } from '../logger'; +import { IDisconnectConfiguration, IDispatchFile, IDispatchService, ISkillFIle, ISkillManifest } from '../models/'; +import { execute } from '../utils'; + +async function runCommand(command: string[], description: string): Promise { + logger.command(description, command.join(' ')); + const cmd: string = command[0]; + const commandArgs: string[] = command.slice(1) + .filter((arg: string) => arg); + + try { + return await execute(cmd, commandArgs); + } catch (err) { + + return err; + } +} + +async function updateDispatch(configuration: IDisconnectConfiguration): Promise { + // Initializing variables for the updateDispatch scope + const dispatchFile: string = `${configuration.dispatchName}.dispatch`; + const dispatchJsonFile: string = `${configuration.dispatchName}.json`; + + logger.message('Removing skill from dispatch...'); + + // dispatch remove(?) + if (!existsSync(join(configuration.dispatchFolder, dispatchFile))) { + logger.error(`Could not find file ${dispatchFile}. Please provide the 'dispatchName' and 'dispatchFolder' parameters.`); + process.exit(1); + } + // tslint:disable-next-line:no-var-require non-literal-require + const dispatchData: IDispatchFile = JSON.parse( + readFileSync(join(configuration.dispatchFolder, dispatchFile)) + .toString()); + const serviceToRemove: IDispatchService | undefined = dispatchData.services.find((service: IDispatchService) => + service.name === configuration.skillId); + if (serviceToRemove) { + dispatchData.serviceIds.splice(dispatchData.serviceIds.findIndex((serviceId: string) => serviceId === serviceToRemove.id)); + dispatchData.services.splice(dispatchData.services.findIndex((service: IDispatchService) => + service.name === configuration.skillId)); + writeFileSync(join(configuration.dispatchFolder, dispatchFile), JSON.stringify(dispatchData, undefined, 4)); + } + + logger.message('Running Dispatch refresh'); + + const dispatchRefreshCmd: string[] = ['dispatch', 'refresh']; + dispatchRefreshCmd.push(...['--dispatch', join(configuration.dispatchFolder, dispatchFile)]); + dispatchRefreshCmd.push(...['--dataFolder', configuration.dispatchFolder]); + await runCommand(dispatchRefreshCmd, `Executing dispatch refresh for the ${configuration.dispatchName} file`); + + logger.message('Running LuisGen...'); + + const luisgenCmd: string[] = ['luisgen']; + luisgenCmd.push(join(configuration.dispatchFolder, dispatchJsonFile)); + luisgenCmd.push(...[`-${configuration.language}`, '"DispatchLuis"']); + luisgenCmd.push(...['-o', configuration.lgOutFolder]); + await runCommand(luisgenCmd, `Executing luisgen for the ${configuration.dispatchName} file`); +} + +export async function disconnectSkill(configuration: IDisconnectConfiguration): Promise { + if (configuration.logger) { + logger = configuration.logger; + } + + // Take VA Skills configurations + //tslint:disable-next-line: no-var-requires non-literal-require + const assistantSkillsFile: ISkillFIle = require(configuration.skillsFile); + const assistantSkills: ISkillManifest[] = assistantSkillsFile.skills; + + // Check if the skill is present in the assistant + const skillToRemove: ISkillManifest | undefined = assistantSkills.find((assistantSkill: ISkillManifest) => + assistantSkill.id === configuration.skillId + ); + + if (!skillToRemove) { + logger.warning(`The skill '${configuration.skillId}' is not present in the assistant Skills configuration file. +Run 'botskills list --assistantSkills ""' in order to list all the skills connected to your assistant`); + process.exit(1); + } else { + await updateDispatch(configuration); + // Removing the skill manifest from the assistant skills array + logger.warning(`Removing the '${configuration.skillId}' skill from your assistant's skills configuration file.`); + assistantSkills.splice(assistantSkills.indexOf(skillToRemove), 1); + + // Updating the assistant skills file's skills property with the assistant skills array + assistantSkillsFile.skills = assistantSkills; + + // Writing (and overriding) the assistant skills file + writeFileSync(configuration.skillsFile, JSON.stringify(assistantSkillsFile, undefined, 4)); + logger.success(`Successfully removed '${configuration.skillId}' skill from your assistant's skills configuration file.`); + } +} + +let logger: ILogger = new ConsoleLogger(); diff --git a/lib/typescript/botskills/src/functionality/index.ts b/lib/typescript/botskills/src/functionality/index.ts new file mode 100644 index 0000000000..cb82266762 --- /dev/null +++ b/lib/typescript/botskills/src/functionality/index.ts @@ -0,0 +1,8 @@ +/** + * Copyright(c) Microsoft Corporation.All rights reserved. + * Licensed under the MIT License. + */ + +export { connectSkill } from './connectSkill'; +export { disconnectSkill } from './disconnectSkill'; +export { listSkill } from './listSkill'; diff --git a/lib/typescript/botskills/src/functionality/listSkill.ts b/lib/typescript/botskills/src/functionality/listSkill.ts new file mode 100644 index 0000000000..1237d68007 --- /dev/null +++ b/lib/typescript/botskills/src/functionality/listSkill.ts @@ -0,0 +1,31 @@ +/** + * Copyright(c) Microsoft Corporation.All rights reserved. + * Licensed under the MIT License. + */ + +import { ConsoleLogger, ILogger} from '../logger'; +import { IListConfiguration, ISkillFIle, ISkillManifest } from '../models'; + +export async function listSkill(configuration: IListConfiguration): Promise { + if (configuration.logger) { + logger = configuration.logger; + } + + // Take VA Skills configurations + //tslint:disable-next-line:non-literal-require + const assistantSkillsFile: ISkillFIle = require(configuration.skillsFile); + const assistantSkills: ISkillManifest[] = assistantSkillsFile.skills; + + if (assistantSkills.length < 1) { + logger.message('There are no Skills connected to the assistant.'); + } else { + let message: string = `The skills already connected to the assistant are the following:`; + assistantSkills.forEach((skillManifest: ISkillManifest) => { + message += `\n\t- ${skillManifest.name}`; + }); + + logger.message(message); + } +} + +let logger: ILogger = new ConsoleLogger(); diff --git a/lib/typescript/botskills/src/index.ts b/lib/typescript/botskills/src/index.ts new file mode 100644 index 0000000000..8c36dada3a --- /dev/null +++ b/lib/typescript/botskills/src/index.ts @@ -0,0 +1,10 @@ +#!/usr/bin/env node +/** + * Copyright(c) Microsoft Corporation.All rights reserved. + * Licensed under the MIT License. + */ + +export * from './functionality'; +export * from './logger'; +export * from './models'; +export * from './utils'; diff --git a/lib/typescript/botskills/src/logger/index.ts b/lib/typescript/botskills/src/logger/index.ts new file mode 100644 index 0000000000..333280ab18 --- /dev/null +++ b/lib/typescript/botskills/src/logger/index.ts @@ -0,0 +1,6 @@ +/** + * Copyright(c) Microsoft Corporation.All rights reserved. + * Licensed under the MIT License. + */ + +export { ConsoleLogger, ILogger } from './logger'; diff --git a/lib/typescript/botskills/src/logger/logger.ts b/lib/typescript/botskills/src/logger/logger.ts new file mode 100644 index 0000000000..f97a2dd65f --- /dev/null +++ b/lib/typescript/botskills/src/logger/logger.ts @@ -0,0 +1,53 @@ +/** + * Copyright(c) Microsoft Corporation.All rights reserved. + * Licensed under the MIT License. + */ + +// tslint:disable: no-console +import chalk from 'chalk'; + +export interface ILogger { + isError: boolean; + isVerbose: boolean; + + command(message: string, command: string): void; + error(message: string): void; + message(message: string): void; + success(message: string, withoutFormat?: boolean): void; + warning(message: string): void; +} + +export class ConsoleLogger implements ILogger { + // tslint:disable: variable-name + private _isError: boolean = false; + private _isVerbose: boolean = false; + // tslint:enable: variable-name + public get isError(): boolean { return this._isError; } + public get isVerbose(): boolean { return this._isVerbose; } + public set isVerbose(value: boolean) { this._isVerbose = value || false; } + + public error(message: string): void { + console.error(chalk.redBright(message)); + this._isError = true; + } + + public message(message: string): void { + console.log(chalk.bold(message)); + } + + public success(message: string): void { + console.log(chalk.greenBright(message)); + } + + public warning(message: string): void { + console.log(chalk.yellow(message)); + } + + public command(message: string, command: string): void { + if (this.isVerbose) { + console.log(chalk.cyan(command)); + } else { + this.message(message); + } + } +} diff --git a/lib/typescript/botskills/src/models/authentication.ts b/lib/typescript/botskills/src/models/authentication.ts new file mode 100644 index 0000000000..af1669767c --- /dev/null +++ b/lib/typescript/botskills/src/models/authentication.ts @@ -0,0 +1,56 @@ +/** + * Copyright(c) Microsoft Corporation.All rights reserved. + * Licensed under the MIT License. + */ + +export interface IScopeManifest { + resourceAppId: string; + resourceAccess: IResourceAccess[]; +} + +export interface IResourceAccess { + id: string; + // tslint:disable-next-line:no-reserved-keywords + type: string; +} + +export interface IAzureAuthSetting { + etag: string; + id: string; + kind: string; + location: string; + name: string; + properties: { + clientId: string; + clientSecret: string; + parameters: { + key: string; + value: string; + }[]; + provisioningState: string; + scopes: string; + serviceProviderDisplayName: string; + serviceProviderId: string; + settingId: string; + }; + resourceGroup: string; + sku: string; + tags: string; + // tslint:disable-next-line:no-reserved-keywords + type: string; +} + +export interface IAppSettingOauthConnection { + oauthConnections: IOauthConnection[]; + microsoftAppId: string; + microsoftAppPassword: string; +} + +export interface IOauthConnection { + name: string; + provider: string; +} + +export interface IAppShowReplyUrl { + replyUrls: string[]; +} diff --git a/lib/typescript/botskills/src/models/cognitiveFile.ts b/lib/typescript/botskills/src/models/cognitiveFile.ts new file mode 100644 index 0000000000..0c344c68a1 --- /dev/null +++ b/lib/typescript/botskills/src/models/cognitiveFile.ts @@ -0,0 +1,14 @@ +/** + * Copyright(c) Microsoft Corporation.All rights reserved. + * Licensed under the MIT License. + */ + +export interface ICognitiveModelFile { + cognitiveModels: { + [key: string]: { + dispatchModel: { + name: string; + }; + }; + }; +} diff --git a/lib/typescript/botskills/src/models/connectConfiguration.ts b/lib/typescript/botskills/src/models/connectConfiguration.ts new file mode 100644 index 0000000000..48dbe39996 --- /dev/null +++ b/lib/typescript/botskills/src/models/connectConfiguration.ts @@ -0,0 +1,24 @@ +/** + * Copyright(c) Microsoft Corporation.All rights reserved. + * Licensed under the MIT License. + */ + +import { ILogger } from '../logger'; + +export interface IConnectConfiguration { + botName: string; + localManifest: string; + remoteManifest: string; + dispatchName: string; + language: string; + luisFolder: string; + dispatchFolder: string; + outFolder: string; + lgOutFolder: string; + skillsFile: string; + resourceGroup: string; + appSettingsFile: string; + cognitiveModelsFile: string; + lgLanguage: string; + logger?: ILogger; +} diff --git a/lib/typescript/botskills/src/models/disconnectConfiguration.ts b/lib/typescript/botskills/src/models/disconnectConfiguration.ts new file mode 100644 index 0000000000..16a5ddde31 --- /dev/null +++ b/lib/typescript/botskills/src/models/disconnectConfiguration.ts @@ -0,0 +1,19 @@ +/** + * Copyright(c) Microsoft Corporation.All rights reserved. + * Licensed under the MIT License. + */ + +import { ILogger } from '../logger'; + +export interface IDisconnectConfiguration { + skillId: string; + skillsFile: string; + outFolder: string; + cognitiveModelsFile: string; + language: string; + luisFolder: string; + dispatchFolder: string; + lgOutFolder: string; + dispatchName: string; + logger?: ILogger; +} diff --git a/lib/typescript/botskills/src/models/dispatchFile.ts b/lib/typescript/botskills/src/models/dispatchFile.ts new file mode 100644 index 0000000000..a87b099250 --- /dev/null +++ b/lib/typescript/botskills/src/models/dispatchFile.ts @@ -0,0 +1,21 @@ +/** + * Copyright(c) Microsoft Corporation.All rights reserved. + * Licensed under the MIT License. + */ + +export interface IDispatchFile { + services: IDispatchService[]; + serviceIds: string[]; +} + +export interface IDispatchService { + intentName: string; + appId: string; + authoringKey: string; + version: string; + region: string; + // tslint:disable-next-line:no-reserved-keywords + type: string; + name: string; + id: string; +} diff --git a/lib/typescript/botskills/src/models/index.ts b/lib/typescript/botskills/src/models/index.ts new file mode 100644 index 0000000000..863e9e1963 --- /dev/null +++ b/lib/typescript/botskills/src/models/index.ts @@ -0,0 +1,28 @@ +/** + * Copyright(c) Microsoft Corporation.All rights reserved. + * Licensed under the MIT License. + */ + +export { + IAppSettingOauthConnection, + IAppShowReplyUrl, + IAzureAuthSetting, + IOauthConnection, + IResourceAccess, + IScopeManifest } from './authentication'; +export { ICognitiveModelFile } from './cognitiveFile'; +export { IConnectConfiguration } from './connectConfiguration'; +export { IDisconnectConfiguration } from './disconnectConfiguration'; +export { IDispatchFile, IDispatchService } from './dispatchFile'; +export { IListConfiguration } from './listConfiguration'; +export { ISkillFIle } from './skillFile'; +export { + IAction, + IActionDefinition, + IAuthenticationConnection, + IEvent, + ISkillManifest, + ISlot, + ITriggers, + IUtterance, + IUtteranceSource } from './skillManifest'; diff --git a/lib/typescript/botskills/src/models/listConfiguration.ts b/lib/typescript/botskills/src/models/listConfiguration.ts new file mode 100644 index 0000000000..5c96c2a96f --- /dev/null +++ b/lib/typescript/botskills/src/models/listConfiguration.ts @@ -0,0 +1,11 @@ +/** + * Copyright(c) Microsoft Corporation.All rights reserved. + * Licensed under the MIT License. + */ + +import { ILogger } from '../logger'; + +export interface IListConfiguration { + skillsFile: string; + logger?: ILogger; +} diff --git a/lib/typescript/botskills/src/models/skillFile.ts b/lib/typescript/botskills/src/models/skillFile.ts new file mode 100644 index 0000000000..82d05225d2 --- /dev/null +++ b/lib/typescript/botskills/src/models/skillFile.ts @@ -0,0 +1,10 @@ +/** + * Copyright(c) Microsoft Corporation.All rights reserved. + * Licensed under the MIT License. + */ + +import { ISkillManifest } from './skillManifest'; + +export interface ISkillFIle { + skills: ISkillManifest[]; +} diff --git a/lib/typescript/botskills/src/models/skillManifest.ts b/lib/typescript/botskills/src/models/skillManifest.ts new file mode 100644 index 0000000000..cdecdca87b --- /dev/null +++ b/lib/typescript/botskills/src/models/skillManifest.ts @@ -0,0 +1,57 @@ +/** + * Copyright(c) Microsoft Corporation.All rights reserved. + * Licensed under the MIT License. + */ + +export interface ISkillManifest { + id: string; + name: string; + endpoint: string; + description: string; + suggestedAction: string; + iconUrl: string; + authenticationConnections: IAuthenticationConnection[]; + actions: IAction[]; +} + +export interface IUtteranceSource { + locale: string; + source: string[]; +} + +export interface ISlot { + name: string; + types: string[]; +} + +export interface IUtterance { + locale: string; + text: string[]; +} + +export interface ITriggers { + utterances: IUtterance[]; + utteranceSources: IUtteranceSource[]; + events: IEvent[]; +} + +export interface IEvent { + name: string; +} + +export interface IAuthenticationConnection { + id: string; + serviceProviderId: string; + scopes: string; +} + +export interface IActionDefinition { + description: string; + slots: ISlot[]; + triggers: ITriggers; +} + +export interface IAction { + id: string; + definition: IActionDefinition; +} diff --git a/lib/typescript/botskills/src/utils/authenticationUtils.ts b/lib/typescript/botskills/src/utils/authenticationUtils.ts new file mode 100644 index 0000000000..70c6d7e93b --- /dev/null +++ b/lib/typescript/botskills/src/utils/authenticationUtils.ts @@ -0,0 +1,208 @@ +/** + * Copyright(c) Microsoft Corporation.All rights reserved. + * Licensed under the MIT License. + */ + +import { writeFileSync } from 'fs'; +import { ILogger } from '../logger'; +import { + IAppSettingOauthConnection, + IAppShowReplyUrl, + IAuthenticationConnection, + IAzureAuthSetting, + IConnectConfiguration, + IOauthConnection, + IScopeManifest, + ISkillManifest } from '../models'; +import { extractArgs, tryExecute } from './'; + +const scopeMap: Map = new Map([ + ['Files.Read.Selected', '5447fe39-cb82-4c1a-b977-520e67e724eb'], + ['Files.ReadWrite.Selected', '17dde5bd-8c17-420f-a486-969730c1b827'], + ['Files.ReadWrite.AppFolder', '8019c312-3263-48e6-825e-2b833497195b'], + ['Reports.Read.All', '02e97553-ed7b-43d0-ab3c-f8bace0d040c'], + ['Sites.ReadWrite.All', '89fe6a52-be36-487e-b7d8-d061c450a026'], + ['Member.Read.Hidden', '658aa5d8-239f-45c4-aa12-864f4fc7e490'], + ['Tasks.ReadWrite.Shared', 'c5ddf11b-c114-4886-8558-8a4e557cd52b'], + ['Tasks.Read.Shared', '88d21fd4-8e5a-4c32-b5e2-4a1c95f34f72'], + ['Contacts.ReadWrite.Shared', 'afb6c84b-06be-49af-80bb-8f3f77004eab'], + ['Contacts.Read.Shared', '242b9d9e-ed24-4d09-9a52-f43769beb9d4'], + ['Calendars.ReadWrite.Shared', '12466101-c9b8-439a-8589-dd09ee67e8e9'], + ['Calendars.Read.Shared', '2b9c4092-424d-4249-948d-b43879977640'], + ['Mail.Send.Shared', 'a367ab51-6b49-43bf-a716-a1fb06d2a174'], + ['Mail.ReadWrite.Shared', '5df07973-7d5d-46ed-9847-1271055cbd51'], + ['Mail.Read.Shared', '7b9103a5-4610-446b-9670-80643382c1fa'], + ['User.Read', 'e1fe6dd8-ba31-4d61-89e7-88639da4683d'], + ['User.ReadWrite', 'b4e74841-8e56-480b-be8b-910348b18b4c'], + ['User.ReadBasic.All', 'b340eb25-3456-403f-be2f-af7a0d370277'], + ['User.Read.All', 'a154be20-db9c-4678-8ab7-66f6cc099a59'], + ['User.ReadWrite.All', '204e0828-b5ca-4ad8-b9f3-f32a958e7cc4'], + ['Group.Read.All', '5f8c59db-677d-491f-a6b8-5f174b11ec1d'], + ['Group.ReadWrite.All', '4e46008b-f24c-477d-8fff-7bb4ec7aafe0'], + ['Directory.Read.All', '06da0dbc-49e2-44d2-8312-53f166ab848a'], + ['Directory.ReadWrite.All', 'c5366453-9fb0-48a5-a156-24f0c49a4b84'], + ['Directory.AccessAsUser.All', '0e263e50-5827-48a4-b97c-d940288653c7'], + ['Mail.Read', '570282fd-fa5c-430d-a7fd-fc8dc98a9dca'], + ['Mail.ReadWrite', '024d486e-b451-40bb-833d-3e66d98c5c73'], + ['Mail.Send', 'e383f46e-2787-4529-855e-0e479a3ffac0'], + ['Calendars.Read', '465a38f9-76ea-45b9-9f34-9e8b0d4b0b42'], + ['Calendars.ReadWrite', '1ec239c2-d7c9-4623-a91a-a9775856bb36'], + ['Contacts.Read', 'ff74d97f-43af-4b68-9f2a-b77ee6968c5d'], + ['Contacts.ReadWrite', 'd56682ec-c09e-4743-aaf4-1a3aac4caa21'], + ['Files.Read', '10465720-29dd-4523-a11a-6a75c743c9d9'], + ['Files.ReadWrite', '5c28f0bf-8a70-41f1-8ab2-9032436ddb65'], + ['Files.Read.All', 'df85f4d6-205c-4ac5-a5ea-6bf408dba283'], + ['Files.ReadWrite.All', '863451e7-0667-486c-a5d6-d135439485f0'], + ['Sites.Read.All', '205e70e5-aba6-4c52-a976-6d2d46c48043'], + ['openid', '37f7f235-527c-4136-accd-4a02d197296e'], + ['offline_access', '7427e0e9-2fba-42fe-b0c0-848c9e6a8182'], + ['People.Read', 'ba47897c-39ec-4d83-8086-ee8256fa737d'], + ['Notes.Create', '9d822255-d64d-4b7a-afdb-833b9a97ed02'], + ['Notes.ReadWrite.CreatedByApp', 'ed68249d-017c-4df5-9113-e684c7f8760b'], + ['Notes.Read', '371361e4-b9e2-4a3f-8315-2a301a3b0a3d'], + ['Notes.ReadWrite', '615e26af-c38a-4150-ae3e-c3b0d4cb1d6a'], + ['Notes.Read.All', 'dfabfca6-ee36-4db2-8208-7a28381419b3'], + ['Notes.ReadWrite.All', '64ac0503-b4fa-45d9-b544-71a463f05da0'], + ['Tasks.Read', 'f45671fb-e0fe-4b4b-be20-3d3ce43f1bcb'], + ['Tasks.ReadWrite', '2219042f-cab5-40cc-b0d2-16b1540b4c5f'], + ['email', '64a6cdd6-aab1-4aaf-94b8-3cc8405e90d0'], + ['profile', '14dad69e-099b-42c9-810b-d002981feec1'] +]); + +function getScopeId(scope: string): string { + return scopeMap.get(scope) || ''; +} + +function createScopeManifest(scopes: string[]): IScopeManifest[] { + return [{ + resourceAppId: '00000003-0000-0000-c000-000000000000', + resourceAccess: scopes.filter((scope: string) => scopeMap.has(scope)) + .map((scope: string) => { + return { + id: getScopeId(scope), + type: 'Scope' + }; + }) + }]; +} + +// tslint:disable-next-line:max-func-body-length export-name +export async function authenticate(configuration: IConnectConfiguration, manifest: ISkillManifest, logger: ILogger): Promise { + // configuring bot auth settings + logger.message('Checking for authentication settings ...'); + if (manifest.authenticationConnections) { + const aadConfig: IAuthenticationConnection | undefined = manifest.authenticationConnections.find( + (connection: IAuthenticationConnection) => connection.serviceProviderId === 'Azure Active Directory v2'); + if (aadConfig) { + logger.message('Configuring Azure AD connection ...'); + + let connectionName: string = aadConfig.id; + const newScopes: string[] = aadConfig.scopes.split(', '); + let scopes: string[] = newScopes.slice(0); + + // check for existing aad connection + let listAuthSettingsCmd: string = `az bot authsetting list `; + listAuthSettingsCmd += `-n ${configuration.botName} `; + listAuthSettingsCmd += `-g ${configuration.resourceGroup}`; + + const connectionsResult: string = await tryExecute('az', extractArgs(listAuthSettingsCmd)); + const connections: IAzureAuthSetting[] = JSON.parse(connectionsResult); + const aadConnection: IAzureAuthSetting | undefined = connections.find( + (connection: IAzureAuthSetting) => connection.properties.serviceProviderDisplayName === 'Azure Active Directory v2'); + if (aadConnection) { + const settingName: string = aadConnection.name.split('/')[1]; + + // Get current aad auth setting + let showAuthSettingsCmd: string = `az bot authsetting show `; + showAuthSettingsCmd += `-n ${configuration.botName} `; + showAuthSettingsCmd += `-g ${configuration.resourceGroup} `; + showAuthSettingsCmd += `-c ${settingName}`; + + const botAuthSettingResult: string = await tryExecute('az', extractArgs(showAuthSettingsCmd)); + const botAuthSetting: IAzureAuthSetting = JSON.parse(botAuthSettingResult); + const existingScopes: string[] = botAuthSetting.properties.scopes.split(' '); + scopes = scopes.concat(existingScopes); + connectionName = settingName; + + // delete current aad auth connection + let deleteAuthSettingCmd: string = `az bot authsetting delete `; + deleteAuthSettingCmd += `-n ${configuration.botName} `; + deleteAuthSettingCmd += `-g ${configuration.resourceGroup} `; + deleteAuthSettingCmd += `-c ${settingName}`; + + const deleteResult: string = await tryExecute('az', extractArgs(deleteAuthSettingCmd)); + } + + // update appsettings.json + logger.message('Updating appsettings.json ...'); + // tslint:disable-next-line:non-literal-require + const appSettings: IAppSettingOauthConnection = require(configuration.appSettingsFile); + + // check for and remove existing aad connections + if (appSettings.oauthConnections) { + appSettings.oauthConnections = appSettings.oauthConnections.filter( + (connection: IOauthConnection) => connection.provider !== 'Azure Active Directory v2'); + } + + // set or add new oauth setting + const oauthSetting: IOauthConnection = { name: connectionName, provider: 'Azure Active Directory v2' }; + if (!appSettings.oauthConnections) { + appSettings.oauthConnections = [oauthSetting]; + } else { + appSettings.oauthConnections.push(oauthSetting); + } + + // update appsettings.json + writeFileSync(configuration.appSettingsFile, JSON.stringify(appSettings, undefined, 4)); + + // Remove duplicate scopes + scopes = [...new Set(scopes)]; + const scopeManifest: IScopeManifest[] = createScopeManifest(scopes); + + // get the information of the app + let azureAppShowCmd: string = `az ad app show`; + azureAppShowCmd += `--id ${appSettings.microsoftAppId}`; + const azureAppShowResult: string = await tryExecute('az', extractArgs(azureAppShowCmd)); + const azureAppReplyUrls: IAppShowReplyUrl = JSON.parse(azureAppShowResult); + + // get the Reply Urls from the app + const replyUrlsSet: Set = new Set(azureAppReplyUrls.replyUrls); + // append the necessary Url if it's not already present + replyUrlsSet.add('https://token.botframework.com/.auth/web/redirect'); + const replyUrls: string[] = [...replyUrlsSet]; + + // Update MSA scopes + logger.message('Configuring MSA app scopes ...'); + let azureAppUpdateCmd: string = `az ad app update `; + azureAppUpdateCmd += `--id ${appSettings.microsoftAppId} `; + azureAppUpdateCmd += `--reply-urls ${replyUrls.join(' ')} `; + const scopeManifestText: string = JSON.stringify(scopeManifest) + .replace(/\"/g, '\''); + azureAppUpdateCmd += `--required-resource-accesses "${scopeManifestText}"`; + + const errorResult: string = await tryExecute('az', extractArgs(azureAppUpdateCmd)); + // Catch error: Updates to converged applications are not allowed in this version. + if (errorResult) { + logger.warning('Could not configure scopes automatically.'); + // manualScopesRequired = true + } + + logger.message('Updating bot oauth settings ...'); + let authSettingCmd: string = `az bot authsetting create `; + authSettingCmd += `--name ${configuration.botName} `; + authSettingCmd += `--resource-group ${configuration.resourceGroup} `; + authSettingCmd += `--setting-name ${connectionName} `; + authSettingCmd += `--client-id "${appSettings.microsoftAppId}" `; + authSettingCmd += `--client-secret "${appSettings.microsoftAppPassword}" `; + authSettingCmd += `--service Aadv2 `; + authSettingCmd += `--parameters clientId="${appSettings.microsoftAppId}" `; + authSettingCmd += `clientSecret="${appSettings.microsoftAppPassword}" tenantId=common `; + authSettingCmd += `--provider-scope-string "${scopes.join(' ')}"`; + + await tryExecute('az', extractArgs(authSettingCmd)); + } else { + logger.error('Could not configure authentication connection automatically.'); + // $manualAuthRequired = $true + } + } +} diff --git a/lib/typescript/botskills/src/utils/childProcessUtils.ts b/lib/typescript/botskills/src/utils/childProcessUtils.ts new file mode 100644 index 0000000000..e62df640e0 --- /dev/null +++ b/lib/typescript/botskills/src/utils/childProcessUtils.ts @@ -0,0 +1,78 @@ +/** + * Copyright(c) Microsoft Corporation.All rights reserved. + * Licensed under the MIT License. + */ + +import * as child_process from 'child_process'; +import { join } from 'path'; + +async function execDispatch(args: string[]): Promise { + const dispatchPath: string = join(__dirname, '..', '..', 'node_modules', 'botdispatch', 'bin', 'netcoreapp2.1', 'Dispatch.dll'); + + // tslint:disable-next-line: typedef + return new Promise((pResolve, pReject) => { + child_process.spawn('dotnet', [dispatchPath, ...args], { stdio: 'inherit' }) + .on('close', (code: number) => { + pResolve(''); + }) + .on('error', (err: Error) => { + pReject(err); + }); + }); +} + +export async function spawn(command: string, args: string[]): Promise { + + // tslint:disable-next-line: typedef + return new Promise((pResolve, pReject) => { + child_process.spawn(command, args, { stdio: 'inherit', env: process.env, argv0: command, cwd: join(__dirname, '..') }) + .on('close', (code: number) => { + pResolve(''); + }) + .on('error', (err: Error) => { + pReject(err); + }); + }); +} + +export async function execute(command: string, args: string[]): Promise { + if (command === 'dispatch') { + return execDispatch(args); + } + + // tslint:disable-next-line: typedef + return new Promise((pResolve, pReject) => { + child_process.exec(`${command} ${args.join(' ')}`, (err: child_process.ExecException | null, stdout: string, stderr: string) => { + if (stderr) { + pReject(stderr); + } + pResolve(stdout); + }); + }); +} + +export async function tryExecute(command: string, args: string[]): Promise { + // tslint:disable-next-line: typedef + return new Promise((pResolve, pReject) => { + try { + child_process.exec( + `${command} ${args.join(' ')}`, + (err: child_process.ExecException | null, stdout: string, stderr: string) => { + if (stderr) { + pReject(stderr); + } + pResolve(stdout); + }); + } catch (err) { + + return err; + } + }); +} + +export function extractArgs(command: string): string[] { + const parts: string[] = command.split(' '); + + return parts.slice(1) + .filter((arg: string) => arg); +} diff --git a/lib/typescript/botskills/src/utils/index.ts b/lib/typescript/botskills/src/utils/index.ts new file mode 100644 index 0000000000..f929eb1973 --- /dev/null +++ b/lib/typescript/botskills/src/utils/index.ts @@ -0,0 +1,8 @@ +/** + * Copyright(c) Microsoft Corporation.All rights reserved. + * Licensed under the MIT License. + */ + +export { execute, extractArgs, spawn, tryExecute } from './childProcessUtils'; +export { authenticate } from './authenticationUtils'; +export { validatePairOfArgs } from './validationUtils'; diff --git a/lib/typescript/botskills/src/utils/validationUtils.ts b/lib/typescript/botskills/src/utils/validationUtils.ts new file mode 100644 index 0000000000..0c93a564c3 --- /dev/null +++ b/lib/typescript/botskills/src/utils/validationUtils.ts @@ -0,0 +1,23 @@ +/** + * Copyright(c) Microsoft Corporation.All rights reserved. + * Licensed under the MIT License. + */ + +import { ILogger } from '../logger'; + +/** + * @param arg1 First argument of the pair of arguments. + * @param arg2 Second argument of the pair of arguments. + * @returns Returns an empty string if the validation is successful, + * or a string with placeholders '{0}' and '{1}' for printing the necessary message. + */ +// tslint:disable-next-line:export-name +export function validatePairOfArgs(arg1: string | undefined, arg2: string | undefined): string { + if (!arg1 && !arg2) { + return `One of the arguments '{0}' or '{1}' should be provided.`; + } else if (arg1 && arg2) { + return `Only one of the arguments '{0' or '{1}' should be provided.`; + } + + return ''; +} diff --git a/lib/typescript/botskills/test/mocha.opts b/lib/typescript/botskills/test/mocha.opts new file mode 100644 index 0000000000..1104a2cf34 --- /dev/null +++ b/lib/typescript/botskills/test/mocha.opts @@ -0,0 +1,4 @@ +--timeout 50000 +--recursive +./**/*Test.js +--colors diff --git a/lib/typescript/botskills/tsconfig.json b/lib/typescript/botskills/tsconfig.json new file mode 100644 index 0000000000..6040509afb --- /dev/null +++ b/lib/typescript/botskills/tsconfig.json @@ -0,0 +1,54 @@ +{ + "compilerOptions": { + /* Basic Options */ + "target": "ES2017", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'commonjs', 'amd', 'system', 'umd' or 'es2015'. */ + // "lib": [], /* Specify library files to be included in the compilation: */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + "outDir": "./lib", /* Redirect output structure to the directory. */ + "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + + /* Source Map Options */ + // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + } +} diff --git a/lib/typescript/botskills/tslint.json b/lib/typescript/botskills/tslint.json new file mode 100644 index 0000000000..75f12624c4 --- /dev/null +++ b/lib/typescript/botskills/tslint.json @@ -0,0 +1,26 @@ +{ + "defaultSeverity": "error", + "extends": [ + "tslint:recommended", + "tslint-microsoft-contrib" + ], + "linterOptions":{ + "exclude": [ + "src/dialogs/shared/resources/*" + ] + }, + "rules": { + "function-name": [ true, + { + "static-method-regex":"^[a-z][\\w\\d]+$" + } + ], + "linebreak-style": [false], + "member-ordering": [false], + "no-relative-imports": [false], + "completed-docs": false + }, + "rulesDirectory": [ + "tslint-microsoft-contrib" + ] + } \ No newline at end of file