diff --git a/.gitignore b/.gitignore index 19fb83c23..e0a9e19f5 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,10 @@ __pycache__ docker-compose.yml compose-override.yml +build-installer.sh +docker-compose-base.yml +build-options.json +iotstack_build_*.zip postbuild.sh pre_backup.sh post_backup.sh @@ -15,6 +19,8 @@ post_restore.sh .docker_notinstalled .docker_outofdate .new_install +.installed +.env !.gitkeep diff --git a/.internal/.dockerignore b/.internal/.dockerignore new file mode 100644 index 000000000..cf7098890 --- /dev/null +++ b/.internal/.dockerignore @@ -0,0 +1 @@ +**/node_modules diff --git a/.internal/.gitignore b/.internal/.gitignore new file mode 100644 index 000000000..1c7815555 --- /dev/null +++ b/.internal/.gitignore @@ -0,0 +1,39 @@ +.DS_STORE +node_modules +scripts/flow/*/.flowconfig +.flowconfig +*~ +*.pyc +.grunt +_SpecRunner.html +__benchmarks__ +build/ +remote-repo/ +coverage/ +.module-cache +fixtures/dom/public/react-dom.js +fixtures/dom/public/react.js +test/the-files-to-test.generated.js +*.log* +chrome-user-data +*.sublime-project +*.sublime-workspace +.idea +*.iml +.vscode +*.swp +*.swo +.temp + +packages/react-devtools-core/dist +packages/react-devtools-extensions/chrome/build +packages/react-devtools-extensions/chrome/*.crx +packages/react-devtools-extensions/chrome/*.pem +packages/react-devtools-extensions/firefox/build +packages/react-devtools-extensions/firefox/*.xpi +packages/react-devtools-extensions/firefox/*.pem +packages/react-devtools-extensions/shared/build +packages/react-devtools-extensions/.tempUserDataDir +packages/react-devtools-inline/dist +packages/react-devtools-shell/dist +packages/react-devtools-scheduling-profiler/dist \ No newline at end of file diff --git a/.internal/.ssh/.gitignore b/.internal/.ssh/.gitignore new file mode 100644 index 000000000..eafb9102c --- /dev/null +++ b/.internal/.ssh/.gitignore @@ -0,0 +1 @@ +id_rsa* diff --git a/.internal/IOTstack.postman_collection.json b/.internal/IOTstack.postman_collection.json new file mode 100644 index 000000000..376da865c --- /dev/null +++ b/.internal/IOTstack.postman_collection.json @@ -0,0 +1,840 @@ +{ + "info": { + "_postman_id": "9ccf19f7-fa69-4acb-b1ae-898fe7392ac1", + "name": "IOTstack", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "Build", + "item": [ + { + "name": "Build", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n\t\"buildOptions\": {\n\t\t\"selectedServices\": [],\n\t\t\"serviceOptions\": {}\n\t}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:32128/build/save", + "host": [ + "localhost" + ], + "port": "32128", + "path": [ + "build", + "save" + ] + }, + "description": "Create build" + }, + "response": [] + }, + { + "name": "Get Previous Build List", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n\t\"buildOptions\": {\n\t\t\"selectedServices\": [],\n\t\t\"serviceOptions\": {}\n\t}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:32128/build/list", + "host": [ + "localhost" + ], + "port": "32128", + "path": [ + "build", + "list" + ] + }, + "description": "Get a list of previous builds stored on file" + }, + "response": [] + }, + { + "name": "Get Previous Build File Details", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n\t\"buildOptions\": {\n\t\t\"selectedServices\": [],\n\t\t\"serviceOptions\": {}\n\t}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:32128/build/get/{{buildTime}}", + "host": [ + "localhost" + ], + "port": "32128", + "path": [ + "build", + "get", + "{{buildTime}}" + ] + }, + "description": "Get a file list of specific build" + }, + "response": [] + }, + { + "name": "Get Previous Build File", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n\t\"buildOptions\": {\n\t\t\"selectedServices\": [],\n\t\t\"serviceOptions\": {}\n\t}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:32128/build/get/{{buildTime}}/{{type}}", + "host": [ + "localhost" + ], + "port": "32128", + "path": [ + "build", + "get", + "{{buildTime}}", + "{{type}}" + ] + }, + "description": "Get a file list of specific build file. Type can be one of: 'yaml', 'json', 'zip' or 'sh'." + }, + "response": [] + }, + { + "name": "Dry Run (Check Issues)", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n\t\"buildOptions\": {\n\t\t\"selectedServices\": [],\n\t\t\"configurations\": {}\n\t}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:32128/build/dryrun", + "host": [ + "localhost" + ], + "port": "32128", + "path": [ + "build", + "dryrun" + ] + }, + "description": "This checks for issues" + }, + "response": [] + }, + { + "name": "Delete a build", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:32128/build/delete/{{build}}", + "host": [ + "localhost" + ], + "port": "32128", + "path": [ + "build", + "delete", + "{{build}}" + ] + }, + "description": "Deletes a build, and all associated files." + }, + "response": [] + } + ] + }, + { + "name": "Configs", + "item": [ + { + "name": "Get Service Config Options", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:32128/config/{{serviceName}}/options", + "host": [ + "localhost" + ], + "port": "32128", + "path": [ + "config", + "{{serviceName}}", + "options" + ] + } + }, + "response": [] + }, + { + "name": "Get All Services Config Options", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:32128/config/options", + "host": [ + "localhost" + ], + "port": "32128", + "path": [ + "config", + "options" + ] + } + }, + "response": [] + }, + { + "name": "Get Service Help URIs", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:32128/config/{{serviceName}}/help", + "host": [ + "localhost" + ], + "port": "32128", + "path": [ + "config", + "{{serviceName}}", + "help" + ] + } + }, + "response": [] + }, + { + "name": "Get Service Scripts", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:32128/config/{{serviceName}}/scripts", + "host": [ + "localhost" + ], + "port": "32128", + "path": [ + "config", + "{{serviceName}}", + "scripts" + ] + }, + "description": "Get a list of scripts that could e useful for checking your system before building, or for debugging." + }, + "response": [] + }, + { + "name": "Get Specific Service Script", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:32128/config/{{serviceName}}/scripts/{{scriptName}}", + "host": [ + "localhost" + ], + "port": "32128", + "path": [ + "config", + "{{serviceName}}", + "scripts", + "{{scriptName}}" + ] + }, + "description": "Get a specific script, if it exists, that can be piped into bash." + }, + "response": [] + }, + { + "name": "Get Service Metadata", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:32128/config/{{serviceName}}/meta", + "host": [ + "localhost" + ], + "port": "32128", + "path": [ + "config", + "{{serviceName}}", + "meta" + ] + }, + "description": "Get metadata about a service, such as its tags." + }, + "response": [] + }, + { + "name": "Get All Services Metadata", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:32128/config/meta", + "host": [ + "localhost" + ], + "port": "32128", + "path": [ + "config", + "meta" + ] + }, + "description": "Get metadata about all services, such as their tags." + }, + "response": [] + } + ] + }, + { + "name": "Templates", + "item": [ + { + "name": "Services", + "item": [ + { + "name": "Get all services in YAML format", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:32128/templates/services/yaml", + "host": [ + "localhost" + ], + "port": "32128", + "path": [ + "templates", + "services", + "yaml" + ] + } + }, + "response": [] + }, + { + "name": "Get specific service in YAML format", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:32128/templates/services/yaml/{{serviceName}}", + "host": [ + "localhost" + ], + "port": "32128", + "path": [ + "templates", + "services", + "yaml", + "{{serviceName}}" + ] + } + }, + "response": [] + }, + { + "name": "Get specific service in JSON format", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:32128/templates/services/json/{{serviceName}}", + "host": [ + "localhost" + ], + "port": "32128", + "path": [ + "templates", + "services", + "json", + "{{serviceName}}" + ] + } + }, + "response": [] + }, + { + "name": "Get all services in JSON format", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:32128/templates/services/json", + "host": [ + "localhost" + ], + "port": "32128", + "path": [ + "templates", + "services", + "json" + ] + } + }, + "response": [] + }, + { + "name": "List all services", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:32128/templates/services/list", + "host": [ + "localhost" + ], + "port": "32128", + "path": [ + "templates", + "services", + "list" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Networks", + "item": [ + { + "name": "Get all networks in JSON format", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:32128/templates/networks/json", + "host": [ + "localhost" + ], + "port": "32128", + "path": [ + "templates", + "networks", + "json" + ] + } + }, + "response": [] + }, + { + "name": "Get specific network in YAML format", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:32128/templates/networks/yaml/{{networkName}}", + "host": [ + "localhost" + ], + "port": "32128", + "path": [ + "templates", + "networks", + "yaml", + "{{networkName}}" + ] + } + }, + "response": [] + }, + { + "name": "Get all networks in YAML format", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:32128/templates/networks/yaml", + "host": [ + "localhost" + ], + "port": "32128", + "path": [ + "templates", + "networks", + "yaml" + ] + } + }, + "response": [] + }, + { + "name": "List all networks", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:32128/templates/networks/list", + "host": [ + "localhost" + ], + "port": "32128", + "path": [ + "templates", + "networks", + "list" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Scripts", + "item": [ + { + "name": "Get script by name", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"options\": {}\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:32128/templates/scripts/{scriptName}", + "host": [ + "localhost" + ], + "port": "32128", + "path": [ + "templates", + "scripts", + "{scriptName}" + ] + } + }, + "response": [] + }, + { + "name": "Download script by name", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"options\": {}\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:32128/templates/scripts/download/{scriptName}", + "host": [ + "localhost" + ], + "port": "32128", + "path": [ + "templates", + "scripts", + "download", + "{scriptName}" + ] + } + }, + "response": [] + } + ] + } + ] + }, + { + "name": "Get API State and Health", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:32128/health", + "host": [ + "localhost" + ], + "port": "32128", + "path": [ + "health" + ] + }, + "description": "Get metadata about a service, such as its tags." + }, + "response": [] + } + ] +} \ No newline at end of file diff --git a/.internal/api.Dockerfile b/.internal/api.Dockerfile new file mode 100644 index 000000000..96acb6626 --- /dev/null +++ b/.internal/api.Dockerfile @@ -0,0 +1,10 @@ +FROM node:14 + +WORKDIR /usr/iotstack_api + +# node_modules is ignored with this copy, as specified in .dockerignore +COPY ./api ./ +RUN npm install + +EXPOSE 32128 +CMD [ "/bin/bash", "start.sh" ] diff --git a/.internal/api/index.js b/.internal/api/index.js new file mode 100644 index 000000000..f2a01fbfa --- /dev/null +++ b/.internal/api/index.js @@ -0,0 +1 @@ +const server = require('./src/index'); diff --git a/.internal/api/package-lock.json b/.internal/api/package-lock.json new file mode 100644 index 000000000..eb33e1454 --- /dev/null +++ b/.internal/api/package-lock.json @@ -0,0 +1,1542 @@ +{ + "name": "iotstack_api", + "version": "0.0.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@sindresorhus/is": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", + "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==" + }, + "@szmarczak/http-timer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", + "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", + "requires": { + "defer-to-connect": "^1.0.1" + } + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "ansi-align": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz", + "integrity": "sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw==", + "requires": { + "string-width": "^3.0.0" + }, + "dependencies": { + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + } + } + }, + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "archiver": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.1.0.tgz", + "integrity": "sha512-iKuQUP1nuKzBC2PFlGet5twENzCfyODmvkxwDV0cEFXavwcLrIW5ssTuHi9dyTPvpWr6Faweo2eQaQiLIwyXTA==", + "requires": { + "archiver-utils": "^2.1.0", + "async": "^3.2.0", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.0.0", + "tar-stream": "^2.1.4", + "zip-stream": "^4.0.4" + } + }, + "archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "requires": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + } + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "async": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", + "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==" + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" + }, + "bl": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz", + "integrity": "sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==", + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + }, + "dependencies": { + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + } + } + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + } + }, + "boxen": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", + "integrity": "sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ==", + "requires": { + "ansi-align": "^3.0.0", + "camelcase": "^5.3.1", + "chalk": "^3.0.0", + "cli-boxes": "^2.2.0", + "string-width": "^4.1.0", + "term-size": "^2.1.0", + "type-fest": "^0.8.1", + "widest-line": "^3.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==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "requires": { + "fill-range": "^7.0.1" + } + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" + }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, + "cacheable-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", + "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^3.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "responselike": "^1.0.2" + }, + "dependencies": { + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "requires": { + "pump": "^3.0.0" + } + }, + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" + } + } + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "chokidar": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.0.tgz", + "integrity": "sha512-JgQM9JS92ZbFR4P90EvmzNpSGhpPBGBSj10PILeDyYFwp4h2/D9OM03wsJ4zW1fEp4ka2DGrnUeD7FuvQ2aZ2Q==", + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.3.1", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.5.0" + } + }, + "ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==" + }, + "cli-boxes": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", + "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==" + }, + "clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "requires": { + "mimic-response": "^1.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "compress-commons": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.0.2.tgz", + "integrity": "sha512-qhd32a9xgzmpfoga1VQEiLEwdKZ6Plnpx5UCgIsf89FSolyJ7WnifY4Gtjgv5WR6hWAyRaHxC5MiEhU/38U70A==", + "requires": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "configstore": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "requires": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" + } + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "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=" + }, + "crc-32": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.0.tgz", + "integrity": "sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA==", + "requires": { + "exit-on-epipe": "~1.0.1", + "printj": "~1.1.0" + } + }, + "crc32-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.1.tgz", + "integrity": "sha512-FN5V+weeO/8JaXsamelVYO1PHyeCsuL3HcG4cqsj0ceARcocxalaShCsohZMSAF+db7UYFwBy1rARK/0oFItUw==", + "requires": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + } + }, + "crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "requires": { + "mimic-response": "^1.0.0" + } + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + }, + "defer-to-connect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", + "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==" + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "requires": { + "is-obj": "^2.0.0" + } + }, + "duplexer3": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", + "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=" + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, + "escape-goat": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", + "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "exit-on-epipe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz", + "integrity": "sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==" + }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fsevents": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.1.tgz", + "integrity": "sha512-YR47Eg4hChJGAB1O3yEAOkGO+rlzutoICGqGo9EZ4lKWokzZRSyIW1QmTzqjtw8MJdj9srP869CuWw/hyzSiBw==", + "optional": true + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "requires": { + "pump": "^3.0.0" + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "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" + } + }, + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "requires": { + "is-glob": "^4.0.1" + } + }, + "global-dirs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-2.1.0.tgz", + "integrity": "sha512-MG6kdOUh/xBnyo9cJFeIKkLEc1AyFq42QTU4XiX51i2NEdxLxLWXIjEjmqKeSuKR7pAZjTqUVoT2b2huxVLgYQ==", + "requires": { + "ini": "1.3.7" + } + }, + "got": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", + "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", + "requires": { + "@sindresorhus/is": "^0.14.0", + "@szmarczak/http-timer": "^1.1.2", + "cacheable-request": "^6.0.0", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^4.1.0", + "lowercase-keys": "^1.0.1", + "mimic-response": "^1.0.1", + "p-cancelable": "^1.0.0", + "to-readable-stream": "^1.0.0", + "url-parse-lax": "^3.0.0" + } + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "has-yarn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", + "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==" + }, + "http-cache-semantics": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=" + }, + "import-lazy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=" + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "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=" + }, + "ini": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz", + "integrity": "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==" + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "requires": { + "ci-info": "^2.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-installed-globally": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz", + "integrity": "sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==", + "requires": { + "global-dirs": "^2.0.1", + "is-path-inside": "^3.0.1" + } + }, + "is-npm": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz", + "integrity": "sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==" + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==" + }, + "is-path-inside": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.2.tgz", + "integrity": "sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg==" + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "is-yarn-global": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", + "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "js-yaml": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", + "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=" + }, + "keyv": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", + "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", + "requires": { + "json-buffer": "3.0.0" + } + }, + "latest-version": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", + "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==", + "requires": { + "package-json": "^6.3.0" + } + }, + "lazystream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", + "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", + "requires": { + "readable-stream": "^2.0.5" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + } + } + }, + "lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" + }, + "lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=" + }, + "lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=" + }, + "lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==" + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "requires": { + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" + }, + "mime-types": { + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "requires": { + "mime-db": "1.44.0" + } + }, + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + }, + "nodemon": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.7.tgz", + "integrity": "sha512-XHzK69Awgnec9UzHr1kc8EomQh4sjTQ8oRf8TsGrSmHDx9/UmiGG9E/mM3BuTfNeFwdNBvrqQq/RHL0xIeyFOA==", + "requires": { + "chokidar": "^3.2.2", + "debug": "^3.2.6", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.0.4", + "pstree.remy": "^1.1.7", + "semver": "^5.7.1", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.3", + "update-notifier": "^4.1.0" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", + "requires": { + "abbrev": "1" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + }, + "normalize-url": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", + "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "p-cancelable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", + "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==" + }, + "package-json": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", + "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==", + "requires": { + "got": "^9.6.0", + "registry-auth-token": "^4.0.0", + "registry-url": "^5.0.0", + "semver": "^6.2.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "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=" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==" + }, + "prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=" + }, + "printj": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz", + "integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==" + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "proxy-addr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", + "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.1" + } + }, + "pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pupa": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz", + "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==", + "requires": { + "escape-goat": "^2.0.0" + } + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readdir-glob": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.1.tgz", + "integrity": "sha512-91/k1EzZwDx6HbERR+zucygRFfiPl2zkIYZtv3Jjr6Mn7SkKcVct8aVO+sSRiGMc6fLf72du3d92/uY63YPdEA==", + "requires": { + "minimatch": "^3.0.4" + } + }, + "readdirp": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", + "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", + "requires": { + "picomatch": "^2.2.1" + } + }, + "registry-auth-token": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.1.tgz", + "integrity": "sha512-6gkSb4U6aWJB4SF2ZvLb76yCBjcvufXBqvvEx1HbmKPkutswjW1xNVRY0+daljIYRbogN7O0etYSlbiaEQyMyw==", + "requires": { + "rc": "^1.2.8" + } + }, + "registry-url": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", + "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", + "requires": { + "rc": "^1.2.8" + } + }, + "responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", + "requires": { + "lowercase-keys": "^1.0.0" + } + }, + "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": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + }, + "semver-diff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", + "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==", + "requires": { + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + }, + "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" + } + }, + "tar-stream": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.4.tgz", + "integrity": "sha512-o3pS2zlG4gxr67GmFYBLlq+dM8gyRGUOvsrHclSkvtVtQbjV0s/+ZE8OpICbaj8clrX3tjeHngYGP7rweaBnuw==", + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + } + }, + "term-size": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz", + "integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==" + }, + "to-readable-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", + "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==" + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "requires": { + "is-number": "^7.0.0" + } + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, + "touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "requires": { + "nopt": "~1.0.10" + } + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "requires": { + "is-typedarray": "^1.0.0" + } + }, + "undefsafe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.3.tgz", + "integrity": "sha512-nrXZwwXrD/T/JXeygJqdCO6NZZ1L66HrxM/Z7mIq2oPanoN0F1nLx3lwJMu6AwJY69hdixaFQOuoYsMjE5/C2A==", + "requires": { + "debug": "^2.2.0" + } + }, + "unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "requires": { + "crypto-random-string": "^2.0.0" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "update-notifier": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.3.tgz", + "integrity": "sha512-Yld6Z0RyCYGB6ckIjffGOSOmHXj1gMeE7aROz4MG+XMkmixBX4jUngrGXNYz7wPKBmtoD4MnBa2Anu7RSKht/A==", + "requires": { + "boxen": "^4.2.0", + "chalk": "^3.0.0", + "configstore": "^5.0.1", + "has-yarn": "^2.1.0", + "import-lazy": "^2.1.0", + "is-ci": "^2.0.0", + "is-installed-globally": "^0.3.1", + "is-npm": "^4.0.0", + "is-yarn-global": "^0.3.0", + "latest-version": "^5.0.0", + "pupa": "^2.0.1", + "semver-diff": "^3.1.1", + "xdg-basedir": "^4.0.0" + } + }, + "url-parse-lax": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", + "requires": { + "prepend-http": "^2.0.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "widest-line": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "requires": { + "string-width": "^4.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==" + }, + "zip-stream": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.0.4.tgz", + "integrity": "sha512-a65wQ3h5gcQ/nQGWV1mSZCEzCML6EK/vyVPcrPNynySP1j3VBbQKh3nhC8CbORb+jfl2vXvh56Ul5odP1bAHqw==", + "requires": { + "archiver-utils": "^2.1.0", + "compress-commons": "^4.0.2", + "readable-stream": "^3.6.0" + } + } + } +} diff --git a/.internal/api/package.json b/.internal/api/package.json new file mode 100644 index 000000000..d9184c827 --- /dev/null +++ b/.internal/api/package.json @@ -0,0 +1,20 @@ +{ + "name": "iotstack_api", + "version": "0.0.1", + "description": "IOTstack API", + "repository": "https://github.com/SensorsIot/IOTstack", + "main": "index.js", + "scripts": { + "start": "NODE_ENV=production node index.js", + "dev": "NODE_ENV=development nodemon --ignore './builds/' index.js" + }, + "author": "", + "license": "ISC", + "dependencies": { + "archiver": "^5.1.0", + "body-parser": "^1.19.0", + "express": "^4.17.1", + "js-yaml": "^3.14.0", + "nodemon": "^2.0.7" + } +} diff --git a/.internal/api/src/.tmp/.gitignore b/.internal/api/src/.tmp/.gitignore new file mode 100644 index 000000000..d6b7ef32c --- /dev/null +++ b/.internal/api/src/.tmp/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/.internal/api/src/controllers/build.js b/.internal/api/src/controllers/build.js new file mode 100644 index 000000000..fe809db55 --- /dev/null +++ b/.internal/api/src/controllers/build.js @@ -0,0 +1,714 @@ +const BuildController = ({ server, settings, version, logger }) => { + const BuildsService = require('../services/builds'); + const ZipService = require('../services/zip'); + const TemplatesController = require('./templates'); + const { getDirectoryList, getFileList, emptyDirectory } = require('../utils/fsUtils'); + const { formatDate } = require('../utils/date'); + const { getUniqueNetworkListFromServices } = require('../utils/networkUtils'); + const buildInstallerGenerator = require('../../templates/scripts/build_installer'); + const retr = {}; + + const buildsService = BuildsService({ server, settings, version, logger }); + const zipService = ZipService({ server, settings, version, logger }); + const templatesController = TemplatesController({ server, settings, version, logger }); + const path = require('path'); + const fs = require('fs'); + + retr.init = () => { + templatesController.init(); + buildsService.init(); + zipService.init(); + logger.debug('BuildController:init()'); + }; + + retr.buildStack = async ({ host, buildOptions, composeFileVersion }) => { + return new Promise(async (resolve, reject) => { + try { + outputStack = { + version: composeFileVersion || '3.6', + services: {}, + networks: {} + }; + + const currentDate = formatDate(); + const toZip = []; + const prebuildScripts = []; + const postbuildScripts = []; + + const failedStack = { + services: [], + networks: [] + }; + + // Get service templates for processing and output + const serviceTemplatePromises = []; + + buildOptions.selectedServices.forEach((selectedService) => { + serviceTemplatePromises.push(templatesController.getServiceTemplateAsJson(selectedService)); + }); + + await Promise.allSettled(serviceTemplatePromises).then((serviceTemplateResults) => { + serviceTemplateResults.forEach((servicePromiseResult) => { + if (servicePromiseResult.status === 'fulfilled' && servicePromiseResult.value) { + Object.keys(servicePromiseResult.value).forEach((serviceName) => { + outputStack.services[serviceName] = servicePromiseResult.value[serviceName]; + }); + } else { + failedStack.services.push(servicePromiseResult); + } + }); + return outputStack; + }); + + const podNetworks = getUniqueNetworkListFromServices({ services: outputStack.services, logger }); + + // Get network templates for processing and output + const networkTemplatePromises = []; + + podNetworks.forEach((networkName) => { + networkTemplatePromises.push(templatesController.getNetworkTemplateAsJson(networkName)); + }); + + await Promise.allSettled(networkTemplatePromises).then((networkTemplateResults) => { + networkTemplateResults.forEach((networkPromiseResult) => { + if (networkPromiseResult.status === 'fulfilled' && networkPromiseResult.value) { + const networkName = Object.keys(networkPromiseResult.value)[0]; + outputStack.networks[networkName] = networkPromiseResult.value[networkName]; + } else { + failedStack.networks.push(networkPromiseResult); + } + }); + return outputStack; + }); + + if (failedStack.services.length > 0 || failedStack.networks.length > 0) { + logger.warn('Failed to compile some templates: ', failedStack); + } + + // All templates gathered and ready for processing by each service. + const templatesBuildLogic = []; + const { + localBuildsDirectory, + buildInstallerFilePostfix, + localTemplatesPath, + localNetworksRelativePath, + localServicesRelativePath, + buildLogicFile, + buildDockerFilePostfix, + buildOptionsFilePostfix, + localTmpPath + } = settings.paths; + + emptyDirectory(localTmpPath, ['.gitignore']); + + const servicesBuildPath = path.join(localTemplatesPath, localServicesRelativePath); + const networksBuildPath = path.join(localTemplatesPath, localNetworksRelativePath); + + // Instantiate each service's build logic + let failedServices = []; + Object.keys(outputStack.services).forEach((serviceName) => { + const serviceBuildScript = path.join(servicesBuildPath, serviceName, buildLogicFile); + if (!fs.existsSync(path.join(servicesBuildPath, serviceName))) { + return; // Skip. This means the service's directory doesn't exist and it is likely an add-on (NextCloud DB for example) + } + + if (fs.existsSync(serviceBuildScript)) { + templatesBuildLogic.push(require(serviceBuildScript)({ settings, version, logger })); + } else { + logger.log(`BuildController::buildStack: No service build file for '${serviceName}'. Looking in: '${serviceBuildScript}'`); + failedServices.push({ serviceName: `No build file. Check logs for more details. Looking in: '${serviceBuildScript}'` }); + } + }); + + // Instantiate each network's build logic + Object.keys(outputStack.networks).forEach((networkName) => { + const networkBuildScript = path.join(networksBuildPath, networkName, buildLogicFile); + if (fs.existsSync(networkBuildScript)) { + templatesBuildLogic.push(require(networkBuildScript)({ settings, version, logger })); + } else { + logger.log(`BuildController::buildStack: No network build file for '${networkName}'. Looking in: '${networkBuildScript}'`); + } + }); + + if (failedServices.length > 0) { + return reject({ + component: 'BuildController::buildStack', + message: `One or more services failed to build: '${JSON.stringify(failedServices)}'` + }); + } + + const issueList = { + services: [], + networks: [], + other: [] + }; + + // Compile service options to JSON output + logger.debug('BuildController::buildStack: Compile started'); + for (let i = 0; i < templatesBuildLogic.length; i++) { + await templatesBuildLogic[i].compile({ + outputTemplateJson: outputStack, + buildOptions, + }); + } + + // Use default values for services if not specified + for (let i = 0; i < templatesBuildLogic.length; i++) { + if (typeof(templatesBuildLogic[i].assume) === 'function') { + await templatesBuildLogic[i].assume({ + outputTemplateJson: outputStack, + buildOptions, + }); + } + } + logger.debug('BuildController::buildStack: Compile completed'); + + logger.debug('BuildController::buildStack: Build started'); + await templatesBuildLogic.reduce((prom, buildLogic) => { + return prom.then(async () => { + if (typeof buildLogic.build === 'function') { + await buildLogic.build({ + outputTemplateJson: outputStack, + buildOptions, + tmpPath: localTmpPath, + fileTimePrefix: currentDate, + zipList: toZip, + prebuildScripts, + postbuildScripts + }); + // .then((res) => { + // logger.log('Result: ', res); + // }); + } + + if (typeof buildLogic.issues === 'function') { + return buildLogic.issues({ + outputTemplateJson: outputStack, + buildOptions, + tmpPath: localTmpPath + }).then((issueResults) => { + if (Array.isArray(issueResults) && issueResults.length > 0) { + issueResults.forEach((issue) => { + if (issue.type === 'service') { + issueList.services.push(issue); + } else if (issue.type === 'network') { + issueList.networks.push(issue); + } else { + issueList.other.push(issue); + } + }); + } + }); + } + + return Promise.resolve({}); + }); + }, Promise.resolve()).then(() => { + logger.debug('BuildController::buildStack: Build Completed'); + }).catch((err) => { + logger.error('BuildController::buildStack: Build Error: ', err); + return reject({ + component: 'BuildController::buildStack', + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + }); + + if (Object.keys(outputStack?.networks ?? {}).length === 0) { + delete outputStack.networks; + } + + const { yamlFilename, yamlOutputFilePath } = await buildsService.saveBuildYaml({ buildJson: outputStack, fileTimePrefix: currentDate }); + const { jsonFilename, jsonOutputFilePath } = await buildsService.saveBuildOptions({ buildOptionsJson: buildOptions, fileTimePrefix: currentDate }); + const dockerComposeBaseFilename = buildDockerFilePostfix.split('_')[1]; + const buildInstallerFileBaseFilename = buildInstallerFilePostfix.split('_')[1]; + const buildOptionsBaseFilename = buildOptionsFilePostfix.split('_')[1]; + + buildInstallerFilePostfix + const buildInstallerContents = await buildInstallerGenerator({ + options: { + build: currentDate, + prebuildScripts, + postbuildScripts, + selectedServices: buildOptions.selectedServices + }, + logger, + version + }); + + const installerScriptFilename = `${currentDate}${buildInstallerFilePostfix}`; + const installerOutputFilePath = path.join(localBuildsDirectory, installerScriptFilename); + + fs.writeFileSync(installerOutputFilePath, buildInstallerContents.data); + + toZip.push({ + fullPath: installerOutputFilePath, + zipName: buildInstallerFileBaseFilename + }); + + toZip.push({ + fullPath: yamlOutputFilePath, + zipName: dockerComposeBaseFilename + }); + + toZip.push({ + fullPath: jsonOutputFilePath, + zipName: buildOptionsBaseFilename + }); + + const zipDirectories = []; + toZip.forEach((zipEntries) => { + if (Array.isArray(zipEntries.directories) && zipEntries.directories.length > 0) { + zipEntries.directories.forEach((directory) => { + zipDirectories.push(directory); + }); + } + }); + + const zipResults = await zipService.zipFiles({ fileList: toZip, fileTimePrefix: currentDate, archiveDirectoryList: zipDirectories }); + + return resolve({ + issueList, + build: currentDate, + host, + files: { + yamlFilename, + jsonFilename, + zipFilename: zipResults.zipFilename + } + }); + + } catch (err) { + logger.log(err); + logger.trace(); + return reject({ + component: 'BuildController::buildStack', + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.checkIssues = ({ buildOptions }) => { + return new Promise(async (resolve, reject) => { + try { + outputStack = { + services: {}, + networks: {} + }; + + const failedStack = { + services: [], + networks: [] + }; + + // Get service templates for processing and output + const serviceTemplatePromises = []; + + buildOptions.selectedServices.forEach((selectedService) => { + serviceTemplatePromises.push(templatesController.getServiceTemplateAsJson(selectedService)); + }); + + await Promise.allSettled(serviceTemplatePromises).then((serviceTemplateResults) => { + serviceTemplateResults.forEach((servicePromiseResult) => { + if (servicePromiseResult.status === 'fulfilled' && servicePromiseResult.value) { + const serviceName = Object.keys(servicePromiseResult.value)[0]; + outputStack.services[serviceName] = servicePromiseResult.value[serviceName]; + } else { + failedStack.services.push(servicePromiseResult); + } + }); + return outputStack; + }); + + const podNetworks = getUniqueNetworkListFromServices({ services: outputStack.services, logger }); + + // Get network templates for processing and output + const networkTemplatePromises = []; + + podNetworks.forEach((networkName) => { + networkTemplatePromises.push(templatesController.getNetworkTemplateAsJson(networkName)); + }); + + await Promise.allSettled(networkTemplatePromises).then((networkTemplateResults) => { + networkTemplateResults.forEach((networkPromiseResult) => { + if (networkPromiseResult.status === 'fulfilled' && networkPromiseResult.value) { + const networkName = Object.keys(networkPromiseResult.value)[0]; + outputStack.networks[networkName] = networkPromiseResult.value[networkName]; + } else { + failedStack.networks.push(networkPromiseResult); + } + }); + return outputStack; + }); + + if (failedStack.services.length > 0 || failedStack.networks.length > 0) { + logger.warn('Failed to compile some templates: ', failedStack); + } + + // All templates gathered and ready for processing by each service. + const templatesBuildLogic = []; + const { + localTemplatesPath, + localNetworksRelativePath, + localServicesRelativePath, + buildLogicFile + } = settings.paths; + const servicesBuildPath = path.join(localTemplatesPath, localServicesRelativePath); + const networksBuildPath = path.join(localTemplatesPath, localNetworksRelativePath); + + // Instantiate each service's build logic + let failedServices = []; + Object.keys(outputStack.services).forEach((serviceName) => { + const serviceBuildScript = path.join(servicesBuildPath, serviceName, buildLogicFile); + if (!fs.existsSync(path.join(servicesBuildPath, serviceName))) { + return; // Skip. This means the service's directory doesn't exist and it is likely an add-on (NextCloud DB for example) + } + + if (fs.existsSync(serviceBuildScript)) { + templatesBuildLogic.push(require(serviceBuildScript)({ settings, version, logger })); + } else { + logger.log(`BuildController::checkIssues: No service build file for '${serviceName}'. Looking in: '${serviceBuildScript}'`); + failedServices.push({ serviceName: `No build file. Check logs for more details. Looking in: '${serviceBuildScript}'` }); + } + }); + + if (failedServices.length > 0) { + return reject({ + component: 'BuildController::checkIssues', + message: `One or more services failed to build: '${JSON.stringify(failedServices)}'`, + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + + // Instantiate each network's build logic + Object.keys(outputStack.networks).forEach((networkName) => { + const networkBuildScript = path.join(networksBuildPath, networkName, buildLogicFile); + if (fs.existsSync(networkBuildScript)) { + templatesBuildLogic.push(require(networkBuildScript)({ settings, version, logger })); + } else { + logger.log(`BuildController::checkIssues: No network build file for '${networkName}'. Looking in: '${networkBuildScript}'`); + } + }); + + const issueList = { + services: [], + networks: [], + other: [] + }; + + // Compile service options to JSON output + logger.debug('BuildController::checkIssues: Compile started'); + for (let i = 0; i < templatesBuildLogic.length; i++) { + await templatesBuildLogic[i].compile({ + outputTemplateJson: outputStack, + buildOptions, + }); + } + + await Promise.allSettled(networkTemplatePromises).then((networkTemplateResults) => { + networkTemplateResults.forEach((networkPromiseResult) => { + if (networkPromiseResult.status === 'fulfilled' && networkPromiseResult.value) { + const networkName = Object.keys(networkPromiseResult.value)[0]; + outputStack.networks[networkName] = networkPromiseResult.value[networkName]; + } else { + failedStack.networks.push(networkPromiseResult); + } + }); + return outputStack; + }); + logger.debug('BuildController::checkIssues: Compile complete'); + + logger.debug('BuildController::checkIssues: Issue check started'); + return templatesBuildLogic.reduce((prom, buildLogic) => { + return prom.then(() => { + if (typeof buildLogic.issues === 'function') { + return buildLogic.issues({ + outputTemplateJson: outputStack, + buildOptions + }).then((issueResults) => { + if (Array.isArray(issueResults) && issueResults.length > 0) { + issueResults.forEach((issue) => { + if (issue.type === 'service') { + issueList.services.push(issue); + } else if (issue.type === 'network') { + issueList.networks.push(issue); + } else { + issueList.other.push(issue); + } + }); + } + }); + } + + return Promise.resolve({ issueList }); + }); + }, Promise.resolve()).then(() => { + logger.debug('BuildController::checkIssues: Issue check completed'); + return resolve({ issueList }); + }).catch((err) => { + logger.error('BuildController::checkIssues: Issue check error: ', err); + return reject({ issueList }); + }); + } catch (err) { + logger.log(err); + logger.trace(); + return reject({ + component: 'BuildController::checkIssues', + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.deletePreviousBuild = ({ host, buildTime }) => { + return new Promise(async (resolve, reject) => { + try { + const { + localBuildsDirectory + } = settings.paths; + + const useBuildTime = buildTime.replace(/\//g, ''); + + if (useBuildTime !== buildTime) { + return reject({ + component: 'BuildController::deletePreviousBuild', + message: `Build '${buildTime}' has an invalid name` + }); + } + + const buildFiles = getFileList(localBuildsDirectory); + let filesDeleted = []; + + buildFiles.forEach((fileName) => { + const deconstructedFilename = fileName.split('_'); + if (deconstructedFilename.length === 2) { + const checkBuildTime = deconstructedFilename[0]; + + if (checkBuildTime === useBuildTime) { + const fullPathZip = path.join(localBuildsDirectory, fileName); + fs.unlinkSync(fullPathZip); + filesDeleted.push(fileName); + } + } + }); + + if (filesDeleted.length < 1) { + return reject({ + component: 'BuildController::deletePreviousBuild', + message: `Build '${buildTime}' not found` + }); + } + return resolve({ filesDeleted, buildTime, host }); + } catch (err) { + logger.log(err); + logger.trace(); + return reject({ + component: 'BuildController::deletePreviousBuild', + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.getPreviousBuildsList = ({ host, buildTime, index, limit }) => { + return new Promise(async (resolve, reject) => { + try { + const { + localBuildsDirectory, + buildDockerFilePostfix, + buildOptionsFilePostfix, + buildZipFilePostfix + } = settings.paths; + + const buildFiles = getFileList(localBuildsDirectory); + + const buildsList = {}; + let singleBuild = {}; + + buildFiles.forEach((fileName) => { + const deconstructedFilename = fileName.split('_'); + if (deconstructedFilename.length === 2) { + const checkBuildTime = deconstructedFilename[0]; + if (typeof buildsList[checkBuildTime] === 'undefined') { + buildsList[checkBuildTime] = {}; + } + + if (fileName.endsWith(buildDockerFilePostfix)) { + if (!buildTime) { + buildsList[checkBuildTime] = { + ...buildsList[checkBuildTime], + yaml: fileName + } + } else { + if (checkBuildTime === buildTime) { + singleBuild = { + ...singleBuild, + yaml: fileName + } + } + } + } + + if (fileName.endsWith(buildOptionsFilePostfix)) { + if (!buildTime) { + buildsList[checkBuildTime] = { + ...buildsList[checkBuildTime], + json: fileName + } + } else { + if (checkBuildTime === buildTime) { + singleBuild = { + ...singleBuild, + json: fileName + } + } + } + } + + if (fileName.endsWith(buildZipFilePostfix)) { + if (!buildTime) { + buildsList[checkBuildTime] = { + ...buildsList[checkBuildTime], + zip: fileName + } + } else { + if (checkBuildTime === buildTime) { + singleBuild = { + ...singleBuild, + zip: fileName + } + } + } + } + } + }); + + if (!buildTime) { + const useLimit = limit ?? -1; + const useIndex = index ?? 0; + + if (useLimit < 1) { + return resolve({ buildsList, host }); + } + + const limitedBuildList = {}; + let currentCount = 0; + Object.keys(buildsList).forEach((build, buildIndex) => { + if (buildIndex >= useIndex) { + currentCount++; + if (currentCount <= useLimit) { + limitedBuildList[build] = buildsList[build]; + } + } + }); + + return resolve({ buildsList: limitedBuildList, host }); + } else { + return resolve({ buildFiles: singleBuild, host }); + } + } catch (err) { + logger.log(err); + logger.trace(); + return reject({ + component: 'BuildController::getPreviousBuildsList', + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.downloadPreviousBuildsList = ({ host, buildTime, type }) => { + return new Promise(async (resolve, reject) => { + try { + if (!buildTime) { + return reject({ + component: 'BuildController::downloadPreviousBuildsList', + message: 'No build time specified' + }); + } + + if (!type) { + return reject({ + component: 'BuildController::downloadPreviousBuildsList', + message: 'No download type specified' + }); + } + + const { + localBuildsDirectory, + buildDockerFilePostfix, + buildZipFilePostfix, + buildOptionsFilePostfix, + buildInstallerFilePostfix + } = settings.paths; + + const buildFiles = getFileList(localBuildsDirectory); + + let singleBuild = { + otherFiles: [] + }; + + buildFiles.forEach((fileName) => { + if (fileName.startsWith(buildTime)) { + if (fileName.endsWith(buildDockerFilePostfix)) { + singleBuild['yaml'] = fileName; + } else if (fileName.endsWith(buildZipFilePostfix)) { + singleBuild['zip'] = fileName; + } else if (fileName.endsWith(buildOptionsFilePostfix)) { + singleBuild['json'] = fileName; + } else if (fileName.endsWith(buildInstallerFilePostfix)) { + singleBuild['installer'] = fileName; + } else { + singleBuild.otherFiles.push(fileName); + } + } + }); + + if (type === 'yaml' || type === 'yml') { + const fullPath = path.join(localBuildsDirectory, singleBuild['yaml']); + const filename = buildDockerFilePostfix.substring(1); // Removes the prefixed underscope _ on filename + return resolve({ fullPath, filename, path: localBuildsDirectory }); + } + + if (type === 'json') { + const fullPath = path.join(localBuildsDirectory, singleBuild['json']); + const filename = buildOptionsFilePostfix.substring(1); // Removes the prefixed underscope _ on filename + return resolve({ fullPath, filename, path: localBuildsDirectory }); + } + + if (type === 'zip') { + const fullPath = path.join(localBuildsDirectory, singleBuild['zip']); + const filename = buildZipFilePostfix.substring(1); // Removes the prefixed underscope _ on filename + return resolve({ fullPath, filename, path: localBuildsDirectory }); + } + + if (type === 'sh' || type === 'bash' || type === 'bootstrap') { + const fullPath = path.join(localBuildsDirectory, singleBuild['bootstrap']); + const filename = buildInstallerFilePostfix.substring(1); // Removes the prefixed underscope _ on filename + return resolve({ fullPath, filename, path: localBuildsDirectory }); + } + + return reject({ + component: 'BuildController::downloadPreviousBuildsList', + message: `Unknown type '${type}'` + }); + } catch (err) { + logger.log(err); + logger.trace(); + return reject({ + component: 'BuildController::downloadPreviousBuildsList', + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + return retr; +} +module.exports = BuildController; diff --git a/.internal/api/src/controllers/configs.js b/.internal/api/src/controllers/configs.js new file mode 100644 index 000000000..aae632835 --- /dev/null +++ b/.internal/api/src/controllers/configs.js @@ -0,0 +1,289 @@ +const ConfigsController = ({ server, settings, version, logger }) => { + const { getDirectoryList, getFileList, emptyDirectory } = require('../utils/fsUtils'); + const retr = {}; + + const path = require('path'); + const fs = require('fs'); + + retr.init = () => { + logger.debug('ConfigsController:init()'); + }; + + retr.getConfigOptions = async ({ serviceName }) => { + return new Promise(async (resolve, reject) => { + let serviceBuildScript; + try { + const { + localTemplatesPath, + localServicesRelativePath, + configLogicFile + } = settings.paths; + const servicesBuildPath = path.join(localTemplatesPath, localServicesRelativePath); + serviceBuildScript = path.join(servicesBuildPath, serviceName, configLogicFile); + + const configLogic = require(serviceBuildScript)({ + settings, + version, + logger, + servicesBuildPath, + serviceBuildScript, + serviceTemplatesList, + serviceName + }); + + return resolve(configLogic.getConfigOptions()); + } catch (err) { + console.log(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ serviceName }); + console.debug({ serviceBuildScript }); + return reject({ + component: 'ConfigsController::getConfigOptions', + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.getAllConfigOptions = async () => { + return new Promise(async (resolve, reject) => { + try { + const { + localTemplatesPath, + localServicesRelativePath, + configLogicFile + } = settings.paths; + const servicesMetadata = {}; + const serviceTemplatesList = getDirectoryList(path.join(localTemplatesPath, localServicesRelativePath)); + serviceTemplatesList.forEach((serviceName) => { + const servicesBuildPath = path.join(localTemplatesPath, localServicesRelativePath); + const serviceBuildScript = path.join(servicesBuildPath, serviceName, configLogicFile); + + const configLogic = require(serviceBuildScript)({ + settings, + version, + logger, + servicesBuildPath, + serviceBuildScript, + serviceTemplatesList, + serviceName + }); + servicesMetadata[serviceName] = configLogic.getConfigOptions(); + }); + + return resolve(servicesMetadata) + } catch (err) { + console.log(err); + console.trace(); + return reject({ + component: 'ConfigsController::getAllConfigOptions', + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.getHelp = async ({ serviceName }) => { + return new Promise(async (resolve, reject) => { + let serviceBuildScript; + try { + const { + localTemplatesPath, + localServicesRelativePath, + configLogicFile + } = settings.paths; + const servicesBuildPath = path.join(localTemplatesPath, localServicesRelativePath); + serviceBuildScript = path.join(servicesBuildPath, serviceName, configLogicFile); + + const configHelp = require(serviceBuildScript)({ + settings, + version, + logger, + servicesBuildPath, + serviceBuildScript, + serviceName + }); + + return resolve(configHelp.getHelp()); + } catch (err) { + console.log(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ serviceName }); + console.debug({ serviceBuildScript }); + return reject({ + component: 'ConfigsController::getHelp', + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.getAllHelp = async () => { + return new Promise(async (resolve, reject) => { + try { + const { + localTemplatesPath, + localServicesRelativePath, + configLogicFile + } = settings.paths; + const servicesMetadata = {}; + const serviceTemplatesList = getDirectoryList(path.join(localTemplatesPath, localServicesRelativePath)); + + serviceTemplatesList.forEach((serviceName) => { + const servicesBuildPath = path.join(localTemplatesPath, localServicesRelativePath); + const serviceBuildScript = path.join(servicesBuildPath, serviceName, configLogicFile); + + const configHelp = require(serviceBuildScript)({ + settings, + version, + logger, + servicesBuildPath, + serviceBuildScript, + serviceName + }); + + servicesMetadata[serviceName] = configHelp.getHelp(); + }); + + return resolve(servicesMetadata); + } catch (err) { + console.log(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ serviceName }); + console.debug({ serviceBuildScript }); + return reject({ + component: 'ConfigsController::getAllHelp', + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.getScripts = async ({ serviceName, scriptName }) => { + return new Promise(async (resolve, reject) => { + let serviceBuildScript; + try { + const { + localTemplatesPath, + localServicesRelativePath, + configLogicFile + } = settings.paths; + const servicesBuildPath = path.join(localTemplatesPath, localServicesRelativePath); + serviceBuildScript = path.join(servicesBuildPath, serviceName, configLogicFile); + + const configLogic = require(serviceBuildScript)({ + settings, + version, + logger, + servicesBuildPath, + serviceBuildScript, + serviceName + }); + + if (scriptName) { + return resolve(configLogic.getCommands().commands[scriptName]); + } + + return resolve(configLogic.getCommands()); + } catch (err) { + console.log(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ serviceName }); + console.debug({ serviceBuildScript }); + console.debug({ scriptName }); + return reject({ + component: 'ConfigsController::getScripts', + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.getMeta = async ({ serviceName }) => { + return new Promise(async (resolve, reject) => { + let serviceBuildScript; + try { + const { + localTemplatesPath, + localServicesRelativePath, + configLogicFile + } = settings.paths; + const servicesBuildPath = path.join(localTemplatesPath, localServicesRelativePath); + serviceBuildScript = path.join(servicesBuildPath, serviceName, configLogicFile); + + const configLogic = require(serviceBuildScript)({ + settings, + version, + logger, + servicesBuildPath, + serviceBuildScript, + serviceName + }); + + return resolve(configLogic.getMeta()); + } catch (err) { + console.log(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ serviceName }); + console.debug({ serviceBuildScript }); + return reject({ + component: 'ConfigsController::getMeta', + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.getAllMeta = async () => { + return new Promise(async (resolve, reject) => { + try { + const { + localTemplatesPath, + localServicesRelativePath, + configLogicFile + } = settings.paths; + const servicesMetadata = {}; + const serviceTemplatesList = getDirectoryList(path.join(localTemplatesPath, localServicesRelativePath)); + serviceTemplatesList.forEach((serviceName) => { + const servicesBuildPath = path.join(localTemplatesPath, localServicesRelativePath); + const serviceBuildScript = path.join(servicesBuildPath, serviceName, configLogicFile); + + const configLogic = require(serviceBuildScript)({ + settings, + version, + logger, + servicesBuildPath, + serviceBuildScript, + serviceTemplatesList, + serviceName + }); + servicesMetadata[serviceName] = configLogic.getMeta(); + }); + + return resolve(servicesMetadata) + } catch (err) { + console.log(err); + console.trace(); + return reject({ + component: 'ConfigsController::getAllMeta', + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + return retr; +} +module.exports = ConfigsController; diff --git a/.internal/api/src/controllers/health.js b/.internal/api/src/controllers/health.js new file mode 100644 index 000000000..72a7efe33 --- /dev/null +++ b/.internal/api/src/controllers/health.js @@ -0,0 +1,35 @@ +const HealthController = ({ server, settings, version, logger }) => { + const retr = {}; + + retr.init = () => { + logger.debug('HealthController:init()'); + }; + + retr.healthCheck = () => { + const baseHealthResults = { + server: "online", + api: true, + version + }; + + return new Promise((resolveHealth, rejectHealth) => { + logger.log('HealthController:healthCheck()'); + return resolveHealth(baseHealthResults); + }); + }; + + retr.healthCheckNoLog = () => { + const baseHealthResults = { + server: "online", + api: true, + version + }; + + return new Promise((resolveHealth, rejectHealth) => { + return resolveHealth(baseHealthResults); + }); + }; + + return retr; +} +module.exports = HealthController; diff --git a/.internal/api/src/controllers/templates.js b/.internal/api/src/controllers/templates.js new file mode 100644 index 000000000..3e8b47b5a --- /dev/null +++ b/.internal/api/src/controllers/templates.js @@ -0,0 +1,412 @@ +const fsUtils = require('../utils/fsUtils'); + +const TemplatesController = ({ server, settings, version, logger }) => { + const TemplatesService = require('../services/templates'); + const { getDirectoryList, filterBadPathStrings } = require('../utils/fsUtils'); + const retr = {}; + + const templatesService = TemplatesService({ server, settings, version, logger }); + const path = require('path'); + const fs = require('fs'); + const yaml = require('js-yaml'); + + retr.init = () => { + templatesService.init(); + logger.debug('TemplatesController:init()'); + }; + + retr.getServiceTemplateAsYaml = (templateName) => { + return new Promise((resolve, reject) => { + try { + if (!templateName) { + return reject({ + component: 'TemplatesController::getServiceTemplateAsYaml', + message: 'No template name passed' + }); + } + return templatesService.getServiceTemplateFromFile(templateName).then((result) => { + return resolve(result); + }).catch((err) => { + return reject({ + component: 'TemplatesController::getServiceTemplateAsYaml', + message: 'Unable to get template', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + }); + } catch (err) { + console.log(err); + console.trace(); + return reject({ + component: 'TemplatesController::getServiceTemplateAsYaml', + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.getNetworkTemplateAsYaml = (templateName) => { + return new Promise((resolve, reject) => { + try { + if (!templateName) { + return reject({ + component: 'TemplatesController::getNetworkTemplateAsYaml', + message: 'No template name passed' + }); + } + return templatesService.getNetworkTemplateFromFile(templateName).then((result) => { + return resolve(result); + }).catch((err) => { + return reject({ + component: 'TemplatesController::getNetworkTemplateAsYaml', + message: 'Unable to get template', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + }); + } catch (err) { + console.log(err); + console.trace(); + return reject({ + component: 'TemplatesController::getNetworkTemplateAsYaml', + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.getServiceTemplateAsJson = (templateName) => { + return new Promise((resolve, reject) => { + try { + if (!templateName) { + return reject({ + component: 'TemplatesController::getServiceTemplateAsJson', + message: 'No template name passed' + }); + } + return templatesService.getServiceTemplateFromFile(templateName, true).then((result) => { + return resolve(result); + }).catch((err) => { + return reject({ + component: 'TemplatesController::getServiceTemplateAsJson', + message: 'Unable to get template', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + }); + } catch (err) { + console.log(err); + console.trace(); + return reject({ + component: 'TemplatesController::getServiceTemplateAsJson', + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.getNetworkTemplateAsJson = (templateName) => { + return new Promise((resolve, reject) => { + try { + if (!templateName) { + return reject({ + component: 'TemplatesController::getNetworkTemplateAsJson', + message: 'No template name passed' + }); + } + return templatesService.getNetworkTemplateFromFile(templateName, true).then((result) => { + return resolve(result); + }).catch((err) => { + return reject({ + component: 'TemplatesController::getNetworkTemplateAsJson', + message: 'Unable to get template', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + }); + } catch (err) { + console.log(err); + console.trace(); + return reject({ + component: 'TemplatesController::getNetworkTemplateAsJson', + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.getAllServiceTemplatesAsYaml = () => { + return new Promise((resolve, reject) => { + try { + const { localTemplatesPath, localServicesRelativePath } = settings.paths; + const serviceTemplatesList = getDirectoryList(path.join(localTemplatesPath, localServicesRelativePath)); + const promiseWaitList = []; + serviceTemplatesList.forEach((serviceDirectory) => { + promiseWaitList.push(templatesService.getServiceTemplateFromFile(serviceDirectory, true)); + }); + return Promise.allSettled(promiseWaitList).then((resultList) => { + const serviceList = {}; + resultList.forEach((servicePromiseResult) => { + if (servicePromiseResult.status === 'fulfilled' && servicePromiseResult.value) { + const serviceName = Object.keys(servicePromiseResult.value)[0]; + serviceList[serviceName] = servicePromiseResult.value[serviceName]; + } + }); + return resolve(yaml.dump(serviceList)); + }).catch((err) => { + return reject({ + component: 'TemplatesController::getAllServiceTemplatesAsYaml', + message: 'Unable to get templates', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + }); + } catch (err) { + console.log(err); + console.trace(); + return reject({ + component: 'TemplatesController::getAllServiceTemplatesAsYaml', + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.getAllNetworkTemplatesAsYaml = () => { + return new Promise((resolve, reject) => { + try { + const { localTemplatesPath, localNetworksRelativePath } = settings.paths; + const networkTemplatesList = getDirectoryList(path.join(localTemplatesPath, localNetworksRelativePath)); + const promiseWaitList = []; + networkTemplatesList.forEach((networkDirectory) => { + promiseWaitList.push(templatesService.getNetworkTemplateFromFile(networkDirectory, true)); + }); + return Promise.allSettled(promiseWaitList).then((resultList) => { + const networkList = {}; + resultList.forEach((networkPromiseResult) => { + if (networkPromiseResult.status === 'fulfilled' && networkPromiseResult.value) { + const networkName = Object.keys(networkPromiseResult.value)[0]; + networkList[networkName] = networkPromiseResult.value[networkName]; + } + }); + return resolve(yaml.dump(networkList)); + }).catch((err) => { + return reject({ + component: 'TemplatesController::getAllNetworkTemplatesAsYaml', + message: 'Unable to get templates', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + }); + } catch (err) { + console.log(err); + console.trace(); + return reject({ + component: 'TemplatesController::getAllNetworkTemplatesAsYaml', + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.getAllServiceTemplatesAsJson = () => { + return new Promise((resolve, reject) => { + try { + const { localTemplatesPath, localServicesRelativePath } = settings.paths; + const serviceTemplatesList = getDirectoryList(path.join(localTemplatesPath, localServicesRelativePath)); + const promiseWaitList = []; + serviceTemplatesList.forEach((serviceDirectory) => { + promiseWaitList.push(templatesService.getServiceTemplateFromFile(serviceDirectory, true)); + }); + return Promise.allSettled(promiseWaitList).then((resultList) => { + const serviceList = {}; + resultList.forEach((servicePromiseResult) => { + if (servicePromiseResult.status === 'fulfilled' && servicePromiseResult.value) { + const serviceName = Object.keys(servicePromiseResult.value)[0]; + serviceList[serviceName] = servicePromiseResult.value[serviceName]; + } + }); + return resolve(serviceList); + }).catch((err) => { + return reject({ + component: 'TemplatesController::getAllServiceTemplatesAsJson', + message: 'Unable to get templates', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + }); + } catch (err) { + console.log(err); + console.trace(); + return reject({ + component: 'TemplatesController::getAllServiceTemplatesAsJson', + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.getAllNetworkTemplatesAsJson = () => { + return new Promise((resolve, reject) => { + try { + const { localTemplatesPath, localNetworksRelativePath } = settings.paths; + const networkTemplatesList = getDirectoryList(path.join(localTemplatesPath, localNetworksRelativePath)); + const promiseWaitList = []; + networkTemplatesList.forEach((networkDirectory) => { + promiseWaitList.push(templatesService.getNetworkTemplateFromFile(networkDirectory, true)); + }); + return Promise.allSettled(promiseWaitList).then((resultList) => { + const networkList = {}; + resultList.forEach((networkPromiseResult) => { + if (networkPromiseResult.status === 'fulfilled' && networkPromiseResult.value) { + const networkName = Object.keys(networkPromiseResult.value)[0]; + networkList[networkName] = networkPromiseResult.value[networkName]; + } + }); + return resolve(networkList); + }).catch((err) => { + return reject({ + component: 'TemplatesController::getAllNetworkTemplatesAsJson', + message: 'Unable to get templates', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + }); + } catch (err) { + console.log(err); + console.trace(); + return reject({ + component: 'TemplatesController::getAllNetworkTemplatesAsJson', + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.getAllServiceTemplatesAsList = () => { + return new Promise((resolve, reject) => { + try { + const { localTemplatesPath, localServicesRelativePath } = settings.paths; + const serviceTemplatesList = getDirectoryList(path.join(localTemplatesPath, localServicesRelativePath)); + return resolve(serviceTemplatesList); + } catch (err) { + console.log(err); + console.trace(); + return reject({ + component: 'TemplatesController::getAllServiceTemplatesAsList', + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.getAllNetworkTemplatesAsList = () => { + return new Promise((resolve, reject) => { + try { + const { localTemplatesPath, localNetworksRelativePath } = settings.paths; + const networkTemplatesList = getDirectoryList(path.join(localTemplatesPath, localNetworksRelativePath)); + return resolve(networkTemplatesList); + } catch (err) { + console.log(err); + console.trace(); + return reject({ + component: 'TemplatesController::getAllNetworkTemplatesAsList', + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.getServiceTemplateFile = ({ templateName, filename }) => { + return new Promise((resolve, reject) => { + try { + const useTemplateName = filterBadPathStrings(templateName); + const useFilename = filterBadPathStrings(filename); + const { localTemplatesPath, localServicesRelativePath, serviceFiles } = settings.paths; + const servicePath = path.join(localTemplatesPath, localServicesRelativePath, useTemplateName); + if (!fs.existsSync(servicePath)) { + return reject({ + component: 'TemplatesController::getServiceTemplateFile', + message: `Service '${templateName}' doesn't exist.` + }); + } + + const serviceFilesPath = path.join(servicePath, serviceFiles); + + if (!fs.existsSync(serviceFilesPath)) { + return reject({ + component: 'TemplatesController::getServiceTemplateFile', + message: `Service '${templateName}' doesn't have any files to share.` + }); + } + + const filePath = path.join(serviceFilesPath, useFilename); + + if (fs.existsSync(filePath)) { + return resolve(filePath); + } + return reject({ + component: 'TemplatesController::getServiceTemplateFile', + message: `File '${filename}' doesn't exist for service '${templateName}'` + }); + } catch (err) { + console.log(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ templateName }); + console.debug({ filename }); + return reject({ + component: 'TemplatesController::getServiceTemplateFile', + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.getScriptTemplateFile = ({ scriptName, options, req }) => { + return new Promise(async (resolve, reject) => { + try { + const useScriptTemplateName = filterBadPathStrings(scriptName); + const { localTemplatesPath, localScriptsRelativePath } = settings.paths; + const scriptPath = path.join(localTemplatesPath, localScriptsRelativePath, useScriptTemplateName); + if (!fs.existsSync(`${scriptPath}.js`)) { + return reject({ + component: 'TemplatesController::getScriptTemplateFile', + message: `Script '${scriptName}' doesn't exist.` + }); + } + + delete require.cache[require.resolve(scriptPath)]; + const scriptToSend = require(scriptPath); + + const results = await scriptToSend({ + req, + scriptName, + options, + server, + settings, + version, + logger + }); + + return resolve(results); + } catch (err) { + console.log(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ scriptName }); + return reject({ + component: 'TemplatesController::getScriptTemplateFile', + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + return retr; +} +module.exports = TemplatesController; diff --git a/.internal/api/src/httpInit.js b/.internal/api/src/httpInit.js new file mode 100644 index 000000000..ba5ffb2b5 --- /dev/null +++ b/.internal/api/src/httpInit.js @@ -0,0 +1,30 @@ +var serverController = ({ logger, listenInterface, listenPort, settings, version } = {}) => { + const express = require('express'); + const middlewares = require('./middlewares/index'); + const routes = require('./routes/index'); + + return new Promise((resolve, reject) => { + try { + const server = express(); + + server.on('error', (err) => { + logger.error(err); + return reject(err); + }); + + middlewares({ app: server, cors: settings.cors, version, logger }); + routes({ server, settings, version, logger }); + + server.listen(listenPort, listenInterface, () => { + logger.info(`Listening on: ${listenInterface}:${listenPort}`); + return resolve(server); + }); + } catch(err) { + logger.error(JSON.stringify(err, Object.getOwnPropertyNames(err))); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + } + + }); +} + +module.exports = serverController; \ No newline at end of file diff --git a/.internal/api/src/index.js b/.internal/api/src/index.js new file mode 100644 index 000000000..f2efdf004 --- /dev/null +++ b/.internal/api/src/index.js @@ -0,0 +1,115 @@ +let appVersion = require('../package.json').version; + +let listenInterface = process.env?.API_INTERFACE ?? '0.0.0.0'; +let listenPort = process.env?.API_PORT ?? '32128'; +let wuiPort = process.env?.WUI_PORT ?? '32777'; +let additionalCorsList = []; + +process.on('SIGINT', () => { + process.exit(); +}); + +const processCliArgs = (args) => { + for (let i = 0; i < args.length; i++) { + switch (args[i]) { + + case '-if': + case '--listen-interface': { + listenInterface = args[i + 1]; + i++; + break; + } + + case '-cors': + case '--cors': { + try { + additionalCorsList = [ + ...additionalCorsList, + ...args[i + 1]?.split(',') ?? [] + ]; + } catch (err) { + console.error('processCliArgs: Error on cors:'); + console.error(err); + process.exit(1); + } + i++; + break; + } + + case '-p': + case '--port': { + try { + if (Number.isFinite(Number.parseInt(args[i + 1]))) { + listenPort = args[i + 1]; + i++; + continue; + } + console.error(`listenPort '${args[i + 1]}' is not a number.`); + } catch (err) { + console.error('processCliArgs: Error on parseInt:'); + console.error(err); + process.exit(1); + } + break; + } + } + } +}; + +const checkCliParams = () => { + const errorList = []; + if (!listenInterface) { + errorList.push(`[-if]: Listen interface not set.`); + } + + if (!listenPort) { + errorList.push(`[-p]: Listen port not set.`); + } + + if (errorList.length > 0) { + throw new Error(errorList.join("\r\n")); + } +}; + +const processEnvVars = (envs) => { + const { cors, CORS } = envs; + + try { + additionalCorsList = [ + ...additionalCorsList, + ...cors?.split(/[\s,]+/).map((c) => c && c.indexOf(':') < 0 ? `${c}:${wuiPort}` : (c || null)).filter((e) => e !== null) ?? [] + ]; + additionalCorsList = [ + ...additionalCorsList, + ...CORS?.split(/[\s,]+/).map((c) => c && c.indexOf(':') < 0 ? `${c}:${wuiPort}` : (c || null)).filter((e) => e !== null) ?? [] + ]; + } catch (err) { + console.error('processEnvVars: Error on cors:'); + console.error(err); + process.exit(1); + } +}; + +const init = () => { + const logger = require('./logger')(); + + logger.info(`IOTstack API Server has started. Version: '${appVersion}', Environment: '${process.env.NODE_ENV}'`); + const settings = require('./settings')({ env: process.env.NODE_ENV, version: appVersion, logger }); + settings.cors.origins = [ + ...settings?.cors?.origins ?? [], + ...additionalCorsList + ]; + const serverSetupPromise = require('./httpInit')({ logger, listenInterface, listenPort, version: appVersion, settings }); + + serverSetupPromise.then((runningServer) => { + logger.info('Server ready.'); + }).catch((err) => { + logger.error({ module: 'Main::Init()', section: 'serverSetupPromise', message: `${err}, stack: ${err.stack}` }); + process.exit(2); + }); +} + +processCliArgs(process.argv); +checkCliParams(); +processEnvVars(process.env) +init(); \ No newline at end of file diff --git a/.internal/api/src/logger.js b/.internal/api/src/logger.js new file mode 100644 index 000000000..a00fd1be6 --- /dev/null +++ b/.internal/api/src/logger.js @@ -0,0 +1,13 @@ +const Logger = () => { + const retr = {}; + + retr.debug = console.debug; + retr.info = console.info; + retr.log = console.log; + retr.warn = console.warn; + retr.error = console.error; + + return retr; +} + +module.exports = Logger; \ No newline at end of file diff --git a/.internal/api/src/middlewares/cors.js b/.internal/api/src/middlewares/cors.js new file mode 100644 index 000000000..94f87a50f --- /dev/null +++ b/.internal/api/src/middlewares/cors.js @@ -0,0 +1,23 @@ +const registerCors = ({ server, corsList, headerList, allowHttp = false, logger } = {}) => { + server.use((req, res, next) => { + const origin = req.headers.origin ? req.headers.origin.replace(/(^\w+:|^)\/\//, '') : ''; + const foundHeader = corsList.indexOf(origin) > -1 ? origin : ''; + + if (foundHeader) { + if (allowHttp) { + res.setHeader('Access-Control-Allow-Origin', `http://${foundHeader}`); + } else { + res.setHeader('Access-Control-Allow-Origin', `https://${foundHeader}`); + } + + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE, HEAD'); + res.setHeader('Access-Control-Allow-Headers', headerList.join(',')); + res.setHeader('Access-Control-Allow-Credentials', true); + } + next(); + }); + + logger.info(`Cors settings loaded: Origins: [${corsList}] Headers: [${headerList}] Allow HTTP: ${allowHttp.toString()}.`); +}; + +module.exports = registerCors; \ No newline at end of file diff --git a/.internal/api/src/middlewares/index.js b/.internal/api/src/middlewares/index.js new file mode 100644 index 000000000..de775a25c --- /dev/null +++ b/.internal/api/src/middlewares/index.js @@ -0,0 +1,19 @@ +const registerMiddlewareHooks = ({ app, cors, logger } = {}) => { + const bodyParser = require('body-parser'); + + app.use(bodyParser.urlencoded({ extended: false })); + app.use(bodyParser.json()); + app.disable('x-powered-by'); + + const corsMiddleware = require('./cors'); + + corsMiddleware({ + server: app, + corsList: cors.origins, + headerList: cors.headers, + allowHttp: process.env.APP_ENV !== 'production', + logger + }); +}; + +module.exports = registerMiddlewareHooks; \ No newline at end of file diff --git a/.internal/api/src/routes/build.js b/.internal/api/src/routes/build.js new file mode 100644 index 000000000..69c9f3ff5 --- /dev/null +++ b/.internal/api/src/routes/build.js @@ -0,0 +1,49 @@ +const registerBuildRoutes = ({ server, settings, version, logger } = {}) => { + const BuildView = require('../views/build'); + + const buildView = BuildView({ server, settings, version, logger }); + + buildView.init(); + + server.post('/build/save', (req, res, next) => { + buildView.buildStack(req, res, next); + }); + + server.post('/build/dryrun', (req, res, next) => { + buildView.checkIssues(req, res, next); + }); + + server.get('/build/list', (req, res, next) => { + buildView.getPreviousBuildsList(req, res, next); + }); + + server.get('/build/list/index/:index/limit/:limit', (req, res, next) => { + buildView.getPreviousBuildsList(req, res, next); + }); + + server.get('/build/list/limit/:limit/index/:index', (req, res, next) => { + buildView.getPreviousBuildsList(req, res, next); + }); + + server.get('/build/list/index/:index', (req, res, next) => { + buildView.getPreviousBuildsList(req, res, next); + }); + + server.get('/build/list/limit/:limit', (req, res, next) => { + buildView.getPreviousBuildsList(req, res, next); + }); + + server.get('/build/get/:buildTime', (req, res, next) => { + buildView.getPreviousBuildsList(req, res, next); + }); + + server.get('/build/get/:buildTime/:type', (req, res, next) => { + buildView.downloadPreviousBuildsList(req, res, next); + }); + + server.post('/build/delete/:buildTime', (req, res, next) => { + buildView.deletePreviousBuild(req, res, next); + }); +}; + +module.exports = registerBuildRoutes; diff --git a/.internal/api/src/routes/configs.js b/.internal/api/src/routes/configs.js new file mode 100644 index 000000000..d2d85f60c --- /dev/null +++ b/.internal/api/src/routes/configs.js @@ -0,0 +1,57 @@ +const registerConfigRoutes = ({ server, settings, version, logger } = {}) => { + const ConfigView = require('../views/configs'); + + const configView = ConfigView({ server, settings, version, logger }); + + configView.init(); + + server.get('/config/:serviceName/options', (req, res, next) => { + configView.getConfigOptions(req, res, next); + }); + + server.get('/config/:serviceName/help', (req, res, next) => { + configView.getHelp(req, res, next); + }); + + server.get('/config/help', (req, res, next) => { + configView.getAllHelp(req, res, next); + }); + + server.get('/config/:serviceName/scripts', (req, res, next) => { + configView.getScripts(req, res, next); + }); + + server.get('/config/:serviceName/commands', (req, res, next) => { + configView.getScripts(req, res, next); + }); + + server.get('/config/:serviceName/scripts/:scriptName', (req, res, next) => { + configView.getScripts(req, res, next); + }); + + server.get('/config/:serviceName/script/:scriptName', (req, res, next) => { + configView.getScripts(req, res, next); + }); + + server.get('/config/:serviceName/commands/:scriptName', (req, res, next) => { + configView.getScripts(req, res, next); + }); + + server.get('/config/:serviceName/command/:scriptName', (req, res, next) => { + configView.getScripts(req, res, next); + }); + + server.get('/config/:serviceName/meta', (req, res, next) => { + configView.getMeta(req, res, next); + }); + + server.get('/config/meta', (req, res, next) => { + configView.getAllMeta(req, res, next); + }); + + server.get('/config/options', (req, res, next) => { + configView.getAllConfigOptions(req, res, next); + }); +}; + +module.exports = registerConfigRoutes; diff --git a/.internal/api/src/routes/health.js b/.internal/api/src/routes/health.js new file mode 100644 index 000000000..9fd0382ea --- /dev/null +++ b/.internal/api/src/routes/health.js @@ -0,0 +1,33 @@ +const registerHealthRoutes = ({ server, settings, version, logger } = {}) => { + const HealthView = require('../views/health'); + + const healthView = HealthView({ server, settings, version, logger }); + + healthView.init(); + + server.post('/health', (req, res, next) => { + healthView.health(req, res, next); + }); + + server.post('/ping', (req, res, next) => { + healthView.health(req, res, next); + }); + + server.get('/health', (req, res, next) => { + healthView.health(req, res, next); + }); + + server.get('/health/no-log', (req, res, next) => { + healthView.healthCheckNoLog(req, res, next); + }); + + server.post('/health/no-log', (req, res, next) => { + healthView.healthCheckNoLog(req, res, next); + }); + + server.get('/ping', (req, res, next) => { + healthView.health(req, res, next); + }); +}; + +module.exports = registerHealthRoutes; \ No newline at end of file diff --git a/.internal/api/src/routes/index.js b/.internal/api/src/routes/index.js new file mode 100644 index 000000000..a046eb583 --- /dev/null +++ b/.internal/api/src/routes/index.js @@ -0,0 +1,19 @@ +const registerRouteHooks = ({ server, settings, version, logger } = {}) => { + + const healthRoutes = require('./health'); + healthRoutes({ server, settings, version, logger }); + + const templatesRoutes = require('./templates'); + templatesRoutes({ server, settings, version, logger }); + + const buildRoutes = require('./build'); + buildRoutes({ server, settings, version, logger }); + + const configRoutes = require('./configs'); + configRoutes({ server, settings, version, logger }); + + const staticRoutes = require('./static'); + staticRoutes({ server, settings, version, logger }); +}; + +module.exports = registerRouteHooks; \ No newline at end of file diff --git a/.internal/api/src/routes/static.js b/.internal/api/src/routes/static.js new file mode 100644 index 000000000..1cd3245ee --- /dev/null +++ b/.internal/api/src/routes/static.js @@ -0,0 +1,20 @@ +const path = require('path'); + +const registerStaticRoutes = ({ server, settings, version, logger } = {}) => { + const express = require('express'); + + const statics = [ + { + route: '/static/none', + path: path.join(__dirname, '../../static/') + } + ]; + + logger.debug('StaticRouter: Serving: ', statics); + + statics.forEach((staticServe) => { + server.use(staticServe.route, express.static(staticServe.path)); + }) +}; + +module.exports = registerStaticRoutes; diff --git a/.internal/api/src/routes/templates.js b/.internal/api/src/routes/templates.js new file mode 100644 index 000000000..cee0c1a24 --- /dev/null +++ b/.internal/api/src/routes/templates.js @@ -0,0 +1,69 @@ +const registerTemplatesRoutes = ({ server, settings, version, logger } = {}) => { + const TemplatesView = require('../views/templates'); + + const templatesView = TemplatesView({ server, settings, version, logger }); + + templatesView.init(); + + server.get('/templates/services/:templateName/file/:filename', (req, res, next) => { + templatesView.getServiceTemplateFile(req, res, next); + }); + + server.get('/templates/services/yaml', (req, res, next) => { + templatesView.getAllServiceTemplatesAsYaml(req, res, next); + }); + + server.get('/templates/services/json', (req, res, next) => { + templatesView.getAllServiceTemplatesAsJson(req, res, next); + }); + + server.get('/templates/services/list', (req, res, next) => { + templatesView.getAllServiceTemplatesAsList(req, res, next); + }); + + server.get('/templates/services/yaml/:templateName', (req, res, next) => { + templatesView.getServiceTemplateAsYaml(req, res, next); + }); + + server.get('/templates/services/json/:templateName', (req, res, next) => { + templatesView.getServiceTemplateAsJson(req, res, next); + }); + + server.get('/templates/networks/yaml', (req, res, next) => { + templatesView.getAllNetworkTemplatesAsYaml(req, res, next); + }); + + server.get('/templates/networks/json', (req, res, next) => { + templatesView.getAllNetworkTemplatesAsJson(req, res, next); + }); + + server.get('/templates/networks/list', (req, res, next) => { + templatesView.getAllNetworkTemplatesAsList(req, res, next); + }); + + server.get('/templates/networks/yaml/:templateName', (req, res, next) => { + templatesView.getNetworkTemplateAsYaml(req, res, next); + }); + + server.get('/templates/networks/json/:templateName', (req, res, next) => { + templatesView.getNetworkTemplateAsJson(req, res, next); + }); + + server.get('/templates/scripts/:scriptName*', (req, res, next) => { + templatesView.getScriptTemplate(req, res, next); + }); + + server.get('/templates/scripts/download/:scriptName*', (req, res, next) => { + templatesView.downloadScriptTemplate(req, res, next); + }); + + server.post('/templates/scripts/:scriptName*', (req, res, next) => { + templatesView.getScriptTemplate(req, res, next); + }); + + server.post('/templates/scripts/download/:scriptName*', (req, res, next) => { + templatesView.downloadScriptTemplate(req, res, next); + }); +}; + +module.exports = registerTemplatesRoutes; \ No newline at end of file diff --git a/.internal/api/src/services/builds.js b/.internal/api/src/services/builds.js new file mode 100644 index 000000000..3592a8319 --- /dev/null +++ b/.internal/api/src/services/builds.js @@ -0,0 +1,71 @@ +const BuildsService = ({ server, settings, version, logger }) => { + const path = require('path'); + const yaml = require('js-yaml'); + const fs = require('fs'); + const retr = {}; + + retr.init = () => { + logger.debug('BuildsService:init()'); + }; + + retr.saveBuildYaml = ({ buildJson, fileTimePrefix }) => { + return new Promise((resolve, reject) => { + let yamlOutputFilePath; + try { + const { localBuildsDirectory, buildDockerFilePostfix } = settings.paths; + const yamlFilename = `${fileTimePrefix}${buildDockerFilePostfix}`; + yamlOutputFilePath = path.join(localBuildsDirectory, yamlFilename); + + let yamlDoc = yaml.safeDump(buildJson, { scalarQuoteStyle: 'double' }); + yamlDoc = yamlDoc.replace(/'/g, '"'); // { scalarQuoteStyle: 'double' } doesn't seem to be working + + fs.writeFileSync(yamlOutputFilePath, yamlDoc, 'utf8'); + return resolve({ yamlOutputFilePath, yamlFilename }); + } catch (err) { + console.log(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ buildJson }); + console.debug({ fileTimePrefix }); + console.debug({ yamlOutputFilePath }); + return reject({ + component: 'BuildsService::saveBuildYaml', + message: 'Error saving yaml to file.', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.saveBuildOptions = ({ buildOptionsJson, fileTimePrefix }) => { + return new Promise((resolve, reject) => { + let jsonOutputFilePath; + try { + buildOptionsJson.build = fileTimePrefix; + const { localBuildsDirectory, buildOptionsFilePostfix } = settings.paths; + const jsonFilename = `${fileTimePrefix}${buildOptionsFilePostfix}`; + jsonOutputFilePath = path.join(localBuildsDirectory, jsonFilename); + + let jsonDoc = JSON.stringify(buildOptionsJson); + + fs.writeFileSync(jsonOutputFilePath, jsonDoc, 'utf8'); + return resolve({ jsonOutputFilePath, jsonFilename }); + } catch (err) { + console.log(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ buildOptionsJson }); + console.debug({ fileTimePrefix }); + console.debug({ jsonOutputFilePath }); + return reject({ + component: 'BuildsService::saveBuildOptions', + message: 'Error saving json to file.', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + return retr; +} +module.exports = BuildsService; diff --git a/.internal/api/src/services/templates.js b/.internal/api/src/services/templates.js new file mode 100644 index 000000000..8701fcf65 --- /dev/null +++ b/.internal/api/src/services/templates.js @@ -0,0 +1,67 @@ +const TemplatesService = ({ server, settings, version, logger }) => { + const path = require('path'); + const yaml = require('js-yaml'); + const fs = require('fs'); + const retr = {}; + + retr.init = () => { + logger.debug('TemplatesService:init()'); + }; + + retr.getServiceTemplateFromFile = (templateName, jsonified = false) => { + return new Promise((resolve, reject) => { + let templateYamlPath + try { + const { localTemplatesPath, localServicesRelativePath, localServicesTemplateYamlFilename } = settings.paths; + templateYamlPath = path.join(localTemplatesPath, localServicesRelativePath, templateName, localServicesTemplateYamlFilename); + if (jsonified) { + const yamlDoc = yaml.safeLoad(fs.readFileSync(templateYamlPath, 'utf8')); + return resolve(yamlDoc); + } + const yamlDoc = yaml.dump(yaml.safeLoad(fs.readFileSync(templateYamlPath, 'utf8'))); + return resolve(yamlDoc); + } catch (err) { + console.log(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ templateName }); + console.debug({ templateYamlPath }); + return reject({ + component: 'TemplatesService::getServiceTemplateFromFile', + message: 'Error getting yaml template from file.', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.getNetworkTemplateFromFile = (templateName, jsonified = false) => { + return new Promise((resolve, reject) => { + let templateYamlPath + try { + const { localTemplatesPath, localNetworksRelativePath, localNetworkTemplateYamlFilename } = settings.paths; + templateYamlPath = path.join(localTemplatesPath, localNetworksRelativePath, templateName, localNetworkTemplateYamlFilename); + if (jsonified) { + const yamlDoc = yaml.safeLoad(fs.readFileSync(templateYamlPath, 'utf8')); + return resolve(yamlDoc); + } + const yamlDoc = yaml.dump(yaml.safeLoad(fs.readFileSync(templateYamlPath, 'utf8'))); + return resolve(yamlDoc); + } catch (err) { + console.log(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ templateName }); + console.debug({ templateYamlPath }); + return reject({ + component: 'TemplatesService::getNetworkTemplateFromFile', + message: 'Error getting yaml template from file.', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + return retr; +} +module.exports = TemplatesService; diff --git a/.internal/api/src/services/zip.js b/.internal/api/src/services/zip.js new file mode 100644 index 000000000..16af97331 --- /dev/null +++ b/.internal/api/src/services/zip.js @@ -0,0 +1,82 @@ +const ZipService = ({ server, settings, version, logger }) => { + const path = require('path'); + const archiver = require('archiver'); + const fs = require('fs'); + const retr = {}; + + retr.init = () => { + logger.debug('ZipService:init()'); + }; + + retr.zipFiles = ({ fileList, fileTimePrefix, archiveDirectoryList = [] }) => { + return new Promise((resolve, reject) => { + let zipOutputFilePath; + try { + const { localBuildsDirectory, buildZipFilePostfix } = settings.paths; + const zipFilename = `${fileTimePrefix}${buildZipFilePostfix}`; + zipOutputFilePath = path.join(localBuildsDirectory, zipFilename); + + const outputStream = fs.createWriteStream(zipOutputFilePath); + const archive = archiver('zip', { + zlib: { level: 9 } // Sets the compression level. + }); + + archive.on('warning', (err) => { + console.log({ + component: 'ZipService::zipFiles', + message: 'A warning was raised', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + }); + + // good practice to catch this error explicitly + archive.on('error', (err) => { + console.error({ + component: 'ZipService::zipFiles', + message: 'A warning was raised', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + return reject({ + component: 'ZipService::zipFiles', + message: 'Error saving zip.', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }) + }); + + outputStream.on('close', () => { + console.debug(`Zip '${zipFilename}' created (${archive.pointer()} bytes) Files given: '${fileList.length}'.`); + return resolve({ zipFilename, zipOutputFilePath }); + }); + + archive.pipe(outputStream); + + if (Array.isArray(archiveDirectoryList) && archiveDirectoryList.length > 0) { + archiveDirectoryList.forEach((directoryName) => { + archive.append(null, { name: directoryName }); + }); + } + + fileList.forEach((filename) => { + archive.file(filename.fullPath, { name: filename.zipName }); + }); + + return archive.finalize(); + } catch (err) { + console.log(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ fileList }); + console.debug({ fileTimePrefix }); + console.debug({ zipOutputFilePath }); + return reject({ + component: 'ZipService::zipFiles', + message: 'Error saving zip.', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + return retr; +} +module.exports = ZipService; diff --git a/.internal/api/src/settings.js b/.internal/api/src/settings.js new file mode 100644 index 000000000..25c378aad --- /dev/null +++ b/.internal/api/src/settings.js @@ -0,0 +1,34 @@ +const Settings = ({ env, logger, version }) => { + const path = require('path'); + + logger.info(`Settings loaded. env: '${env}', AppVersion: '${version}'`); + + const retr = { + cors: { + origins: ["localhost", "127.0.0.1", "localhost:32777", "127.0.0.1:32777", "localhost:3000"], + headers: ["Content-Type" ,"Origin", "Accept"] + }, + paths: { + localTemplatesPath: path.join(__dirname, '../templates/'), + localTmpPath: path.join(__dirname, '/.tmp/'), + localBuildsDirectory: path.join(__dirname, '../builds/'), + localServicesRelativePath: '/services/', + localNetworksRelativePath: '/networks/', + localScriptsRelativePath: '/scripts/', + localServicesTemplateYamlFilename: 'template.yml', + localNetworkTemplateYamlFilename: 'template.yml', + localServicesTemplateConfig: 'template.js', + buildLogicFile: 'build.js', + configLogicFile: 'config.js', + buildDockerFilePostfix: '_docker-compose-base.yml', + buildOptionsFilePostfix: '_build-options.json', + buildZipFilePostfix: '_build.zip', + buildInstallerFilePostfix: '_build-installer.sh', + serviceFiles: '/serviceFiles/', + buildFiles: '/buildFiles/' + } + }; + return retr; +} + +module.exports = Settings; diff --git a/.internal/api/src/utils/commonBuildChecks.js b/.internal/api/src/utils/commonBuildChecks.js new file mode 100644 index 000000000..9372df48e --- /dev/null +++ b/.internal/api/src/utils/commonBuildChecks.js @@ -0,0 +1,93 @@ +const { + getExternalPort +} = require('./dockerParse'); + +const checkPortConflicts = ({ buildTemplate, buildOptions, serviceName }) => { + const portConflicts = []; + + const currentServiceExternalPorts = buildTemplate?.services?.[serviceName]?.ports?.map((port) => { + return getExternalPort(port); + }) ?? []; + + // This is for services that have network mode set to host (and ports can't be specified in the docker-compose file) + const currentConfiguredServiceExternalPorts = Object.values(buildOptions?.configurations?.services?.[serviceName]?.ports ?? [])?.map((port) => { + return getExternalPort(port); + }) ?? []; + + Object.keys(buildTemplate?.services ?? {}).forEach((service) => { + if (service === serviceName) { + return null; // Skip self + } + + const checkingServiceExternalPorts = buildTemplate?.services?.[service]?.ports?.map((port) => { + return getExternalPort(port); + }) ?? []; + + checkingServiceExternalPorts.forEach((port) => { + if ([...currentConfiguredServiceExternalPorts, ...currentServiceExternalPorts].includes(port)) { + portConflicts.push({ + type: 'service', + name: serviceName, + issueType: 'portConflict', + message: `Port '${port}' is also used by '${service}'` + }); + } + }); + }); + + return portConflicts; +}; + +const getSetPortsByConfigName = ({ buildTemplate, buildOptions, serviceName, configOptions, portName }) => { + const serviceConfig = buildOptions?.configurations?.services?.[serviceName]; + const namedPort = Object.keys(configOptions?.labeledPorts ?? {}).find((ele) => { return configOptions.labeledPorts[ele] === portName; }); + const setPortValue = serviceConfig?.ports?.[namedPort] ?? ''; + const internalPort = setPortValue.split(':')[1]; + const externalPort = setPortValue.split(':')[0]; + + return { internalPort, externalPort }; +}; + +const checkNetworkConflicts = ({ buildTemplate, buildOptions, serviceName }) => { + const currentServiceNetworkMode = buildTemplate?.services?.[serviceName]?.network_mode ?? null; + const currentServiceNetworks = buildTemplate?.services?.[serviceName]?.networks ?? []; + + if (currentServiceNetworks.length > 0 && currentServiceNetworkMode) { + return { + type: 'service', + name: serviceName, + issueType: 'networkConflict', + message: `Networks configured: ${currentServiceNetworks.length} and 'Network Mode' is set. You can only choose to set networks or network mode.` + } + } + + return false; +}; + +const checkDependencyServices = ({ buildTemplate, buildOptions, serviceName, ignoreDependencies }) => { + const dependsOnServicesList = buildTemplate?.services?.[serviceName]?.depends_on ?? []; + const selectedServices = buildOptions?.selectedServices ?? []; + const missingServices = []; + + dependsOnServicesList.forEach((requiredService) => { + if (selectedServices.indexOf(requiredService) < 0) { + if ((ignoreDependencies ?? []).indexOf(requiredService) < 0) { + missingServices.push({ + type: 'service', + name: serviceName, + issueType: 'dependsOn', + message: `Service '${requiredService}' is missing from selected services` + }); + } + } + }); + + return missingServices; +}; + +module.exports = { + checkPortConflicts, + getSetPortsByConfigName, + checkNetworkConflicts, + checkDependencyServices +}; diff --git a/.internal/api/src/utils/commonCompileLogic.js b/.internal/api/src/utils/commonCompileLogic.js new file mode 100644 index 000000000..63911c048 --- /dev/null +++ b/.internal/api/src/utils/commonCompileLogic.js @@ -0,0 +1,296 @@ +const { + getExternalVolume, + getInternalVolume, + replaceExternalVolume, + getEnvironmentKey, + getEnvironmentValue, + replaceEnvironmentValue, +} = require('./dockerParse'); + +const { + generateFileOrFolderName, + generatePassword, + generateAlphanumeric, + generateRandomPort +} = require('./stringGenerate'); + +const { byName } = require('./interpolate'); + +const arraysEqual = (a, b) => { + if (!Array.isArray(a) || !Array.isArray(b)) { + return false; + } + if (a === b) { + return true; + } + + if (a.length !== b.length) { + return false; + } + + a.sort(); + b.sort(); + + for (let i = 0; i < a.length; ++i) { + if (a[i] !== b[i]) { + return false; + } + } + return true; +} + +const setCommonInterpolations = ({ stringList, inputString }) => { + let result = []; + if (Array.isArray(stringList)) { + result = stringList.map((iString) => { + return byName(iString, { + randomPassword: generatePassword(), + password: generatePassword(), + adminPassword: generatePassword(), + folderName: generateFileOrFolderName(), + compiledTime: new Date().getTime(), + randomAlphanumeric: generateAlphanumeric(), + randomPort: generateRandomPort() + }); + }); + } + + if (typeof inputString === 'string') { + return byName(inputString, { + randomPassword: generatePassword(), + password: generatePassword(), + adminPassword: generatePassword(), + folderName: generateFileOrFolderName(), + compiledTime: new Date().getTime(), + randomAlphanumeric: generateAlphanumeric(), + randomPort: generateRandomPort() + }); + } + + return result; +}; + +const setImageTag = ({ buildTemplate, buildOptions, serviceName }) => { + const serviceTemplate = buildTemplate?.services?.[serviceName]; + const serviceConfig = buildOptions?.configurations?.services?.[serviceName]; + const oldImage = serviceTemplate?.image; + + if (typeof(serviceTemplate?.image) === 'string' && (typeof(serviceConfig?.tag) === 'string')) { + serviceTemplate.image = byName(serviceTemplate.image, { + tag: serviceConfig.tag + }); + } + + return oldImage !== serviceTemplate?.image; +}; + +const setModifiedPorts = ({ buildTemplate, buildOptions, serviceName }) => { + const serviceTemplate = buildTemplate?.services?.[serviceName] ?? {}; + const serviceConfig = buildOptions?.configurations?.services?.[serviceName]; + + const modifiedPortList = Object.keys(serviceConfig?.ports ?? {}); + let updated = false; + + if (serviceTemplate.network_mode === 'host') { + delete buildTemplate?.services?.[serviceName]?.['ports']; + return true; + } + + for (let i = 0; i < modifiedPortList.length; i++) { + (serviceTemplate?.ports ?? []).forEach((port, index) => { + const eiPort = port.split('/')[0]; + if (eiPort === modifiedPortList[i]) { + if (serviceTemplate.ports[index] !== serviceConfig.ports[modifiedPortList[i]]) { + updated = true; + } + + serviceTemplate.ports[index] = serviceConfig.ports[modifiedPortList[i]]; + serviceTemplate.ports[index] = setCommonInterpolations({ inputString: serviceTemplate.ports[index] }); + } + }); + } + + return updated; +}; + +const setLoggingState = ({ buildTemplate, buildOptions, serviceName }) => { + const serviceTemplate = buildTemplate?.services?.[serviceName]; + const serviceConfig = buildOptions?.configurations?.services?.[serviceName]; + + const currentLogging = Object.keys(serviceTemplate?.logging ?? {}); + + if (serviceConfig?.loggingEnabled === false) { + if (serviceTemplate.logging) { + delete serviceTemplate?.logging; + return true; + } + return false; + } + + return Object.keys(serviceTemplate?.logging ?? {}).length !== currentLogging.length; +}; + +const setNetworkMode = ({ buildTemplate, buildOptions, serviceName }) => { + const serviceTemplate = buildTemplate?.services?.[serviceName] ?? {}; + const serviceConfig = buildOptions?.configurations?.services?.[serviceName]; + + const currentNetworkMode = serviceTemplate?.['network_mode']; + + if (serviceConfig?.networkMode) { + if ( + serviceTemplate['network_mode'] !== serviceConfig.networkMode + && serviceConfig.networkMode !== 'unchanged' + && serviceConfig.networkMode !== '' + ) { + serviceTemplate['network_mode'] = serviceConfig.networkMode; + } + + if (serviceConfig.networkMode === 'none') { + delete serviceTemplate['network_mode']; + } + + if (serviceTemplate.network_mode === 'host') { + delete buildTemplate?.services?.[serviceName]?.['ports']; + } + } + + return currentNetworkMode !== serviceTemplate['network_mode']; +}; + +const setNetworks = ({ buildTemplate, buildOptions, serviceName }) => { + const serviceTemplate = buildTemplate?.services?.[serviceName]; + const serviceConfig = buildOptions?.configurations?.services?.[serviceName]; + let updated = false; + + const originalNetworks = [ ...serviceTemplate?.networks ?? [] ]; + + const networksList = Object.keys(serviceConfig?.networks ?? {}); + if (networksList.length > 0) { + serviceTemplate.networks = []; + networksList.forEach((network) => { + if (serviceConfig.networks[network] === true) { + serviceTemplate.networks.push(network); + } + }); + } + + if (!arraysEqual(originalNetworks, serviceTemplate?.networks ?? [])) { + updated = true; + } + + return updated; +}; + +const setVolumes = ({ buildTemplate, buildOptions, serviceName }) => { + const serviceTemplate = buildTemplate?.services?.[serviceName]; + const serviceConfig = buildOptions?.configurations?.services?.[serviceName]; + let updated = false; + + if (Array.isArray(serviceConfig?.volumes ?? false)) { + serviceConfig.volumes.forEach((configVolume, volumeIndex) => { + const configInternalVolume = getInternalVolume(configVolume); + let found = false; + for (let i = 0; i < (serviceTemplate?.volumes ?? []).length; i++) { + const templateInternalVolume = getInternalVolume(serviceTemplate.volumes[i]); + + if (templateInternalVolume === configInternalVolume) { + const configExternalVolume = getExternalVolume(configVolume); + if (configExternalVolume === '') { + serviceTemplate.volumes.splice(i, 1); + } else { + serviceTemplate.volumes[i] = replaceExternalVolume(configVolume, configExternalVolume); + serviceTemplate.volumes[i] = setCommonInterpolations({ inputString: serviceTemplate.volumes[i] }); + } + updated = true; + found = true; + break; + } + } + + if (!found) { + serviceTemplate.volumes[i].push(configVolume); + } + }); + } + + return updated; +}; + +const setEnvironmentVariables = ({ buildTemplate, buildOptions, serviceName }) => { + const serviceTemplate = buildTemplate?.services?.[serviceName]; + const serviceConfig = buildOptions?.configurations?.services?.[serviceName]; + let updated = false; + + if (Array.isArray(serviceConfig?.environment ?? false)) { + serviceConfig.environment.forEach((configEnvironment, environmentIndex) => { + const configEnvironmentKey = getEnvironmentKey(configEnvironment); + let found = false; + for (let i = 0; i < (serviceTemplate?.environment ?? []).length; i++) { + const templateEnvironmentKey = getEnvironmentKey(serviceTemplate.environment[i]); + + if (templateEnvironmentKey === configEnvironmentKey) { + const newEnvironmentValue = getEnvironmentValue(configEnvironment); + if (newEnvironmentValue === '') { + serviceTemplate.environment.splice(i, 1); + } else { + serviceTemplate.environment[i] = replaceEnvironmentValue(configEnvironment, newEnvironmentValue); + serviceTemplate.environment[i] = setCommonInterpolations({ inputString: serviceTemplate.environment[i] }); + } + updated = true; + found = true; + break; + } + } + + if (!found) { + serviceTemplate.environment[i].push(configEnvironment); + } + }); + } + + return updated; +}; + +const setDevices = ({ buildTemplate, buildOptions, serviceName }) => { + const serviceTemplate = buildTemplate?.services?.[serviceName]; + const serviceConfig = buildOptions?.configurations?.services?.[serviceName]; + let updated = false; + + const currentDevices = serviceTemplate?.devices ?? {}; + + if (Array.isArray(serviceConfig?.devices ?? false)) { + serviceTemplate.devices = serviceConfig?.devices?.map((device) => { + if (device === '') { + return null; + } + return device; + }).filter((ele) => { + return ele !== null; + }); + updated = true; + } + + const newDevices = serviceTemplate?.devices ?? {}; + + if (currentDevices.length === 0) { + delete serviceTemplate.devices; + } + + if (arraysEqual(currentDevices, newDevices)) { + updated = false; + } + + return updated; +}; + +module.exports = { + setImageTag, + setModifiedPorts, + setLoggingState, + setNetworkMode, + setNetworks, + setVolumes, + setEnvironmentVariables, + setCommonInterpolations, + setDevices +}; diff --git a/.internal/api/src/utils/date.js b/.internal/api/src/utils/date.js new file mode 100644 index 000000000..cfa8d6dd2 --- /dev/null +++ b/.internal/api/src/utils/date.js @@ -0,0 +1,11 @@ +const { pad } = require('./pad'); + +const formatDate = (currentDate) => { + const useDate = Number.isFinite(Date.parse(currentDate)) ? currentDate : new Date(); + + return `${pad(useDate.getFullYear(), 4)}-${pad((useDate.getMonth() + 1), 2)}-${pad(useDate.getDate(), 2)}T${pad(useDate.getHours(), 2)}-${pad(useDate.getMinutes(), 2)}-${pad(useDate.getSeconds(), 2)}`; +}; + +module.exports = { + formatDate +}; diff --git a/.internal/api/src/utils/dockerParse.js b/.internal/api/src/utils/dockerParse.js new file mode 100644 index 000000000..b81e0fb1c --- /dev/null +++ b/.internal/api/src/utils/dockerParse.js @@ -0,0 +1,123 @@ +const getExternalPort = (intExtStr) => { + if (typeof(intExtStr) === 'string') { + const splitted = intExtStr.split(':'); + if (splitted.length === 2) { + return splitted[0]; + } + } + + return intExtStr; +}; + +const getInternalPort = (intExtStr) => { + if (typeof(intExtStr) === 'string') { + const portSection = intExtStr.split('/'); // So that /TCP or /UDP is not returned. + const splitted = portSection[0].split(':'); + if (splitted.length === 2) { + return splitted[1]; + } + } + + return intExtStr; +}; + +const getPortProtocol = (intExtProtStr) => { + if (typeof(intExtProtStr) === 'string') { + const splitted = intExtProtStr.split('/'); + if (splitted.length === 2) { + return splitted[1]; + } + } + + return intExtProtStr; +}; + +const replaceExternalPort = (intExtStr, newExtPort) => { + if (typeof(intExtStr) === 'string' && (typeof(newExtPort) === 'string' || typeof(newExtPort) === 'number')) { + const intAndProtLoc = intExtStr.indexOf(':'); + if (intAndProtLoc > 0 && intAndProtLoc < 6) { + const portsWithoutExt = intExtStr.substring(intAndProtLoc, intExtStr.length); + return `${newExtPort}${portsWithoutExt}`; + } + } + + return intExtStr; +}; + +const getExternalVolume = (intExtStr) => { + if (typeof(intExtStr) === 'string') { + const splitted = intExtStr.split(':'); + if (splitted.length === 2) { + return splitted[0]; + } + } + + return intExtStr; +}; + +const getInternalVolume = (intExtStr) => { + if (typeof(intExtStr) === 'string') { + const splitted = intExtStr.split(':'); + if (splitted.length === 2) { + return splitted[1]; + } + } + + return intExtStr; +}; + +const replaceExternalVolume = (intExtStr, newExtVolume) => { + if (typeof(intExtStr) === 'string' && (typeof(newExtVolume) === 'string')) { + const intLoc = intExtStr.indexOf(':'); + const volumesWithoutExt = intExtStr.substring(intLoc, intExtStr.length); + return `${newExtVolume}${volumesWithoutExt}`; + } + + return intExtStr; +}; + +const getEnvironmentKey = (EnvKVStr) => { + if (typeof(EnvKVStr) === 'string') { + const splitted = EnvKVStr.split('='); + if (splitted.length === 2) { + return splitted[0]; + } + } + + return EnvKVStr; +}; + +const getEnvironmentValue = (EnvKVStr) => { + if (typeof(EnvKVStr) === 'string') { + const splitted = EnvKVStr.split('='); + if (splitted.length === 2) { + return splitted[1]; + } + } + + return EnvKVStr; +}; + +const replaceEnvironmentValue = (EnvKVStr, newEnvValue) => { + if (typeof(EnvKVStr) === 'string' && (typeof(newEnvValue) === 'string')) { + const intLoc = EnvKVStr.indexOf('='); + const EnvWithoutValue = EnvKVStr.substring(0, intLoc); + return `${EnvWithoutValue}=${newEnvValue}`; + } + + return EnvKVStr; +}; + + +module.exports = { + getExternalPort, + getInternalPort, + replaceExternalPort, + getPortProtocol, + getExternalVolume, + getInternalVolume, + replaceExternalVolume, + getEnvironmentKey, + getEnvironmentValue, + replaceEnvironmentValue +}; \ No newline at end of file diff --git a/.internal/api/src/utils/fsUtils.js b/.internal/api/src/utils/fsUtils.js new file mode 100644 index 000000000..7d17308d4 --- /dev/null +++ b/.internal/api/src/utils/fsUtils.js @@ -0,0 +1,45 @@ +const fs = require('fs'); +const path = require('path'); + +const filterBadPathStrings = (inputPath) => { + let filteredPath = inputPath.split('..').join(''); + filteredPath = inputPath.split('~/').join(''); + + return filteredPath; +} + +const getDirectoryList = (directoryPath) => { + const filteredPath = filterBadPathStrings(directoryPath); + return fs.readdirSync(filteredPath, { withFileTypes: true }) + .filter((dirent) => { return dirent.isDirectory(); }) + .map((dirent) => { return dirent.name; }); +}; + +const getFileList = (directoryPath) => { + const filteredPath = filterBadPathStrings(directoryPath); + return fs.readdirSync(filteredPath, { withFileTypes: true }) + .filter((dirent) => { return !dirent.isDirectory(); }) + .map((dirent) => { return dirent.name; }); +}; + +const emptyDirectory = (directoryPath, ignoreList) => { + fs.readdir(directoryPath, (err, files) => { + if (err) throw err; + files.forEach((file) => { + if (Array.isArray(ignoreList) && ignoreList.indexOf(file) > -1) { + return; + } + + fs.unlink(path.join(directoryPath, file), err => { + if (err) throw err; + }); + }); + }); +}; + +module.exports = { + getDirectoryList, + getFileList, + emptyDirectory, + filterBadPathStrings +}; diff --git a/.internal/api/src/utils/interpolate.js b/.internal/api/src/utils/interpolate.js new file mode 100644 index 000000000..59bfd9eeb --- /dev/null +++ b/.internal/api/src/utils/interpolate.js @@ -0,0 +1,8 @@ +const byName = (formatString, replacements) => Object.keys(replacements).reduce( + (result, name) => result.replace(`{$${name}}`, replacements[name]), + formatString || '' +); + +module.exports = { + byName +}; \ No newline at end of file diff --git a/.internal/api/src/utils/networkUtils.js b/.internal/api/src/utils/networkUtils.js new file mode 100644 index 000000000..e9c14f4f0 --- /dev/null +++ b/.internal/api/src/utils/networkUtils.js @@ -0,0 +1,24 @@ +const getUniqueNetworkListFromServices = ({ services, logger }) => { + try { + const networkList = []; + Object.keys(services).forEach((serviceName) => { + if (Array.isArray(services[serviceName].networks)) { + services[serviceName].networks.forEach((networkName) => { + if (networkList.indexOf(networkName) < 0) { + networkList.push(networkName); + } + }); + } + }); + + return networkList; + } catch (err) { + logger.error(err); + console.trace(); + return []; + } +}; + +module.exports = { + getUniqueNetworkListFromServices +}; diff --git a/.internal/api/src/utils/pad.js b/.internal/api/src/utils/pad.js new file mode 100644 index 000000000..002508392 --- /dev/null +++ b/.internal/api/src/utils/pad.js @@ -0,0 +1,18 @@ +const pad = (str, size, withChar = "0", atEnd = false) => { + let s = str + ""; + if (atEnd) { + while (s.length < size) { + s = s + withChar; + } + } else { + while (s.length < size) { + s = withChar + s; + } + } + + return s; +}; + +module.exports = { + pad +}; diff --git a/.internal/api/src/utils/stringGenerate.js b/.internal/api/src/utils/stringGenerate.js new file mode 100644 index 000000000..6b5ad2fc1 --- /dev/null +++ b/.internal/api/src/utils/stringGenerate.js @@ -0,0 +1,62 @@ +const crypto = require('crypto'); + +const generatePassword = ({ chars, minLength, maxLength } = {}) => { + const useChars = chars ?? "0123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnopqrstuvwxyz_"; // Doesn't have O, I or l + const useMinLength = minLength ?? 16; + const useMaxLength = maxLength ?? 24; + + const passwordLength = Math.floor(Math.random() * (useMaxLength - useMinLength + 1) + useMinLength); + const generatedPassword = Array(passwordLength) + .fill('') + .map(() => useChars[Math.floor(Math.random() * (useChars.length + 1))]) + .join(''); + + return generatedPassword; +}; + +const generateFileOrFolderName = ({ chars, minLength, maxLength } = {}) => { + const useChars = chars ?? "0123456789abcdefghijkmnopqrstuvwxyz_"; + const firstChars = useChars.replace(/[^a-z]+/gi, '').split(''); // Only allow letters + + // - 1 since first letter is generated outside of map() + const useMinLength = (minLength ?? 4) - 1; + const useMaxLength = (maxLength ?? 16) - 1; + + const nameLength = Math.floor(Math.random() * (useMaxLength - useMinLength + 1) + useMinLength); + let generatedName = Array(nameLength) + .fill('') + .map(() => useChars[Math.floor(Math.random() * (useChars.length + 1))]) + .join(''); + + generatedName = firstChars[Math.floor(Math.random() * (firstChars.length + 1))] + generatedName; + + return generatedName; +}; + +const generateAlphanumeric = ({ chars, minLength, maxLength } = {}) => { + const useChars = chars ?? "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + const useMinLength = minLength ?? 8; + const useMaxLength = maxLength ?? 16; + + const stringLength = Math.floor(Math.random() * (useMaxLength - useMinLength + 1) + useMinLength); + const generatedString = Array(stringLength) + .fill('') + .map(() => useChars[Math.floor(Math.random() * (useChars.length + 1))]) + .join(''); + + return generatedString; +}; + +const generateRandomPort = ({ minPort, maxPort } = {}) => { + const useMinPort = minPort ?? 1001; + const useMaxPort = maxPort ?? 65534; + + return Math.floor(Math.random() * (useMaxPort - useMinPort + 1) + useMinPort) +}; + +module.exports = { + generatePassword, + generateFileOrFolderName, + generateAlphanumeric, + generateRandomPort +}; diff --git a/.internal/api/src/views/build.js b/.internal/api/src/views/build.js new file mode 100644 index 000000000..e75c7ad37 --- /dev/null +++ b/.internal/api/src/views/build.js @@ -0,0 +1,141 @@ +const BuildView = ({ server, settings, version, logger } = {}) => { + const BuildsController = require('../controllers/build'); + const retr = {}; + + const buildsController = BuildsController({ server, settings, version, logger }); + + retr.init = () => { + buildsController.init(); + }; + + retr.buildStack = (req, res, next) => { + try { + const { buildOptions } = req.body; + buildsController.buildStack({ buildOptions }).then((result) => { + return res.send(result); + }).catch((err) => { + logger.error(err); + return res.status(500).send({ + component: 'BuildView::buildStack', + message: 'Error getting yaml template', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + }); + } catch (err) { + logger.error(err); + logger.log(req.body); + return res.status(500).send({ + component: 'BuildView::buildStack', + message: 'Unhandled error', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }; + + retr.checkIssues = (req, res, next) => { + try { + const { buildOptions } = req.body; + buildsController.checkIssues({ buildOptions }).then((result) => { + return res.send(result); + }).catch((err) => { + logger.error(err); + return res.status(500).send({ + component: 'BuildView::checkIssues', + message: 'Error getting yaml template', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + }); + } catch (err) { + logger.error(err); + logger.log(req.body); + return res.status(500).send({ + component: 'BuildView::checkIssues', + message: 'Unhandled error', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }; + + retr.getPreviousBuildsList = (req, res, next) => { + const { buildTime, index, limit } = req.params; + try { + buildsController.getPreviousBuildsList({ host: req.headers.host, buildTime, index, limit }).then((result) => { + return res.send(result); + }).catch((err) => { + logger.error(err); + return res.status(500).send({ + component: 'BuildView::getPreviousBuildsList', + message: 'Error getting build', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + }); + } catch (err) { + logger.error(err); + logger.log(req.body); + return res.status(500).send({ + component: 'BuildView::getPreviousBuildsList', + message: 'Unhandled error', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }; + + retr.deletePreviousBuild = (req, res, next) => { + const { buildTime } = req.params; + try { + buildsController.deletePreviousBuild({ host: req.headers.host, buildTime }).then((result) => { + return res.send(result); + }).catch((err) => { + logger.error(err); + return res.status(500).send({ + component: 'BuildView::deletePreviousBuild', + message: 'Error getting build', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + }); + } catch (err) { + logger.error(err); + logger.log(req.body); + return res.status(500).send({ + component: 'BuildView::deletePreviousBuild', + message: 'Unhandled error', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }; + + retr.downloadPreviousBuildsList = (req, res, next) => { + const { buildTime, type } = req.params; + try { + buildsController.downloadPreviousBuildsList({ host: req.headers.host, buildTime, type }).then((result) => { + if (!result.filename || !result.fullPath) { + return res.status(500).send({ + component: 'BuildView::downloadPreviousBuildsList', + message: 'Error getting build' + }); + } + + return res.download(result.fullPath, result.filename); + }).catch((err) => { + logger.error(err); + return res.status(500).send({ + component: 'BuildView::downloadPreviousBuildsList', + message: 'Error getting build', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + }); + } catch (err) { + logger.error(err); + logger.log(req.body); + return res.status(500).send({ + component: 'BuildView::downloadPreviousBuildsList', + message: 'Unhandled error', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }; + + return retr; +}; + +module.exports = BuildView; diff --git a/.internal/api/src/views/configs.js b/.internal/api/src/views/configs.js new file mode 100644 index 000000000..22485c293 --- /dev/null +++ b/.internal/api/src/views/configs.js @@ -0,0 +1,179 @@ +const ConfigsView = ({ server, settings, version, logger } = {}) => { + const ConfigsController = require('../controllers/configs'); + const retr = {}; + + const configsController = ConfigsController({ server, settings, version, logger }); + + retr.init = () => { + configsController.init(); + }; + + retr.getConfigOptions = (req, res, next) => { + try { + const { serviceName } = req.params; + configsController.getConfigOptions({ serviceName }).then((result) => { + return res.send(result); + }).catch((err) => { + logger.error(err); + return res.status(500).send({ + component: 'ConfigsView::getConfigOptions', + message: 'Error getting config options', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + }); + } catch (err) { + logger.error(err); + logger.log(req.body); + return res.status(500).send({ + component: 'ConfigsView::getConfigOptions', + message: 'Unhandled error', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }; + + retr.getAllConfigOptions = (req, res, next) => { + try { + configsController.getAllConfigOptions().then((result) => { + return res.send(result); + }).catch((err) => { + logger.error(err); + return res.status(500).send({ + component: 'ConfigsView::getAllConfigOptions', + message: 'Error getting config options', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + }); + } catch (err) { + logger.error(err); + logger.log(req.body); + return res.status(500).send({ + component: 'ConfigsView::getAllConfigOptions', + message: 'Unhandled error', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }; + + retr.getHelp = (req, res, next) => { + try { + const { serviceName } = req.params; + configsController.getHelp({ serviceName }).then((result) => { + return res.send(result); + }).catch((err) => { + logger.error(err); + return res.status(500).send({ + component: 'ConfigsView::getHelp', + message: 'Error getting help', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + }); + } catch (err) { + logger.error(err); + logger.log(req.body); + return res.status(500).send({ + component: 'ConfigsView::getHelp', + message: 'Unhandled error', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }; + + retr.getAllHelp = (req, res, next) => { + try { + configsController.getAllHelp().then((result) => { + return res.send(result); + }).catch((err) => { + logger.error(err); + return res.status(500).send({ + component: 'ConfigsView::getAllHelp', + message: 'Error getting help', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + }); + } catch (err) { + logger.error(err); + logger.log(req.body); + return res.status(500).send({ + component: 'ConfigsView::getAllHelp', + message: 'Unhandled error', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }; + + retr.getScripts = (req, res, next) => { + try { + const { serviceName, scriptName } = req.params; + configsController.getScripts({ serviceName, scriptName }).then((result) => { + return res.send(result); + }).catch((err) => { + logger.error(err); + return res.status(500).send({ + component: 'ConfigsView::getScripts', + message: 'Error getting scripts', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + }); + } catch (err) { + logger.error(err); + logger.log(req.body); + return res.status(500).send({ + component: 'ConfigsView::getScripts', + message: 'Unhandled error', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }; + + retr.getMeta = (req, res, next) => { + try { + const { serviceName } = req.params; + configsController.getMeta({ serviceName }).then((result) => { + return res.send(result); + }).catch((err) => { + logger.error(err); + return res.status(500).send({ + component: 'ConfigsView::getMeta', + message: 'Error getting help', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + }); + } catch (err) { + logger.error(err); + logger.log(req.body); + return res.status(500).send({ + component: 'ConfigsView::getMeta', + message: 'Unhandled error', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }; + + retr.getAllMeta = (req, res, next) => { + try { + configsController.getAllMeta().then((result) => { + return res.send(result); + }).catch((err) => { + logger.error(err); + return res.status(500).send({ + component: 'ConfigsView::getMeta', + message: 'Error getting help', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + }); + } catch (err) { + logger.error(err); + logger.log(req.body); + return res.status(500).send({ + component: 'ConfigsView::getMeta', + message: 'Unhandled error', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }; + + return retr; +}; + +module.exports = ConfigsView; diff --git a/.internal/api/src/views/health.js b/.internal/api/src/views/health.js new file mode 100644 index 000000000..0cc5c71a1 --- /dev/null +++ b/.internal/api/src/views/health.js @@ -0,0 +1,32 @@ +const HealthView = ({ server, settings, version, logger } = {}) => { + const HealthController = require('../controllers/health'); + const retr = {}; + + const healthController = HealthController({ server, settings, version, logger }); + + retr.init = () => { + healthController.init(); + }; + + retr.health = (req, res, next) => { + healthController.healthCheck().then((result) => { + return res.send(result); + }).catch((err) => { + logger.error(err); + return res.status(500).send(result); + }); + }; + + retr.healthCheckNoLog = (req, res, next) => { + healthController.healthCheckNoLog().then((result) => { + return res.send(result); + }).catch((err) => { + logger.error(err); + return res.status(500).send(result); + }); + }; + + return retr; +}; + +module.exports = HealthView; diff --git a/.internal/api/src/views/templates.js b/.internal/api/src/views/templates.js new file mode 100644 index 000000000..b2c686d57 --- /dev/null +++ b/.internal/api/src/views/templates.js @@ -0,0 +1,207 @@ +const TemplatesView = ({ server, settings, version, logger } = {}) => { + const TemplatesController = require('../controllers/templates'); + const retr = {}; + + const templatesController = TemplatesController({ server, settings, version, logger }); + + retr.init = () => { + templatesController.init(); + }; + + retr.getServiceTemplateAsYaml = (req, res, next) => { + const { templateName } = req.params; + templatesController.getServiceTemplateAsYaml(templateName).then((result) => { + return res.send(result); + }).catch((err) => { + logger.error(err); + return res.status(500).send({ + component: 'TemplatesView::getServiceTemplateAsYaml', + message: 'Error getting yaml template', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + }); + }; + + retr.getServiceTemplateAsJson = (req, res, next) => { + const { templateName } = req.params; + templatesController.getServiceTemplateAsJson(templateName).then((result) => { + return res.send(result); + }).catch((err) => { + logger.error(err); + return res.status(500).send({ + component: 'TemplatesView::getServiceTemplateAsJson', + message: 'Error getting json template', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + }); + }; + + retr.getAllServiceTemplatesAsYaml = (req, res, next) => { + templatesController.getAllServiceTemplatesAsYaml().then((result) => { + return res.send(result); + }).catch((err) => { + logger.error(err); + return res.status(500).send({ + component: 'TemplatesView::getAllServiceTemplatesAsYaml', + message: 'Error getting yaml templates', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + }); + }; + + retr.getAllServiceTemplatesAsJson = (req, res, next) => { + templatesController.getAllServiceTemplatesAsJson().then((result) => { + return res.send(result); + }).catch((err) => { + logger.error(err); + return res.status(500).send({ + component: 'TemplatesView::getAllServiceTemplatesAsJson', + message: 'Error getting json templates', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + }); + }; + + retr.getAllServiceTemplatesAsList = (req, res, next) => { + templatesController.getAllServiceTemplatesAsList().then((result) => { + return res.send(result); + }).catch((err) => { + logger.error(err); + return res.status(500).send({ + component: 'TemplatesView::getAllServiceTemplatesAsList', + message: 'Error getting templates list', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + }); + }; + + retr.getNetworkTemplateAsYaml = (req, res, next) => { + const { templateName } = req.params; + templatesController.getNetworkTemplateAsYaml(templateName).then((result) => { + return res.send(result); + }).catch((err) => { + logger.error(err); + return res.status(500).send({ + component: 'TemplatesView::getNetworkTemplateAsYaml', + message: 'Error getting yaml template', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + }); + }; + + retr.getNetworkTemplateAsJson = (req, res, next) => { + const { templateName } = req.params; + templatesController.getNetworkTemplateAsJson(templateName).then((result) => { + return res.send(result); + }).catch((err) => { + logger.error(err); + return res.status(500).send({ + component: 'TemplatesView::getNetworkTemplateAsJson', + message: 'Error getting json template', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + }); + }; + + retr.getAllNetworkTemplatesAsYaml = (req, res, next) => { + templatesController.getAllNetworkTemplatesAsYaml().then((result) => { + return res.send(result); + }).catch((err) => { + logger.error(err); + return res.status(500).send({ + component: 'TemplatesView::getAllNetworkTemplatesAsYaml', + message: 'Error getting yaml templates', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + }); + }; + + retr.getAllNetworkTemplatesAsJson = (req, res, next) => { + templatesController.getAllNetworkTemplatesAsJson().then((result) => { + return res.send(result); + }).catch((err) => { + logger.error(err); + return res.status(500).send({ + component: 'TemplatesView::getAllNetworkTemplatesAsJson', + message: 'Error getting json templates', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + }); + }; + + retr.getAllNetworkTemplatesAsList = (req, res, next) => { + templatesController.getAllNetworkTemplatesAsList().then((result) => { + return res.send(result); + }).catch((err) => { + logger.error(err); + return res.status(500).send({ + component: 'TemplatesView::getAllNetworkTemplatesAsList', + message: 'Error getting templates list', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + }); + }; + + retr.getServiceTemplateFile = (req, res, next) => { + const { templateName, filename } = req.params; + templatesController.getServiceTemplateFile({ templateName, filename }).then((result) => { + return res.download(result); + }).catch((err) => { + logger.error(err); + return res.status(500).send({ + component: 'TemplatesView::getServiceTemplateFile', + message: `Error getting template '${templateName}' file '${filename}'`, + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + }); + }; + + retr.getScriptTemplate = (req, res, next) => { + const { scriptName } = req.params; + const { options } = req.body; + templatesController.getScriptTemplateFile({ scriptName, options, req }).then((result) => { + return res.send(result.data); + }).catch((err) => { + logger.error(err); + return res.status(500).send({ + component: 'TemplatesView::getScriptTemplate', + message: `Error getting script template '${scriptName}''`, + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + }); + }; + + retr.downloadScriptTemplate = (req, res, next) => { + const { scriptName } = req.params; + const { options, contentTypeRequest } = req.body; + const allowedContentTypes = [ + 'text/plain', + 'text/csv', + 'text/html', + 'application/json' + ]; + + templatesController.getScriptTemplateFile({ scriptName, options, req }).then((result) => { + const buildFilename = (typeof result.filename !== 'string' || !result.filename) ? scriptName : result.filename; + + const useContentType = allowedContentTypes.includes(contentTypeRequest) ? contentTypeRequest : 'text/plain'; + res.set({ + 'Content-Disposition': `attachment; filename="${buildFilename}"`, + 'Content-Type': useContentType, + }); + + return res.send(result.data); + }).catch((err) => { + logger.error(err); + return res.status(500).send({ + component: 'TemplatesView::downloadScriptTemplate', + message: `Error getting script template '${scriptName}''`, + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + }); + }; + + return retr; +}; + +module.exports = TemplatesView; diff --git a/.internal/api/start.sh b/.internal/api/start.sh new file mode 100644 index 000000000..e726b7941 --- /dev/null +++ b/.internal/api/start.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +echo "API bootstrap script startup" +echo "IOTstack Template Path: " +echo "IOTstack Template Services: $(ls /usr/iotstack_api/templates/services/ | wc -l)" +echo "IOTstack Template Networks: $(ls /usr/iotstack_api/templates/networks/ | wc -l)" +echo "IOTstack Template Scripts: $(ls /usr/iotstack_api/templates/scripts/ | wc -l)" + +if [[ $IOTENV == "development" || "$1" = "development" ]]; then + echo "Starting API in development mode" + npm run dev +else + npm start +fi diff --git a/.templates/mosquitto/iotstack_defaults/pwfile/pwfile b/.internal/api/static/.gitkeep similarity index 100% rename from .templates/mosquitto/iotstack_defaults/pwfile/pwfile rename to .internal/api/static/.gitkeep diff --git a/.internal/ctrl_api.sh b/.internal/ctrl_api.sh new file mode 100644 index 000000000..60ed9258f --- /dev/null +++ b/.internal/ctrl_api.sh @@ -0,0 +1,157 @@ +#!/bin/bash + +CPWD=$(pwd) + +if [[ ! "$(basename $CPWD)" == ".internal" ]]; then + cd .internal/ +fi + +source ./meta.sh +DNAME=iostack_api +FULL_NAME="$DNAME:$VERSION" + +RUN_MODE="production" + +if [ "$1" == "stop" ]; then + echo "docker stop \$(docker images -q --format \"{{.Repository}}:{{.Tag}} {{.ID}}\" | grep \"$DNAME\" | cut -d ' ' -f2)" + docker stop $(docker images -q --format "{{.Repository}}:{{.Tag}} {{.ID}}" | grep "$DNAME" | cut -d ' ' -f2) 2> /dev/null + echo "docker stop \$(docker ps -q --format \"{{.Image}} {{.ID}}\" | grep \"$DNAME\" | cut -d ' ' -f2)" + docker stop $(docker ps -q --format "{{.Image}} {{.ID}}" | grep "$DNAME" | cut -d ' ' -f2) 2> /dev/null + echo "docker stop \$(docker ps -q --format \"{{.ID}} {{.Ports}}\" | grep \"$API_PORT\" | cut -d ' ' -f1)" + docker stop $(docker ps -q --format "{{.ID}} {{.Ports}}" | grep "$API_PORT" | cut -d ' ' -f1) 2> /dev/null +else + if [[ $IOTENV == "development" || "$1" == "development" ]]; then + RUN_MODE="development" + echo "[Development: '$FULL_NAME'] Stopping container:" + echo "docker stop \$(docker images -q --format \"{{.Repository}}:{{.Tag}} {{.ID}}\" | grep \"$DNAME\" | cut -d ' ' -f2)" + docker stop $(docker images -q --format "{{.Repository}}:{{.Tag}} {{.ID}}" | grep "$DNAME" | cut -d ' ' -f2) + echo "docker stop \$(docker ps -q --format \"{{.Image}} {{.ID}}\" | grep \"$DNAME\" | cut -d ' ' -f2)" + docker stop $(docker ps -q --format "{{.Image}} {{.ID}}" | grep "$DNAME" | cut -d ' ' -f2) + echo "docker stop \$(docker ps -q --format \"{{.ID}} {{.Ports}}\" | grep $API_PORT | cut -d ' ' -f1)" + docker stop $(docker ps -q --format "{{.ID}} {{.Ports}}" | grep "$API_PORT" | cut -d ' ' -f1) + echo "docker rmi \$FULL_NAME --force" + docker rmi $FULL_NAME --force + echo "" + echo "Rebuilding container:" + echo "docker build --no-cache -t $FULL_NAME -f ./api.Dockerfile ." + docker pull node:14 # Docker occasionally fails to pull image when building when it is not cached. + docker build --no-cache -t $FULL_NAME -f ./api.Dockerfile . + else + if [[ "$(docker images -q $FULL_NAME 2> /dev/null)" == "" ]]; then + echo "Building '$FULL_NAME'" + echo "This may take 5 to 10 minutes." + docker pull node:14 # Docker occasionally fails to pull image when building when it is not cached. + echo "" + docker build --quiet -t $FULL_NAME -f ./api.Dockerfile . + DBR=$? + if [[ ! $DBR -eq 0 ]]; then + echo "" + echo "-----------------------------------" + echo "" + echo "Docker build encountered an error when building '$FULL_NAME'." + echo "If this error is stating that there's no permission to read a file or directory then change the permissions or owner to one that the '$HOSTUSER' user can read." + echo "" + echo "Examples:" + echo " Update owner:" + echo " sudo chown -R $HOSTUSER $IOTSTACKPWD/.internal/" + echo "" + echo " Update permissions:" + echo " sudo chmod -R 755 $IOTSTACKPWD/.internal/" + echo "" + echo " Checking owner and permissions:" + echo " ls -ahl $IOTSTACKPWD/.internal/" + echo "" + echo "-----------------------------------" + echo "" + sleep 1 + exit 2 + fi + else + echo "Build for '$FULL_NAME' already exists. Skipping..." + fi + fi + + CORS_LIST="" + for sepspace in "$(hostname --all-ip-addresses)"; do + sepspace="$(echo $sepspace | xargs)" + CORS_LIST="$CORS_LIST $sepspace:$API_PORT " + done + + if ! docker ps --format '{{.Image}}' | grep -w $FULL_NAME &> /dev/null; then + if [[ $IOTENV == "development" || "$1" == "development" ]]; then + echo "Starting in development watch mode the IOTstack API Server on port: $API_PORT" + docker run \ + -p $API_PORT:$API_PORT \ + --mount type=bind,source="$IOTSTACKPWD"/.internal/templates,target=/usr/iotstack_api/templates,readonly \ + --mount type=bind,source="$IOTSTACKPWD"/.internal/saved_builds,target=/usr/iotstack_api/builds \ + --mount type=bind,source="$IOTSTACKPWD"/.internal/.ssh/id_rsa,target=/root/.ssh/id_rsa,readonly \ + --mount type=bind,source="$IOTSTACKPWD"/.internal/api,target=/usr/iotstack_api \ + --add-host=host.docker.internal:host-gateway \ + -e IOTENV="$RUN_MODE" \ + -e API_PORT="$API_PORT" \ + -e WUI_PORT=$WUI_PORT \ + -e API_INTERFACE="$API_INTERFACE" \ + -e HOSTUSER="$HOSTUSER" \ + -e IOTSTACKPWD="$IOTSTACKPWD" \ + -e CORS="$CORS_LIST" \ + -e HOSTSSH_ADDR="$HOSTSSH_ADDR" \ + -e HOSTSSH_PORT="$HOSTSSH_PORT" \ + --restart unless-stopped \ + $FULL_NAME + + # --net=host \ + # -p $API_PORT:$API_PORT \ + else + echo "Starting IOTstack API Server on port: $API_PORT" + docker run -d \ + --mount type=bind,source="$IOTSTACKPWD"/.internal/templates,target=/usr/iotstack_api/templates,readonly \ + --mount type=bind,source="$IOTSTACKPWD"/.internal/saved_builds,target=/usr/iotstack_api/builds \ + --mount type=bind,source="$IOTSTACKPWD"/.internal/.ssh/id_rsa,target=/root/.ssh/id_rsa,readonly \ + --net=host \ + --add-host=host.docker.internal:host-gateway \ + -e IOTENV="$RUN_MODE" \ + -e API_PORT="$API_PORT" \ + -e API_INTERFACE="$API_INTERFACE" \ + -e HOSTUSER="$HOSTUSER" \ + -e IOTSTACKPWD="$IOTSTACKPWD" \ + -e CORS="$CORS_LIST" \ + -e HOSTSSH_ADDR="$HOSTSSH_ADDR" \ + -e HOSTSSH_PORT="$HOSTSSH_PORT" \ + --restart unless-stopped \ + $FULL_NAME + + # docker run -p $API_PORT:$API_PORT \ + # --mount type=bind,source="$IOTSTACKPWD"/.internal/templates,target=/usr/iotstack_api/templates,readonly \ + # --mount type=bind,source="$IOTSTACKPWD"/.internal/saved_builds,target=/usr/iotstack_api/builds,readonly \ + # --mount type=bind,source="$IOTSTACKPWD"/.internal/.ssh/id_rsa,target=/root/.ssh/id_rsa,readonly \ + # -e API_PORT="$API_PORT" \ + # -e API_INTERFACE="$API_INTERFACE" \ + # -e cors="yourLanIpHere:$WUI_PORT" \ + # -e HOSTUSER="$HOSTUSER" \ + # -e IOTSTACKPWD="$IOTSTACKPWD" \ + # -e CORS="$(CORS_LIST)" \ + # -e HOSTSSH_ADDR="$HOSTSSH_ADDR" \ + # -e HOSTSSH_PORT="$HOSTSSH_PORT" \ + # --restart unless-stopped \ + # $FULL_NAME + + # docker run -p $API_PORT:$API_PORT \ + # --mount type=bind,source="$IOTSTACKPWD"/.internal/templates,target=/usr/iotstack_api/templates,readonly \ + # --mount type=bind,source="$IOTSTACKPWD"/.internal/saved_builds,target=/usr/iotstack_api/builds,readonly \ + # --mount type=bind,source="$IOTSTACKPWD"/.internal/.ssh/id_rsa,target=/root/.ssh/id_rsa,readonly \ + # -e API_PORT="$API_PORT" \ + # -e API_INTERFACE="$API_INTERFACE" \ + # -e HOSTUSER="$HOSTUSER" \ + # -e IOTSTACKPWD="$IOTSTACKPWD" \ + # -e CORS="$(CORS_LIST)" \ + # -e HOSTSSH_ADDR="$HOSTSSH_ADDR" \ + # -e HOSTSSH_PORT="$HOSTSSH_PORT" \ + # --restart unless-stopped \ + # -it $FULL_NAME /bin/bash + fi + else + echo "IOTstack API Server is running. Check port: $API_PORT or run 'docker ps'" + fi +fi + +cd $CPWD diff --git a/.internal/ctrl_pycli.sh b/.internal/ctrl_pycli.sh new file mode 100644 index 000000000..695b9f615 --- /dev/null +++ b/.internal/ctrl_pycli.sh @@ -0,0 +1,121 @@ +#!/bin/bash + +CPWD=$(pwd) + +if [[ ! "$(basename $CPWD)" == ".internal" ]]; then + cd .internal/ +fi + +source ./meta.sh +DNAME=iostack_pycli +FULL_NAME="$DNAME:$VERSION" + +RUN_MODE="production" + +if [ "$1" == "stop" ]; then + echo "docker stop \$(docker images -q --format \"{{.Repository}}:{{.Tag}} {{.ID}}\" | grep \"$DNAME\" | cut -d ' ' -f2)" + docker stop $(docker images -q --format "{{.Repository}}:{{.Tag}} {{.ID}}" | grep "$DNAME" | cut -d ' ' -f2) 2> /dev/null + echo "docker stop \$(docker ps -q --format \"{{.Image}} {{.ID}}\" | grep \"$DNAME\" | cut -d ' ' -f2)" + docker stop $(docker ps -q --format "{{.Image}} {{.ID}}" | grep "$DNAME" | cut -d ' ' -f2) 2> /dev/null +else + if [[ $IOTENV == "development" || "$1" = "development" ]]; then + RUN_MODE="development" + echo "[Development: '$FULL_NAME'] Stopping container:" + echo "docker stop $(docker images -q --format "{{.Repository}}:{{.Tag}}" | grep "$DNAME") || docker rmi $FULL_NAME --force" + docker stop $(docker images -q --format "{{.Repository}}:{{.Tag}}" | grep "$DNAME") 2> /dev/null || docker rmi $FULL_NAME --force 2> /dev/null + echo "docker stop $(docker ps -q --format "{{.Image}} {{.ID}}" | grep "$DNAME" | cut -d ' ' -f2) || docker rmi $FULL_NAME --force 2> /dev/null" + docker stop $(docker ps -q --format "{{.Image}} {{.ID}}" | grep "$DNAME" | cut -d ' ' -f2) 2> /dev/null || docker rmi $FULL_NAME --force 2> /dev/null + echo "" + echo "Rebuilding container:" + echo "docker build --no-cache -t $FULL_NAME -f ./pycli.Dockerfile ." + docker pull python:3 # Docker occasionally fails to pull image when building when it is not cached. + docker build --no-cache -t $FULL_NAME -f ./pycli.Dockerfile . + echo "" + else + if [[ "$(docker images -q $FULL_NAME 2> /dev/null)" == "" ]]; then + echo "Building '$FULL_NAME'" + echo "This may take 5 to 10 minutes." + docker pull python:3 # Docker occasionally fails to pull image when building when it is not cached. + echo "" + docker build --quiet -t $FULL_NAME -f ./pycli.Dockerfile . + DBR=$? + if [[ ! $DBR -eq 0 ]]; then + echo "" + echo "-----------------------------------" + echo "" + echo "Docker build encountered an error when building '$FULL_NAME'." + echo "If this error is stating that there's no permission to read a file or directory then change the permissions or owner to one that the '$HOSTUSER' user can read." + echo "" + echo "Examples:" + echo " Update owner:" + echo " sudo chown -R $HOSTUSER $IOTSTACKPWD/.internal/" + echo "" + echo " Update permissions:" + echo " sudo chmod -R 755 $IOTSTACKPWD/.internal/" + echo "" + echo " Checking owner and permissions:" + echo " ls -ahl $IOTSTACKPWD/.internal/" + echo "" + echo "-----------------------------------" + echo "" + sleep 1 + exit 2 + fi + else + echo "Build for '$FULL_NAME' already exists. Skipping..." + fi + fi + + if ! docker ps --format '{{.Image}}' | grep -w $FULL_NAME &> /dev/null; then + echo "Starting IOTstack PyCLI instance" + + docker run \ + --mount type=bind,source="$IOTSTACKPWD"/.internal/saved_builds,target=/usr/iotstack_api/builds,readonly \ + --mount type=bind,source="$IOTSTACKPWD"/.internal/.ssh/id_rsa,target=/root/.ssh/id_rsa,readonly \ + --net=host \ + --add-host=host.docker.internal:host-gateway \ + -e IOTENV="$RUN_MODE" \ + -e HOSTUSER="$HOSTUSER" \ + -e IOTSTACKPWD="$IOTSTACKPWD" \ + -e API_ADDR="$PYCLI_CON_API" \ + -e HOST_CON_API="$PYCLI_HOST_CON_API" \ + -e WUI_ADDR="$PYCLI_CON_WUI" \ + -e HOSTSSH_ADDR="$HOSTSSH_ADDR" \ + -e HOSTSSH_PORT="$HOSTSSH_PORT" \ + --restart no \ + -it $FULL_NAME + # docker run -d \ + # --mount type=bind,source="$IOTSTACKPWD"/.internal/saved_builds,target=/usr/iotstack_api/builds \ + # --mount type=bind,source="$IOTSTACKPWD"/.internal/.ssh,target=/root/.ssh,readonly \ + # -e HOSTUSER="$HOSTUSER" \ + # -e IOTSTACKPWD="$IOTSTACKPWD" \ + # --restart no \ + # $FULL_NAME + + # docker run \ + # --mount type=bind,source="$IOTSTACKPWD"/.internal/saved_builds,target=/usr/iotstack_api/builds,readonly \ + # --mount type=bind,source="$IOTSTACKPWD"/.internal/.ssh,target=/root/.ssh,readonly \ + # -e cors="yourLanIpHere:32777" \ + # -e HOSTUSER="$HOSTUSER" \ + # -e IOTSTACKPWD="$IOTSTACKPWD" \ + # --restart no \ + # $FULL_NAME + + # docker run \ + # --mount type=bind,source="$IOTSTACKPWD"/.internal/saved_builds,target=/usr/iotstack_api/builds,readonly \ + # --mount type=bind,source="$IOTSTACKPWD"/.internal/.ssh,target=/root/.ssh,readonly \ + # -e IOTENV="$RUN_MODE" \ + # -e HOSTUSER="$HOSTUSER" \ + # -e IOTSTACKPWD="$IOTSTACKPWD" \ + # -e API_ADDR="$PYCLI_CON_API" \ + # -e WUI_ADDR="$PYCLI_CON_WUI" \ + # -e HOSTSSH_ADDR="$HOSTSSH_ADDR" \ + # -e HOSTSSH_PORT="$HOSTSSH_PORT" \ + # --restart no \ + # -it $FULL_NAME /bin/bash + else + echo "IOTstack CLI is running. Check with 'docker ps'." + fi +fi + +cd $CPWD diff --git a/.internal/ctrl_wui.sh b/.internal/ctrl_wui.sh new file mode 100644 index 000000000..be3e327a5 --- /dev/null +++ b/.internal/ctrl_wui.sh @@ -0,0 +1,95 @@ +#!/bin/bash + +CPWD=$(pwd) + +if [[ ! "$(basename $CPWD)" == ".internal" ]]; then + cd .internal/ +fi + +source ./meta.sh +DNAME=iostack_wui +FULL_NAME="$DNAME:$VERSION" + +RUN_MODE="production" + +if [ "$1" == "stop" ]; then + echo "docker stop \$(docker images -q --format \"{{.Repository}}:{{.Tag}} {{.ID}}\" | grep \"$DNAME\" | cut -d ' ' -f2)" + docker stop $(docker images -q --format "{{.Repository}}:{{.Tag}} {{.ID}}" | grep "$DNAME" | cut -d ' ' -f2) 2> /dev/null + echo "docker stop \$(docker ps -q --format \"{{.Image}} {{.ID}}\" | grep \"$DNAME\" | cut -d ' ' -f2)" + docker stop $(docker ps -q --format "{{.Image}} {{.ID}}" | grep "$DNAME" | cut -d ' ' -f2) 2> /dev/null + echo "docker stop \$(docker ps -q --format \"{{.ID}} {{.Ports}}\" | grep \"$WUI_PORT\" | cut -d ' ' -f1)" + docker stop $(docker ps -q --format "{{.ID}} {{.Ports}}" | grep "$WUI_PORT" | cut -d ' ' -f1) 2> /dev/null +else + if [[ $IOTENV == "development" || "$1" == "development" ]]; then + RUN_MODE="development" + echo "[Development: '$FULL_NAME'] Stopping container:" + echo "docker stop \$(docker images -q --format \"{{.Repository}}:{{.Tag}} {{.ID}}\" | grep \"$DNAME\" | cut -d ' ' -f2)" + docker stop $(docker images -q --format "{{.Repository}}:{{.Tag}} {{.ID}}" | grep "$DNAME" | cut -d ' ' -f2) 2> /dev/null || docker rmi $FULL_NAME --force 2> /dev/null + echo "docker stop \$(docker ps -q --format \"{{.Image}} {{.ID}}\" | grep \"$DNAME\" | cut -d ' ' -f2) || docker rmi $FULL_NAME --force 2> /dev/null" + docker stop $(docker ps -q --format "{{.Image}} {{.ID}}" | grep "$DNAME" | cut -d ' ' -f2) 2> /dev/null || docker rmi $FULL_NAME --force 2> /dev/null + echo "docker stop \$(docker ps -q --format "{{.ID}} {{.Ports}}" | grep $WUI_PORT | cut -d ' ' -f1) 2> /dev/null" + docker stop $(docker ps -q --format "{{.ID}} {{.Ports}}" | grep "$WUI_PORT" | cut -d ' ' -f1) 2> /dev/null + echo "" + echo "Rebuilding container:" + echo "docker build --no-cache -t $FULL_NAME -f ./wui/wui.dev.Dockerfile ." + docker pull node:14 # Docker occasionally fails to pull image when building when it is not cached. + docker build --no-cache -t $FULL_NAME -f ./wui/wui.dev.Dockerfile . + else + if [[ "$(docker images -q $FULL_NAME 2> /dev/null)" == "" ]]; then + echo "React WUI production build not found." + echo "Building '$FULL_NAME'" + echo "This may take 5 to 10 minutes." + docker pull node:14 # Docker occasionally fails to pull image when building when it is not cached. + echo "" + docker build --quiet -t $FULL_NAME -f ./wui.Dockerfile . + DBR=$? + if [[ ! $DBR -eq 0 ]]; then + echo "" + echo "-----------------------------------" + echo "" + echo "Docker build encountered an error when building '$FULL_NAME'." + echo "If this error is stating that there's no permission to read a file or directory then change the permissions or owner to one that the '$HOSTUSER' user can read." + echo "" + echo "Examples:" + echo " Update owner:" + echo " sudo chown -R $HOSTUSER $IOTSTACKPWD/.internal/" + echo "" + echo " Update permissions:" + echo " sudo chmod -R 755 $IOTSTACKPWD/.internal/" + echo "" + echo " Checking owner and permissions:" + echo " ls -ahl $IOTSTACKPWD/.internal/" + echo "" + echo "-----------------------------------" + echo "" + sleep 1 + exit 2 + fi + else + echo "Build for '$FULL_NAME' already exists. Skipping..." + fi + fi + + if ! docker ps --format '{{.Image}}' | grep -w $FULL_NAME &> /dev/null; then + if [[ $IOTENV == "development" || "$1" == "development" ]]; then + echo "Starting in development watch mode the IOTstack WUI Server" + docker run -p $WUI_PORT:$WUI_PORT \ + -e IOTENV="$RUN_MODE" \ + -e PORT=$WUI_PORT \ + --mount type=bind,source="$IOTSTACKPWD"/.internal/wui,target=/usr/iotstack_wui \ + --restart unless-stopped $FULL_NAME + else + echo "Starting IOTstack WUI Server" + docker run -d -p $WUI_PORT:$WUI_PORT -e PORT=$WUI_PORT --restart unless-stopped $FULL_NAME + fi + else + echo "IOTstack WUI Server is running" + fi + + # docker run -d -p $WUI_PORT:$WUI_PORT $FULL_NAME + # docker run -p $WUI_PORT:$WUI_PORT -e PORT=$WUI_PORT $FULL_NAME + + # docker run -p $WUI_PORT:$WUI_PORT -it $FULL_NAME /bin/bash +fi + +cd $CPWD diff --git a/.internal/docker_menu.sh b/.internal/docker_menu.sh new file mode 100644 index 000000000..b5602c327 --- /dev/null +++ b/.internal/docker_menu.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +CPWD=$(pwd) + +if [[ ! "$(basename $CPWD)" == ".internal" ]]; then + cd .internal/ +fi + +if [ "$1" == "stop" ]; then + bash ./ctrl_api.sh stop + bash ./ctrl_wui.sh stop + bash ./ctrl_pycli.sh stop +else + bash ./ctrl_api.sh + bash ./ctrl_wui.sh + bash ./ctrl_pycli.sh +fi + +cd $CPWD diff --git a/.internal/example_build_scripts/example1.sh b/.internal/example_build_scripts/example1.sh new file mode 100644 index 000000000..39b8b73e7 --- /dev/null +++ b/.internal/example_build_scripts/example1.sh @@ -0,0 +1,60 @@ +#!/bin/bash + +# Before running, ensure install.sh has run successfully, and the menu containers (specifically the API container) have been built and are running. +# They can be started by running ./menu.sh + +echo "This will generate a build and echo out the command that's required to install it." +echo "Checkout the 'IOTstack.postman_collection.json' file for all API calls." +echo "" +echo "Services: mosquitto, nodered, influxdb, grafana" +read -n 1 -s -r -p "Press any key to continue" +echo "" +echo "" + +API_HOST=localhost:32128 + +HAS_ERROR="false" + +# Generate build +CBRES=$(curl -s \ + -d '{"buildOptions":{"selectedServices":["mosquitto","nodered","influxdb","grafana"],"configurations":{"services":{"nodered":{"devices":["/dev/ttyAMA0:/dev/ttyAMA0","/dev/vcio:/dev/vcio","/dev/gpiomem:/dev/gpiomem"],"addonsList":["node-red-node-pi-gpiod","node-red-contrib-influxdb","node-red-contrib-boolean-logic","node-red-node-rbe","node-red-dashboard"],"loggingEnabled":true,"networks":{"iotstack_nw":true},"networkMode":"unchanged"}},"networks":{},"meta":{}}}}' \ + -H "Content-Type: application/json" \ + -X POST http://$API_HOST/build/save) + +if [[ $? -eq 0 ]]; then + BUILD=$(echo $CBRES | jq -r '.build') # Extract build name from API response + + echo "" + echo "Build '$BUILD' created." +else + HAS_ERROR="true" +fi + +# Contact the API again to get the installer bootstrap code +# using the build number to help automate the installation selection + +CIRES=$(curl -s \ + -d "{\"options\":{\"build\":\"$BUILD\",\"nofluff\":true}}" \ + -H "Content-Type: application/json" \ + -X POST http://$API_HOST/templates/scripts/bootstrap) + +# Show output +if [[ $? -eq 0 && "$HAS_ERROR" == "false" ]]; then + echo "" + echo "Running the install script command below will replace your current IOTstack" + echo "" + echo "Build can be deleted by calling:" + echo " curl -s -X POST http://$API_HOST/build/delete/$BUILD" + echo "" + echo "Install script is:" + echo "$CIRES" +else + HAS_ERROR="true" +fi + +if [[ "$HAS_ERROR" == "true" ]]; then + echo "Something went wrong. Check that the API is running." + echo "API host being used: $API_HOST" + echo "You can start the API manually with:" + echo " bash .internal/ctrl_api.sh" +fi \ No newline at end of file diff --git a/.internal/example_build_scripts/example2.sh b/.internal/example_build_scripts/example2.sh new file mode 100644 index 000000000..02209f6be --- /dev/null +++ b/.internal/example_build_scripts/example2.sh @@ -0,0 +1,60 @@ +#!/bin/bash + +# Before running, ensure install.sh has run successfully, and the menu containers (specifically the API container) have been built and are running. +# They can be started by running ./menu.sh + +echo "This will generate a build and echo out the command that's required to install it." +echo "Checkout the 'IOTstack.postman_collection.json' file for all API calls." +echo "" +echo "Services: plex, pihole, wireguard" +read -n 1 -s -r -p "Press any key to continue" +echo "" +echo "" + +API_HOST=localhost:32128 + +HAS_ERROR="false" + +# Generate build +CBRES=$(curl -s \ + -d '{"buildOptions":{"selectedServices":["plex","pihole","wireguard"],"configurations":{"services":{"wireguard":{"volumes":["./services/wireguard/config:/config","/lib/modules:/lib/modules"],"networks":{},"networkMode":"unchanged","loggingEnabled":true,"environment":["PUID=1000","PGID=1000","TZ=Etc/UTC","SERVERURL={$wireguardDuckDns}","SERVERPORT=19942","PEERS=1","PEERDNS=auto","INTERNAL_SUBNET=100.64.0.0/24"]},"plex":{"volumes":["./volumes/plex/config:/config","./volumes/plex/transcode:/transcode"],"networks":{},"networkMode":"unchanged","loggingEnabled":true,"environment":["PUID=1000","PGID=1000","VERSION=docker"]},"pihole":{"volumes":["./volumes/pihole/etc-pihole/:/etc/pihole/","./volumes/pihole/etc-dnsmasq.d/:/etc/dnsmasq.d/"],"networks":{"iotstack_nw":true,"vpn_nw":true},"networkMode":"unchanged","loggingEnabled":true,"environment":["TZ=Etc/UTC","WEBPASSWORD=password","DNS1=8.8.8.8","DNS2=8.8.4.4","INTERFACE=eth0"]}},"networks":{},"meta":{}}}}' \ + -H "Content-Type: application/json" \ + -X POST http://$API_HOST/build/save) + +if [[ $? -eq 0 ]]; then + BUILD=$(echo $CBRES | jq -r '.build') # Extract build name from API response + + echo "" + echo "Build '$BUILD' created." +else + HAS_ERROR="true" +fi + +# Contact the API again to get the installer bootstrap code +# using the build number to help automate the installation selection + +CIRES=$(curl -s \ + -d "{\"options\":{\"build\":\"$BUILD\",\"nofluff\":true}}" \ + -H "Content-Type: application/json" \ + -X POST http://$API_HOST/templates/scripts/bootstrap) + +# Show output +if [[ $? -eq 0 && "$HAS_ERROR" == "false" ]]; then + echo "" + echo "Running the install script command below will replace your current IOTstack" + echo "" + echo "Build can be deleted by calling:" + echo " curl -s -X POST http://$API_HOST/build/delete/$BUILD" + echo "" + echo "Install script is:" + echo "$CIRES" +else + HAS_ERROR="true" +fi + +if [[ "$HAS_ERROR" == "true" ]]; then + echo "Something went wrong. Check that the API is running." + echo "API host being used: $API_HOST" + echo "You can start the API manually with:" + echo " bash .internal/ctrl_api.sh" +fi \ No newline at end of file diff --git a/.internal/example_build_scripts/readme.md b/.internal/example_build_scripts/readme.md new file mode 100644 index 000000000..519a1ee33 --- /dev/null +++ b/.internal/example_build_scripts/readme.md @@ -0,0 +1 @@ +This folder contains scripts that will generate builds using cURL. diff --git a/.internal/jscli/index.js b/.internal/jscli/index.js new file mode 100644 index 000000000..f2a01fbfa --- /dev/null +++ b/.internal/jscli/index.js @@ -0,0 +1 @@ +const server = require('./src/index'); diff --git a/.internal/jscli/package-lock.json b/.internal/jscli/package-lock.json new file mode 100644 index 000000000..fee14fa29 --- /dev/null +++ b/.internal/jscli/package-lock.json @@ -0,0 +1,886 @@ +{ + "name": "iotstack_cli", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@sindresorhus/is": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", + "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==" + }, + "@szmarczak/http-timer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", + "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", + "requires": { + "defer-to-connect": "^1.0.1" + } + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, + "ansi-align": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz", + "integrity": "sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw==", + "requires": { + "string-width": "^3.0.0" + }, + "dependencies": { + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + } + } + }, + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" + }, + "blessed": { + "version": "0.1.81", + "resolved": "https://registry.npmjs.org/blessed/-/blessed-0.1.81.tgz", + "integrity": "sha1-+WLWh+wsNpVwrnGvhDJW5tDKESk=" + }, + "boxen": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", + "integrity": "sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ==", + "requires": { + "ansi-align": "^3.0.0", + "camelcase": "^5.3.1", + "chalk": "^3.0.0", + "cli-boxes": "^2.2.0", + "string-width": "^4.1.0", + "term-size": "^2.1.0", + "type-fest": "^0.8.1", + "widest-line": "^3.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==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "requires": { + "fill-range": "^7.0.1" + } + }, + "cacheable-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", + "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^3.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "responselike": "^1.0.2" + }, + "dependencies": { + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "requires": { + "pump": "^3.0.0" + } + }, + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" + } + } + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "chokidar": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz", + "integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==", + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.3.1", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.5.0" + } + }, + "ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==" + }, + "cli-boxes": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", + "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==" + }, + "clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "requires": { + "mimic-response": "^1.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "configstore": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "requires": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" + } + }, + "crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==" + }, + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "requires": { + "mimic-response": "^1.0.0" + } + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + }, + "defer-to-connect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", + "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==" + }, + "dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "requires": { + "is-obj": "^2.0.0" + } + }, + "duplexer3": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", + "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=" + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, + "escape-goat": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", + "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==" + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "optional": true + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "requires": { + "pump": "^3.0.0" + } + }, + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "requires": { + "is-glob": "^4.0.1" + } + }, + "global-dirs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-2.1.0.tgz", + "integrity": "sha512-MG6kdOUh/xBnyo9cJFeIKkLEc1AyFq42QTU4XiX51i2NEdxLxLWXIjEjmqKeSuKR7pAZjTqUVoT2b2huxVLgYQ==", + "requires": { + "ini": "1.3.7" + } + }, + "got": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", + "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", + "requires": { + "@sindresorhus/is": "^0.14.0", + "@szmarczak/http-timer": "^1.1.2", + "cacheable-request": "^6.0.0", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^4.1.0", + "lowercase-keys": "^1.0.1", + "mimic-response": "^1.0.1", + "p-cancelable": "^1.0.0", + "to-readable-stream": "^1.0.0", + "url-parse-lax": "^3.0.0" + } + }, + "graceful-fs": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", + "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "has-yarn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", + "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==" + }, + "http-cache-semantics": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" + }, + "ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=" + }, + "import-lazy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=" + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" + }, + "ini": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz", + "integrity": "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==" + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "requires": { + "ci-info": "^2.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-installed-globally": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz", + "integrity": "sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==", + "requires": { + "global-dirs": "^2.0.1", + "is-path-inside": "^3.0.1" + } + }, + "is-npm": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz", + "integrity": "sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==" + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==" + }, + "is-path-inside": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.2.tgz", + "integrity": "sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg==" + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "is-yarn-global": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", + "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==" + }, + "json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=" + }, + "keyv": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", + "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", + "requires": { + "json-buffer": "3.0.0" + } + }, + "latest-version": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", + "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==", + "requires": { + "package-json": "^6.3.0" + } + }, + "lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==" + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "requires": { + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "neo-blessed": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/neo-blessed/-/neo-blessed-0.2.0.tgz", + "integrity": "sha512-C2kC4K+G2QnNQFXUIxTQvqmrdSIzGTX1ZRKeDW6ChmvPRw8rTkTEJzbEQHiHy06d36PCl/yMOCjquCRV8SpSQw==" + }, + "nodemon": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.7.tgz", + "integrity": "sha512-XHzK69Awgnec9UzHr1kc8EomQh4sjTQ8oRf8TsGrSmHDx9/UmiGG9E/mM3BuTfNeFwdNBvrqQq/RHL0xIeyFOA==", + "requires": { + "chokidar": "^3.2.2", + "debug": "^3.2.6", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.0.4", + "pstree.remy": "^1.1.7", + "semver": "^5.7.1", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.3", + "update-notifier": "^4.1.0" + } + }, + "nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", + "requires": { + "abbrev": "1" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + }, + "normalize-url": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", + "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "p-cancelable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", + "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==" + }, + "package-json": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", + "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==", + "requires": { + "got": "^9.6.0", + "registry-auth-token": "^4.0.0", + "registry-url": "^5.0.0", + "semver": "^6.2.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==" + }, + "prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=" + }, + "pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pupa": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz", + "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==", + "requires": { + "escape-goat": "^2.0.0" + } + }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + } + }, + "readdirp": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", + "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", + "requires": { + "picomatch": "^2.2.1" + } + }, + "registry-auth-token": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.1.tgz", + "integrity": "sha512-6gkSb4U6aWJB4SF2ZvLb76yCBjcvufXBqvvEx1HbmKPkutswjW1xNVRY0+daljIYRbogN7O0etYSlbiaEQyMyw==", + "requires": { + "rc": "^1.2.8" + } + }, + "registry-url": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", + "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", + "requires": { + "rc": "^1.2.8" + } + }, + "responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", + "requires": { + "lowercase-keys": "^1.0.0" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + }, + "semver-diff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", + "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==", + "requires": { + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" + }, + "string-width": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + }, + "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" + } + }, + "term-size": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz", + "integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==" + }, + "to-readable-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", + "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==" + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "requires": { + "is-number": "^7.0.0" + } + }, + "touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "requires": { + "nopt": "~1.0.10" + } + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==" + }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "requires": { + "is-typedarray": "^1.0.0" + } + }, + "undefsafe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.3.tgz", + "integrity": "sha512-nrXZwwXrD/T/JXeygJqdCO6NZZ1L66HrxM/Z7mIq2oPanoN0F1nLx3lwJMu6AwJY69hdixaFQOuoYsMjE5/C2A==", + "requires": { + "debug": "^2.2.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "requires": { + "crypto-random-string": "^2.0.0" + } + }, + "update-notifier": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.3.tgz", + "integrity": "sha512-Yld6Z0RyCYGB6ckIjffGOSOmHXj1gMeE7aROz4MG+XMkmixBX4jUngrGXNYz7wPKBmtoD4MnBa2Anu7RSKht/A==", + "requires": { + "boxen": "^4.2.0", + "chalk": "^3.0.0", + "configstore": "^5.0.1", + "has-yarn": "^2.1.0", + "import-lazy": "^2.1.0", + "is-ci": "^2.0.0", + "is-installed-globally": "^0.3.1", + "is-npm": "^4.0.0", + "is-yarn-global": "^0.3.0", + "latest-version": "^5.0.0", + "pupa": "^2.0.1", + "semver-diff": "^3.1.1", + "xdg-basedir": "^4.0.0" + } + }, + "url-parse-lax": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", + "requires": { + "prepend-http": "^2.0.0" + } + }, + "widest-line": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "requires": { + "string-width": "^4.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==" + } + } +} diff --git a/.internal/jscli/package.json b/.internal/jscli/package.json new file mode 100644 index 000000000..8f343b6d1 --- /dev/null +++ b/.internal/jscli/package.json @@ -0,0 +1,17 @@ +{ + "name": "iotstack_cli", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "start": "NODE_ENV=production node index.js", + "dev": "NODE_ENV=development nodemon index.js" + }, + "author": "", + "license": "ISC", + "dependencies": { + "blessed": "^0.1.81", + "neo-blessed": "^0.2.0", + "nodemon": "^2.0.7" + } +} diff --git a/.internal/jscli/src/index.js b/.internal/jscli/src/index.js new file mode 100644 index 000000000..e365d2af8 --- /dev/null +++ b/.internal/jscli/src/index.js @@ -0,0 +1,35 @@ +const blessed = require('neo-blessed'); + +const appVersion = require('../package.json').version; + +let listenInterface = process.env?.API_INTERFACE ?? '0.0.0.0'; +let listenPort = process.env?.API_PORT ?? '32128'; + +const processEnvVars = (envs) => { + const { + HOSTUSER, + IOTSTACKPWD, + HOSTSSH_ADDR, + HOSTSSH_PORT + } = envs; +}; + +const init = () => { + const logger = require('./logger')(); + process.stdin.removeAllListeners('data'); + const screen = blessed.screen({ + smartCSR: true, + title: 'IOTstack JSCLI', + }); + // screen.disableMouse(); + + const MainMenu = require('./menus/main'); + + const mainMenu = MainMenu({ screen, version: appVersion, logger }); + mainMenu.init(); + mainMenu.render(); + +} + +processEnvVars(process.env) +init(); \ No newline at end of file diff --git a/.internal/jscli/src/logger.js b/.internal/jscli/src/logger.js new file mode 100644 index 000000000..051c43727 --- /dev/null +++ b/.internal/jscli/src/logger.js @@ -0,0 +1,13 @@ +const Logger = () => { + const retr = {}; + + retr.debug = console.debug; + retr.info = console.info; + retr.log = console.log; + retr.warn = console.warn; + retr.error = console.error; + + return retr; +} + +module.exports = Logger; diff --git a/.internal/jscli/src/menus/main.js b/.internal/jscli/src/menus/main.js new file mode 100644 index 000000000..abe6981cd --- /dev/null +++ b/.internal/jscli/src/menus/main.js @@ -0,0 +1,39 @@ +const blessed = require('neo-blessed'); + +const MainMenu = ({ screen, settings, version, logger }) => { + const retr = {}; + + retr.init = () => { + logger.debug('MainMenu:init()'); + }; + + retr.render = () => { + const box = blessed.box({ + top: 'center', + left: 'center', + width: '50%', + height: '50%', + content: 'Hello {bold}world{/bold}!', + tags: true, + border: { + type: 'line' + }, + style: { + fg: 'white', + bg: 'magenta', + border: { + fg: '#f0f0f0' + }, + hover: { + bg: 'green' + } + } + }); + + screen.append(box); + screen.render(); + } + + return retr; +} +module.exports = MainMenu; diff --git a/.internal/jscli/start.sh b/.internal/jscli/start.sh new file mode 100644 index 000000000..98da2bdbb --- /dev/null +++ b/.internal/jscli/start.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +echo "JSCLI bootstrap script startup" + +if [[ $IOTENV == "development" || "$1" = "development" ]]; then + echo "Starting JSCLI in development mode" + npm run dev +else + npm start +fi diff --git a/.internal/meta.sh b/.internal/meta.sh new file mode 100644 index 000000000..beb8bc902 --- /dev/null +++ b/.internal/meta.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +# Used for knowing when an update has occured +export VERSION="v0.0.1" + +# Used for the interal docker menu instances to know where IOTstack is installed +export IOTSTACKPWD="${IOTSTACK_IOTSTACKPWD:-$(cd .. && pwd)}" + +# Used for the interal docker menu instances to know which user to set permissions for, and for SSH connections +export HOSTUSER="${IOTSTACK_HOSTUSER:-$(whoami)}" + +# For menu CLI and API containers to connect to +export HOSTSSH_ADDR="${IOTSTACK_HOSTSSH_ADDR:-"host.docker.internal"}" + +# SSH port +export HOSTSSH_PORT="${IOTSTACK_HOSTSSH_PORT:-22}" + +# For the host to know how to connect to itself (or a remote host) +export HOST_CON_IP="${IOTSTACK_HOST_CON_IP:-"localhost"}" + +# API Port +export API_PORT="${IOTSTACK_API_PORT:-32128}" + +# WUI Port +export WUI_PORT="${IOTSTACK_WUI_PORT:-32777}" + +# Listen interface for API. 0.0.0.0 is all interfaces +export API_INTERFACE="${IOTSTACK_API_INTERFACE:-0.0.0.0}" + +# Host and port for the docker CLI to know where the API is running +export PYCLI_CON_API="${IOTSTACK_PYCLI_CON_API:-"$HOSTSSH_ADDR:$API_PORT"}" + +# Host and port for the docker CLI to know where the API is running when executing commands via SSH +export PYCLI_HOST_CON_API="${IOTSTACK_HOST_CON_IP:-"$HOST_CON_IP:$API_PORT"}" + +# Host and port for the docker CLI to know where the WUI is running +export PYCLI_CON_WUI="${IOTSTACK_PYCLI_CON_WUI:-"$HOST_CON_IP:$WUI_PORT"}" + +# If this is set, menu containers will run in developer mode. +# export IOTENV="development" + +# If this is set, before install.sh is initially run, the installer will switch to the branch specified here immediately after cloning. +# export IOTSTACK_INSTALL_BRANCH="experimental" diff --git a/.internal/pycli.Dockerfile b/.internal/pycli.Dockerfile new file mode 100644 index 000000000..ef561a04e --- /dev/null +++ b/.internal/pycli.Dockerfile @@ -0,0 +1,8 @@ +FROM python:3 + +WORKDIR /usr/iotstack_pycli + +COPY ./pycli ./ +RUN pip install --no-cache-dir -r requirements.txt + +CMD [ "python", "./entry.py" ] diff --git a/scripts/backup_restore.py b/.internal/pycli/backup_restore.py old mode 100755 new mode 100644 similarity index 83% rename from scripts/backup_restore.py rename to .internal/pycli/backup_restore.py index e13b8d0a5..c91276079 --- a/scripts/backup_restore.py +++ b/.internal/pycli/backup_restore.py @@ -5,11 +5,11 @@ def main(): from blessed import Terminal from deps.chars import specialChars, commonTopBorder, commonBottomBorder, commonEmptyLine - global renderMode import time - import subprocess + from deps.host_exec import execInteractive global signal + global renderMode global backupRestoreSelectionInProgress global mainMenuList global currentMenuItemIndex @@ -25,7 +25,8 @@ def main(): def runBackup(): global needsRender print("Execute Backup:") - subprocess.call("./scripts/backup.sh", shell=True) + print("bash ./scripts/backup.sh") + execInteractive('bash ./scripts/backup.sh') print("") print("Backup completed.") print("Press [Up] or [Down] arrow key to show the menu if it has scrolled too far.") @@ -36,9 +37,13 @@ def runBackup(): def dropboxInstall(): global needsRender print("Install Dropbox:") - subprocess.call("git clone https://github.com/andreafabrizi/Dropbox-Uploader.git ~/Dropbox-Uploader", shell=True) - subprocess.call("chmod +x ~/Dropbox-Uploader/dropbox_uploader.sh", shell=True) - subprocess.call("cd ~/Dropbox-Uploader && ./dropbox_uploader.sh", shell=True) + print("git clone https://github.com/andreafabrizi/Dropbox-Uploader.git ~/Dropbox-Uploader") + execInteractive('git clone https://github.com/andreafabrizi/Dropbox-Uploader.git ~/Dropbox-Uploader') + print("chmod +x ~/Dropbox-Uploader/dropbox_uploader.sh") + execInteractive('chmod +x ~/Dropbox-Uploader/dropbox_uploader.sh') + print("cd ~/Dropbox-Uploader && ./dropbox_uploader.sh") + execInteractive('cd ~/Dropbox-Uploader && ./dropbox_uploader.sh') + time.sleep(1) # Give time to see errors print("") print("Dropbox install finished") print("Press [Up] or [Down] arrow key to show the menu if it has scrolled too far.") @@ -50,7 +55,7 @@ def rcloneInstall(): global needsRender print("Install rClone:") print("sudo apt install -y rclone") - subprocess.call("sudo apt install -y rclone", shell=True) + execInteractive('sudo apt install -y rclone') print("") print("rClone install finished") print("Please run 'rclone config' to configure the rclone google drive backup") @@ -61,7 +66,8 @@ def rcloneInstall(): def rCloneSetup(): global needsRender print("Setup rclone:") - subprocess.call("rclone config", shell=True) + print("rclone config") + execInteractive('rclone config') print("") print("rclone setup completed. Press [Up] or [Down] arrow key to show the menu if it has scrolled too far.") time.sleep(1) @@ -71,7 +77,8 @@ def rCloneSetup(): def runRestore(): global needsRender print("Execute Restore:") - subprocess.call("./scripts/restore.sh", shell=True) + print('bash ./scripts/restore.sh') + execInteractive('bash ./scripts/restore.sh') print("") print("Restore completed.") print("Press [Up] or [Down] arrow key to show the menu if it has scrolled too far.") @@ -113,7 +120,7 @@ def onResize(sig, action): mainRender(1, mainMenuList, currentMenuItemIndex) def renderHotZone(term, menu, selection, hotzoneLocation): - lineLengthAtTextStart = 71 + lineLengthAtTextStart = 53 print(term.move(hotzoneLocation[0], hotzoneLocation[1])) for (index, menuItem) in enumerate(menu): toPrint = "" @@ -141,7 +148,7 @@ def mainRender(needsRender, menu, selection): print("") print(term.center(commonTopBorder(renderMode))) print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Select backup command to run {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} Select backup command to run {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) print(term.center(commonEmptyLine(renderMode))) print(term.center(commonEmptyLine(renderMode))) print(term.center(commonEmptyLine(renderMode))) @@ -154,15 +161,15 @@ def mainRender(needsRender, menu, selection): if not hideHelpText: if term.height < 30: print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Not enough vertical room to render controls help text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} Not enough vertical room to render controls help text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) print(term.center(commonEmptyLine(renderMode))) else: print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Enter] to run command {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Escape] to go back to main menu {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [Enter] to run command {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [Escape] to go back to main menu {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) print(term.center(commonEmptyLine(renderMode))) print(term.center(commonEmptyLine(renderMode))) print(term.center(commonBottomBorder(renderMode))) diff --git a/.internal/pycli/buildstack_menu.py b/.internal/pycli/buildstack_menu.py new file mode 100644 index 000000000..bd55aa2e6 --- /dev/null +++ b/.internal/pycli/buildstack_menu.py @@ -0,0 +1,532 @@ +#!/usr/bin/env python3 +import signal + +checkedMenuItems = [] +results = {} + +def main(): + import os + import time + import ruamel.yaml + import math + import sys + import traceback + import subprocess + from deps.chars import specialChars, commonTopBorder, commonBottomBorder, commonEmptyLine, padText + from deps.api import getBuildServicesList, getBuildServicesJsonList, getBuildServicesMetaData, getBuildServicesOptionsData, saveBuild, checkBuild + from blessed import Terminal + global signal + global renderMode + global term + global paginationSize + global paginationStartIndex + global hideHelpText + global activeMenuLocation + global lastSelection + global apiServicesList + global apiServicesJson + global apiServicesMetadata + global apiServicesOptions + global apiCheckBuild + global apiBuildOutput + global selectedServices + global hasIssuesChecked + global buildOptions + + # Runtime vars + menu = [] + selectedServices = [] + buildOptions = { + "selectedServices": [], + "configurations": { + "services": {} + } + } + hasIssuesChecked = False + apiServicesList = None + apiServicesJson = None + apiServicesMetadata = None + apiServicesOptions = None + apiCheckBuild = None + apiBuildOutput = None + term = Terminal() + hotzoneLocation = [7, 0] # Top text + paginationToggle = [10, term.height - 25] # Top text + controls text + paginationStartIndex = 0 + paginationSize = paginationToggle[0] + activeMenuLocation = 0 + lastSelection = 0 + + try: # If not already set, then set it. + hideHelpText = hideHelpText + except: + hideHelpText = False + + def hasReportedIssue(serviceName): + if apiCheckBuild != None and 'json' in apiCheckBuild and apiCheckBuild['json'] != None and 'issueList' in apiCheckBuild['json']: + if len(apiCheckBuild['json']['issueList']['services']) > 0: + issuesList = apiCheckBuild['json']['issueList']['services'] + for issue in issuesList: + if issue['name'] == serviceName: + return True + return False + return None + + def updateMenuIssues(menu): + for (index, menuItem) in enumerate(menu): + if menuItem[1] in selectedServices: + menuItem[2]['issues'] = hasReportedIssue(menuItem[1]) + else: + menuItem[2]['issues'] = None + + def checkForIssues(): + try: + global apiCheckBuild + apiCheckBuild = checkBuild(os.getenv('API_ADDR'), selectedServices, buildOptions["configurations"]) + return True + except Exception as err: + print("Issue checking build:") + print(err) + print(sys.exc_info()) + traceback.print_exc() + input("Press Enter to continue...") + return False + + def executeServiceOptions(): + global buildOptions + global hasIssuesChecked + menuItem = menu[selection] + serviceName = menuItem[1] + if "validOptions" in menuItem[2] and not menuItem[2]["validOptions"] == False: + execGlobals = { + "validMenuItems": [], + "toRun": "runOptionsMenu", + "currentServiceName": serviceName, + "apiBuildOptions": apiServicesOptions['json'][serviceName], + "apiServicesOptions": apiServicesOptions['json'], + "renderMode": renderMode, + "buildOptions": buildOptions + } + execLocals = locals() + optionsScriptPath = "./serviceOptions/options_screen.py" + with open(optionsScriptPath, "rb") as pythonDynamicImportFile: + code = compile(pythonDynamicImportFile.read(), optionsScriptPath, "exec") # Finish here + exec(code, execGlobals, execLocals) + mainRender(menu, selection, 1) + hasIssuesChecked = False + else: + return True + + def buildServices(): + try: + if len(selectedServices) > 0: + global apiBuildOutput + apiBuildOutput = saveBuild(os.getenv('API_ADDR'), selectedServices, buildOptions["configurations"]) + return True + else: + print("No items selected") + return False + except Exception as err: + print("Issue running build:") + print(err) + print(sys.exc_info()) + traceback.print_exc() + input("Press Enter to continue...") + return False + + def generateLineText(text, textLength=None, paddingBefore=0, lineLength=24): + result = "" + for i in range(paddingBefore): + result += " " + + textPrintableCharactersLength = textLength + + if (textPrintableCharactersLength) == None: + textPrintableCharactersLength = len(text) + + result += text + remainingSpace = lineLength - textPrintableCharactersLength + + for i in range(remainingSpace): + result += " " + + return result + + def renderHotZone(term, renderType, menu, selection, paddingBefore): + global paginationSize + optionsLength = len(" >> Options ") + optionsIssuesSpace = len(" ") + selectedTextLength = len("-> ") + spaceAfterissues = len("") + issuesLength = len(" !! Issue") + + print(term.move(hotzoneLocation[0], hotzoneLocation[1])) + + if paginationStartIndex >= 1: + print(term.center("{b} {uaf} {uaf}{uaf}{uaf} {ual} {b}".format( + b=specialChars[renderMode]["borderVertical"], + uaf=specialChars[renderMode]["upArrowFull"], + ual=specialChars[renderMode]["upArrowLine"] + ))) + else: + print(term.center(commonEmptyLine(renderMode))) + + menuItemsActiveRow = term.get_location()[0] + if renderType == 2 or renderType == 1: # Rerender entire hotzone + for (index, menuItem) in enumerate(menu): # Menu loop + if index >= paginationStartIndex and index < paginationStartIndex + paginationSize: + + # Menu highlight logic + if index == selection: + lineText = generateLineText(menuItem[0], paddingBefore=paddingBefore) + activeMenuLocation = term.get_location()[0] + formattedLineText = '-> {t.blue_on_green}{title}{t.normal} <-'.format(t=term, title=menuItem[0][0:21]) + paddedLineText = generateLineText(formattedLineText, textLength=len(menuItem[0]) + selectedTextLength, paddingBefore=paddingBefore - selectedTextLength) + toPrint = paddedLineText + else: + titleLength = len("longest title be4 trunc") + menuItemTitle = menuItem[0] + if len(menuItemTitle) > titleLength: + menuItemTitle = menuItemTitle[0:titleLength - 3] + '...' + + lineText = generateLineText(menuItemTitle, paddingBefore=paddingBefore) + toPrint = '{title}{t.normal}'.format(t=term, title=lineText) + # ##### + + # Options and issues + if "validOptions" in menuItem[2] and not menuItem[2]["validOptions"] == False: + toPrint = toPrint + '{t.blue_on_black} {raf}{raf}{t.normal}'.format(t=term, raf=specialChars[renderMode]["rightArrowFull"]) + toPrint = toPrint + ' {t.white_on_black} Options {t.normal}'.format(t=term) + else: + for i in range(optionsLength): + toPrint += " " + + for i in range(optionsIssuesSpace): + toPrint += " " + + if menuItem[2]["checked"]: + if "issues" in menuItem[2] and menuItem[2]["issues"] == True and hasIssuesChecked == True: + toPrint = toPrint + '{t.red_on_orange} !! {t.normal}'.format(t=term) + toPrint = toPrint + ' {t.orange_on_black}Issue {t.normal}'.format(t=term) + elif "issues" in menuItem[2] and menuItem[2]["issues"] == False and hasIssuesChecked == True: + toPrint = toPrint + ' {t.green_on_blue} Pass {t.normal} '.format(t=term) + else: + toPrint = toPrint + ' {t.red_on_black} Unknown {t.normal} '.format(t=term) + else: + for i in range(issuesLength): + toPrint += " " + + for i in range(spaceAfterissues): + toPrint += " " + # ##### + + # Menu check render logic + if menuItem[2]["checked"]: + toPrint = " (X) " + toPrint + else: + toPrint = " ( ) " + toPrint + + toPrint = "{bv} {toPrint} {bv}".format(bv=specialChars[renderMode]["borderVertical"], toPrint=toPrint) # Generate border + toPrint = term.center(toPrint) # Center Text (All lines should have the same amount of printable characters) + # ##### + print(toPrint) + + + if renderType == 3: # Only partial rerender of hotzone (the unselected menu item, and the newly selected menu item rows) + global lastSelection + global renderOffsetLastSelection + global renderOffsetCurrentSelection + # TODO: Finish this, currently disabled. To enable, update the actions for UP and DOWN array keys below to assigned 3 to needsRender + renderOffsetLastSelection = lastSelection - paginationStartIndex + renderOffsetCurrentSelection = selection - paginationStartIndex + lineText = generateLineText(menu[lastSelection][0], paddingBefore=paddingBefore) + toPrint = '{title}{t.normal}'.format(t=term, title=lineText) + print('{t.move_y(lastSelection)}{title}'.format(t=term, title=toPrint)) + print(renderOffsetCurrentSelection, lastSelection, renderOffsetLastSelection) + lastSelection = selection + + if paginationStartIndex + paginationSize < len(menu): + print(term.center("{b} {daf} {daf}{daf}{daf} {dal} {b}".format( + b=specialChars[renderMode]["borderVertical"], + daf=specialChars[renderMode]["downArrowFull"], + dal=specialChars[renderMode]["downArrowLine"] + ))) + else: + print(term.center(commonEmptyLine(renderMode))) + + def mainRender(menu, selection, renderType = 1): + global paginationStartIndex + global paginationSize + paddingBefore = 4 + + if selection >= paginationStartIndex + paginationSize: + paginationStartIndex = selection - (paginationSize - 1) + 1 + renderType = 1 + + if selection <= paginationStartIndex - 1: + paginationStartIndex = selection + renderType = 1 + + try: + if (renderType == 1): + print(term.clear()) + print(term.move_y(7 - hotzoneLocation[0])) + print(term.black_on_cornsilk4(term.center('IOTstack Build Menu'))) + print("") + print(term.center(commonTopBorder(renderMode))) + + print(term.center(commonEmptyLine(renderMode))) + print(term.center("{bv} Select containers to build {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center(commonEmptyLine(renderMode))) + print(term.center(commonEmptyLine(renderMode))) + print(term.center(commonEmptyLine(renderMode))) + + if len(menu) > 0: + renderHotZone(term, renderType, menu, selection, paddingBefore) + else: + print(term.center("{bv} No menu items were loaded. Press [ESC] to go back {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center(commonEmptyLine(renderMode))) + + if (renderType == 1): + print(term.center(commonEmptyLine(renderMode))) + allIssuesLength = 0 + try: + allIssuesLength = len(apiCheckBuild['json']['issueList']['services']) + except: + pass + hideTextSize = 1 + if not hideHelpText: + hideTextSize = 11 + room = term.height - (22 + hideTextSize + allIssuesLength + min(paginationSize, len(menu))) + if room < 0: + print(term.center(commonEmptyLine(renderMode))) + print(term.center("{bv} Not enough room to render controls help text (H:{th}, V:{rm}) {bv}".format(bv=specialChars[renderMode]["borderVertical"], th=(str(term.height).zfill(3)), rm=(str(room).zfill(3))))) + print(term.center(commonEmptyLine(renderMode))) + if not hideHelpText: + print(term.center(commonEmptyLine(renderMode))) + print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [Space] to select or deselect service {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [Right] for options for containers that support them {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [Tab] Expand or collapse build menu size {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [R] Refresh list {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + # print(term.center("{bv} [F] Filter options {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + if hasIssuesChecked: + print(term.center("{bv} [Enter] to create build {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + else: + print(term.center("{bv} [Enter] to check build {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [Escape] to cancel build {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center(commonEmptyLine(renderMode))) + print(term.center(commonEmptyLine(renderMode))) + print(term.center(commonBottomBorder(renderMode))) + + if apiCheckBuild != None and 'json' in apiCheckBuild and apiCheckBuild['json'] != None and 'issueList' in apiCheckBuild['json']: + if 'services' in apiCheckBuild['json']['issueList']: + if len(apiCheckBuild['json']['issueList']['services']) > 0: + issuesList = apiCheckBuild['json']['issueList'] + print(term.center("")) + print(term.center("")) + print(term.center("")) + print(term.center(("{btl}{bh}{bh}{bh}{bh}{bh} Build Issues ({bil}) {bh}" + "{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}" + "{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}" + "{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}" + "{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}" + "{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}" + "{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}" + "{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}" + "{bh}{bh}{btr}").format( + btl=specialChars[renderMode]["borderTopLeft"], + btr=specialChars[renderMode]["borderTopRight"], + bh=specialChars[renderMode]["borderHorizontal"], + bil=str(len(issuesList['services'])).zfill(2) + ))) + print(term.center(commonEmptyLine(renderMode, size = 139))) + # print(term.center("{bv} {t.red_on_orange}!{t.normal} Menu can still be built with issues detected. IOTstack will attempt to use the best configuration to get your services running. {bv}".format(t=term, bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} {t.red_on_orange}!{t.normal} Services can still be built with issues detected. IOTstack will attempt to use the best configuration to get your services running. {bv}".format(t=term, bv=specialChars[renderMode]["borderVertical"]))) + print(term.center(commonEmptyLine(renderMode, size = 139))) + for service in issuesList['services']: + issueMessageMaxLength = len("No pallette addons selected for NodeRed. Select addons in options to remove this warning. Default modules") + issueMessage = str(service['message']) + if len(issueMessage) > issueMessageMaxLength: + issueMessage = issueMessage[0:issueMessageMaxLength - 3] + '...' + + spacesAndBracketsLen = len(" () ") + issueAndTypeLen = len(service['name']) + len(service['issueType']) + spacesAndBracketsLen + serviceNameAndConflictType = '{t.red_on_black}{service}{t.normal} ({t.yellow_on_black}{issueType}{t.normal}) '.format(t=term, service=service['name'], issueType=service['issueType']) + formattedServiceNameAndConflictType = generateLineText(str(serviceNameAndConflictType), textLength=issueAndTypeLen, paddingBefore=0, lineLength=30) + issueDescription = generateLineText(str(issueMessage), textLength=len(str(issueMessage)), paddingBefore=0, lineLength=105) + print(term.center("{bv} {nm} - {desc} {bv}".format(nm=formattedServiceNameAndConflictType, desc=issueDescription, bv=specialChars[renderMode]["borderVertical"]) )) + print(term.center(commonEmptyLine(renderMode, size = 139))) + print(term.center(commonBottomBorder(renderMode, size = 139))) + elif apiCheckBuild != None and (apiCheckBuild['json'] == None or apiCheckBuild['status'] == -1): + print("API failed. See API logs for details.") + print("Press [Esc] to go back") + + + except Exception as err: + print("There was an error rendering the menu:") + print(err) + print('Error reported:') + print(sys.exc_info()) + traceback.print_exc() + print("Press [Esc] to go back") + return + + return + + def checkMenuItem(selection): + global selectedServices + global hasIssuesChecked + hasIssuesChecked = False + + if menu[selection][2]["checked"] == True: + menu[selection][2]["checked"] = False + while menu[selection][1] in selectedServices: selectedServices.remove(menu[selection][1]) + else: + menu[selection][2]["checked"] = True + selectedServices.append(menu[selection][1]) + + def onResize(sig, action): + global paginationToggle + paginationToggle = [10, term.height - 25] + mainRender(menu, selection, 1) + + def populateMenu(): + global hasIssuesChecked + hasIssuesChecked = False + menu.clear() + hasError = [] + if not apiServicesList == None and 'json' in apiServicesList: + for service in apiServicesList['json']: + try: + itemChecked = False + hasIssue = None + if service in selectedServices: + itemChecked = True + hasIssue = hasReportedIssue(service) + + menu.append([service, service, { "checked": itemChecked, "options": None, "tags": [], "issues": hasIssue }]) + menu[-1][0] = apiServicesMetadata['json'][service]['displayName'] + menu[-1][2]["tags"] = apiServicesMetadata['json'][service]['serviceTypeTags'] + + if service in apiServicesOptions['json']: + menu[-1][2]["options"] = apiServicesOptions['json'][service] + execGlobals = { + "validMenuItems": [], + "toRun": "createMenuOptions", + "currentServiceName": service, + "apiBuildOptions": apiServicesOptions['json'][service], + "apiServicesOptions": apiServicesOptions['json'] + } + execLocals = locals() + optionsScriptPath = "./serviceOptions/options_screen.py" + with open(optionsScriptPath, "rb") as pythonDynamicImportFile: + code = compile(pythonDynamicImportFile.read(), optionsScriptPath, "exec") # Finish here + exec(code, execGlobals, execLocals) + menu[-1][2]["validOptions"] = False + if "validMenuItems" in execGlobals: + if len(execGlobals["validMenuItems"]) > 0: + menu[-1][2]["validOptions"] = True + except Exception as err: + print(sys.exc_info()) + traceback.print_exc() + hasError.append([service, err]) + else: + print("Menu could not be loaded. API call did not return JSON:") + print(apiServicesList) + input("Press [Enter] to continue") + + if len(hasError) > 0: + print("There were errors loading the menu:") + for errorItem in hasError: + print(errorItem) + input("Press [Enter] to continue") + return False + return True + + def loadMenu(): + global apiServicesList + global apiServicesJson + global apiServicesMetadata + global apiServicesOptions + print('Loading Build Services...') + apiServicesList = getBuildServicesList(os.getenv('API_ADDR')) + print('Loading Service Templates...') + apiServicesJson = getBuildServicesJsonList(os.getenv('API_ADDR')) + print('Loading Service Metadatas...') + apiServicesMetadata = getBuildServicesMetaData(os.getenv('API_ADDR')) + print('Loading Service Options...') + apiServicesOptions = getBuildServicesOptionsData(os.getenv('API_ADDR')) + print('Loading Done') + populateMenu() + + if __name__ == 'builtins': + global results + global signal + needsRender = 1 + signal.signal(signal.SIGWINCH, onResize) + loadMenu() + with term.fullscreen(): + selection = 0 + mainRender(menu, selection, 1) + selectionInProgress = True + with term.cbreak(): + while selectionInProgress: + key = term.inkey(esc_delay=0.05) + if key.is_sequence: + if key.name == 'KEY_TAB': + needsRender = 1 + if paginationSize == paginationToggle[0]: + paginationSize = paginationToggle[1] + paginationStartIndex = 0 + else: + paginationSize = paginationToggle[0] + if key.name == 'KEY_DOWN': + selection += 1 + needsRender = 2 + if key.name == 'KEY_UP': + selection -= 1 + needsRender = 2 + if key.name == 'KEY_RIGHT': + executeServiceOptions() + if key.name == 'KEY_ENTER': + if len(selectedServices) > 0: + if hasIssuesChecked == False: + checkForIssues() + updateMenuIssues(menu) + hasIssuesChecked = True + needsRender = 1 + else: + buildResult = buildServices() + results["buildState"] = buildResult + if not buildResult == None: + selectionInProgress = False + return results["buildState"] + if key.name == 'KEY_ESCAPE': + results["buildState"] = False + return results["buildState"] + elif key: + if key == ' ': # Space pressed + checkMenuItem(selection) # Update checked list + needsRender = 1 + elif key == 'h': # H pressed + hideHelpText = ~hideHelpText + elif key == 'r': # R pressed + loadMenu() + else: + hideHelpText = True + needsRender = 1 + else: + print(key) + time.sleep(0.5) + + if len(menu) > 0: + selection = selection % len(menu) + + mainRender(menu, selection, needsRender) + +originalSignalHandler = signal.getsignal(signal.SIGINT) +main() +signal.signal(signal.SIGWINCH, originalSignalHandler) diff --git a/.internal/pycli/compose_override_entry.py b/.internal/pycli/compose_override_entry.py new file mode 100644 index 000000000..5b8532d7e --- /dev/null +++ b/.internal/pycli/compose_override_entry.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 + +# This is the entry file for merging compose-override.yml + +import sys +import traceback +import ruamel.yaml +import os +import sys + +yaml = ruamel.yaml.YAML() +yaml.preserve_quotes = True + +hasError = False + +try: + print('ruamel.yaml Version:', ruamel.yaml.__version__) + print('') + + if len(sys.argv) > 1: + try: + print('(args - base ) sys.argv[1]: ', sys.argv[1]) + print('(args - override) sys.argv[2]: ', sys.argv[2]) + print('(args - output ) sys.argv[3]: ', sys.argv[3]) + baseYamlFile = sys.argv[1] + overrideYamlFile = sys.argv[2] + outputYamlFile = sys.argv[3] + except: + print("Error: Not enough args") + print("Usage:") + print(" compose_override_entry.py [inputFile] [mergeFile] [outputFile]") + print("") + print("Example:") + print(" compose_override_entry.py ./docker-compose.tmp.yml ./compose-override.yml ./docker-compose.yml") + sys.exit(4) + + print('baseYamlFile: ', baseYamlFile) + print('overrideYamlFile: ', overrideYamlFile) + print('outputYamlFile: ', outputYamlFile) + + else: + print('(env) PYCLI_OVERRIDE_YML: ', os.getenv('PYCLI_OVERRIDE_YML')) + print('(env) PYCLI_BASE_YML: ', os.getenv('PYCLI_BASE_YML')) + print('(env) PYCLI_OUTPUT_YML: ', os.getenv('PYCLI_OUTPUT_YML')) + overrideYamlFile = os.getenv('PYCLI_OVERRIDE_YML') + baseYamlFile = os.getenv('PYCLI_BASE_YML') + outputYamlFile = os.getenv('PYCLI_OUTPUT_YML') + + if overrideYamlFile == None: + hasError = True + print("Error: overrideYamlFile not set. Set environment variable 'PYCLI_OVERRIDE_YML'") + + if baseYamlFile == None: + hasError = True + print("Error: baseYamlFile not set. Set environment variable 'PYCLI_BASE_YML'") + + if outputYamlFile == None: + hasError = True + print("Error: outputYamlFile not set. Set environment variable 'PYCLI_OUTPUT_YML'") + + if hasError: + sys.exit(1) + + def mergeYaml(priorityYaml, defaultYaml): + finalYaml = {} + if isinstance(defaultYaml, dict): + for dk, dv in defaultYaml.items(): + if dk in priorityYaml: + finalYaml[dk] = mergeYaml(priorityYaml[dk], dv) + else: + finalYaml[dk] = dv + for pk, pv in priorityYaml.items(): + if pk in finalYaml: + finalYaml[pk] = mergeYaml(finalYaml[pk], pv) + else: + finalYaml[pk] = pv + else: + finalYaml = defaultYaml + return finalYaml + + def main(): + with open(r'%s' % baseYamlFile) as fileBaseYamlFile: + baseYaml = yaml.load(fileBaseYamlFile) + + with open(r'%s' % overrideYamlFile) as fileOverrideYamlFile: + yamlOverride = yaml.load(fileOverrideYamlFile) + + mergedYaml = mergeYaml(yamlOverride, baseYaml) + + with open(r'%s' % outputYamlFile, 'w') as outputFile: + yaml.dump(mergedYaml, outputFile) + + main() + print('') + print('Merge complete') + +except SystemExit: + sys.exit(0) +except: + print("Something went wrong: ") + print(sys.exc_info()) + print(traceback.print_exc()) + print("") + sys.exit(2) \ No newline at end of file diff --git a/scripts/deps/__init__.py b/.internal/pycli/deps/__init__.py old mode 100755 new mode 100644 similarity index 100% rename from scripts/deps/__init__.py rename to .internal/pycli/deps/__init__.py diff --git a/.internal/pycli/deps/api.py b/.internal/pycli/deps/api.py new file mode 100644 index 000000000..c4fcc7686 --- /dev/null +++ b/.internal/pycli/deps/api.py @@ -0,0 +1,313 @@ +import requests +import time + +def checkApiHealth(host, protocol="http://"): + try: + url = '{protocol}{host}/health/no-log'.format(host=host, protocol=protocol) + apiRequest = requests.get(url, timeout = 2) + + res = {} + res['apiRequest'] = apiRequest + res['status'] = apiRequest.status_code + res['text'] = apiRequest.text + try: + res['json'] = apiRequest.json() + except: + res['json'] = None + except: + res = {} + res['funcName'] = 'checkApiHealth' + res['url'] = url + res['apiRequest'] = None + res['status'] = -1 + res['text'] = None + res['json'] = None + + return res + +def deleteBuild(host, build, protocol="http://"): + try: + url = '{protocol}{host}/build/delete/{build}'.format(host=host, build=build, protocol=protocol) + apiRequest = requests.post(url, timeout = 2) + + res = {} + res['apiRequest'] = apiRequest + res['status'] = apiRequest.status_code + res['text'] = apiRequest.text + try: + res['json'] = apiRequest.json() + except: + res['json'] = None + except: + res = {} + res['funcName'] = 'deleteBuild' + res['url'] = url + res['apiRequest'] = None + res['status'] = -1 + res['text'] = None + res['json'] = None + + return res + +def getTemplateBuildBootstrap(host, build, noFluff = True, protocol="http://"): + try: + url = '{protocol}{host}/templates/scripts/bootstrap'.format(host=host, protocol=protocol) + requestData = {} + requestData['options'] = {} + requestData['options']['build'] = build + + if noFluff: + requestData['options']['nofluff'] = True + + apiRequest = requests.post(url = url, json = requestData, timeout = 2) + + res = {} + res['apiRequest'] = apiRequest + res['status'] = apiRequest.status_code + res['text'] = apiRequest.text + try: + res['json'] = apiRequest.json() + except: + res['json'] = None + except: + res = {} + res['funcName'] = 'getTemplateBuildBootstrap' + res['url'] = url + res['body'] = requestData + res['apiRequest'] = None + res['status'] = -1 + res['text'] = None + res['json'] = None + + return res + +def getPreviousBuildList(host, index = None, limit = None, protocol="http://"): + try: + requestParams = '' + if not index == None: + requestParams = requestParams + '/index/{index}'.format(index=index) + + if not limit == None: + requestParams = requestParams + '/limit/{limit}'.format(limit=limit) + + url = '{protocol}{host}/build/list{requestParams}'.format(host=host, requestParams=requestParams, protocol=protocol) + + apiRequest = requests.get(url, timeout = 2) + + res = {} + res['apiRequest'] = apiRequest + res['status'] = apiRequest.status_code + res['text'] = apiRequest.text + try: + res['json'] = apiRequest.json() + except: + res['json'] = None + except: + res = {} + res['funcName'] = 'getPreviousBuildList' + res['url'] = url + res['apiRequest'] = None + res['status'] = -1 + res['text'] = None + res['json'] = None + + return res + +def checkWuiState(host, protocol="http://"): + try: + apiRequest = requests.get('{protocol}{host}/'.format(host=host, protocol=protocol), timeout = 2) + + res = {} + res['apiRequest'] = apiRequest + res['status'] = apiRequest.status_code + res['text'] = apiRequest.text + res['json'] = None + except: + res = {} + res['funcName'] = 'checkWuiState' + res['apiRequest'] = None + res['status'] = -1 + res['text'] = None + res['json'] = None + + return res + +def getBuildServicesList(host, protocol="http://"): + try: + url = '{protocol}{host}/templates/services/list'.format(host=host, protocol=protocol) + + apiRequest = requests.get(url, timeout = 2) + + res = {} + res['apiRequest'] = apiRequest + res['status'] = apiRequest.status_code + res['text'] = apiRequest.text + try: + res['json'] = apiRequest.json() + except: + res['json'] = None + except: + res = {} + res['funcName'] = 'getBuildServicesList' + res['url'] = url + res['apiRequest'] = None + res['status'] = -1 + res['text'] = None + res['json'] = None + + return res + +def getBuildServicesJsonList(host, protocol="http://"): + try: + url = '{protocol}{host}/templates/services/json'.format(host=host, protocol=protocol) + + apiRequest = requests.get(url, timeout = 2) + + res = {} + res['apiRequest'] = apiRequest + res['status'] = apiRequest.status_code + res['text'] = apiRequest.text + try: + res['json'] = apiRequest.json() + except: + res['json'] = None + except: + res = {} + res['funcName'] = 'getBuildServicesJsonList' + res['url'] = url + res['apiRequest'] = None + res['status'] = -1 + res['text'] = None + res['json'] = None + + return res + +def getBuildServicesYamlList(host, protocol="http://"): + try: + url = '{protocol}{host}/templates/services/yaml'.format(host=host, protocol=protocol) + + apiRequest = requests.get(url, timeout = 2) + + res = {} + res['apiRequest'] = apiRequest + res['status'] = apiRequest.status_code + res['text'] = apiRequest.text + res['json'] = None + except: + res = {} + res['funcName'] = 'getBuildServicesYamlList' + res['url'] = url + res['apiRequest'] = None + res['status'] = -1 + res['text'] = None + res['json'] = None + + return res + +def getBuildServicesMetaData(host, protocol="http://"): + try: + url = '{protocol}{host}/config/meta'.format(host=host, protocol=protocol) + + apiRequest = requests.get(url, timeout = 2) + + res = {} + res['apiRequest'] = apiRequest + res['status'] = apiRequest.status_code + res['text'] = apiRequest.text + try: + res['json'] = apiRequest.json() + except: + res['json'] = None + except: + res = {} + res['funcName'] = 'getBuildServicesMetaData' + res['url'] = url + res['apiRequest'] = None + res['status'] = -1 + res['text'] = None + res['json'] = None + + return res + +def getBuildServicesOptionsData(host, protocol="http://"): + try: + url = '{protocol}{host}/config/options'.format(host=host, protocol=protocol) + + apiRequest = requests.get(url, timeout = 2) + + res = {} + res['apiRequest'] = apiRequest + res['status'] = apiRequest.status_code + res['text'] = apiRequest.text + try: + res['json'] = apiRequest.json() + except: + res['json'] = None + except: + res = {} + res['funcName'] = 'getBuildServicesOptionsData' + res['url'] = url + res['apiRequest'] = None + res['status'] = -1 + res['text'] = None + res['json'] = None + + return res + +def saveBuild(host, selectedServices, configurations = {}, protocol="http://"): + try: + url = '{protocol}{host}/build/save'.format(host=host, protocol=protocol) + requestData = {} + requestData['buildOptions'] = {} + requestData['buildOptions']['selectedServices'] = selectedServices + requestData['buildOptions']['configurations'] = configurations + + apiRequest = requests.post(url = url, json = requestData, timeout = 2) + + res = {} + res['apiRequest'] = apiRequest + res['status'] = apiRequest.status_code + res['text'] = apiRequest.text + try: + res['json'] = apiRequest.json() + except: + res['json'] = None + except: + res = {} + res['funcName'] = 'saveBuild' + res['url'] = url + res['apiRequest'] = None + res['status'] = -1 + res['text'] = None + res['json'] = None + + return res + +def checkBuild(host, selectedServices, configurations = {}, protocol="http://"): + try: + requestData = {} + requestData['buildOptions'] = {} + requestData['buildOptions']['selectedServices'] = selectedServices + requestData['buildOptions']['configurations'] = configurations + url = '{protocol}{host}/build/dryrun'.format(host=host, protocol=protocol) + + apiRequest = requests.post(url = url, json = requestData, timeout = 2) + + res = {} + res['apiRequest'] = apiRequest + res['status'] = apiRequest.status_code + res['text'] = apiRequest.text + try: + res['json'] = apiRequest.json() + except: + res['json'] = None + except: + res = {} + res['funcName'] = 'checkBuild' + res['url'] = url + res['apiRequest'] = None + res['status'] = -1 + res['text'] = None + res['json'] = None + + return res diff --git a/scripts/deps/chars.py b/.internal/pycli/deps/chars.py old mode 100755 new mode 100644 similarity index 93% rename from scripts/deps/chars.py rename to .internal/pycli/deps/chars.py index 51cfa0d77..096275cce --- a/scripts/deps/chars.py +++ b/.internal/pycli/deps/chars.py @@ -40,7 +40,7 @@ } } -def commonTopBorder(renderMode, size=80): +def commonTopBorder(renderMode, size=62): output = "" output += "{btl}".format(btl=specialChars[renderMode]["borderTopLeft"]) for i in range(size): @@ -48,7 +48,7 @@ def commonTopBorder(renderMode, size=80): output += "{btr}".format(btr=specialChars[renderMode]["borderTopRight"]) return output -def commonBottomBorder(renderMode, size=80): +def commonBottomBorder(renderMode, size=62): output = "" output += "{bbl}".format(bbl=specialChars[renderMode]["borderBottomLeft"]) for i in range(size): @@ -63,7 +63,7 @@ def padText(text, size=45): output += " " return output -def commonEmptyLine(renderMode, size=80): +def commonEmptyLine(renderMode, size=62): output = "" output += "{bv}".format(bv=specialChars[renderMode]["borderVertical"]) for i in range(size): diff --git a/.internal/pycli/deps/host_exec.py b/.internal/pycli/deps/host_exec.py new file mode 100644 index 000000000..dc8267f89 --- /dev/null +++ b/.internal/pycli/deps/host_exec.py @@ -0,0 +1,23 @@ +import os +import subprocess + +hostUser = os.getenv('HOSTUSER') +iotstackPwd = os.getenv('IOTSTACKPWD') +hostAddress = os.getenv('HOSTSSH_ADDR') +hostPort = os.getenv('HOSTSSH_PORT') + +def getCommandString(command, iotstackPwd=iotstackPwd, hostUser=hostUser, hostAddress=hostAddress): + return "ssh -t -o StrictHostKeychecking=no -o ConnectTimeout=5 {hostUser}@{hostAddress} -p {hostPort} 'cd {iotstackPwd} && {command}' 2> /dev/null".format( + hostUser=hostUser, + iotstackPwd=iotstackPwd, + hostAddress=hostAddress, + hostPort=hostPort, + command=command + ) + +def execSilent(command): + execRes = subprocess.check_output(getCommandString(command), stderr=subprocess.PIPE, shell=True) + return execRes.decode('ascii').strip() + +def execInteractive(command): + return subprocess.call(getCommandString(command), shell=True) diff --git a/scripts/docker_commands.py b/.internal/pycli/docker_commands.py old mode 100755 new mode 100644 similarity index 83% rename from scripts/docker_commands.py rename to .internal/pycli/docker_commands.py index e3bfa9c97..40ec7358c --- a/scripts/docker_commands.py +++ b/.internal/pycli/docker_commands.py @@ -6,7 +6,7 @@ def main(): from deps.chars import specialChars, commonTopBorder, commonBottomBorder, commonEmptyLine import math import time - import subprocess + from deps.host_exec import execInteractive global dockerCommandsSelectionInProgress global renderMode @@ -31,7 +31,7 @@ def onResize(sig, action): def startStack(): print("Start Stack:") print("docker-compose up -d --remove-orphans") - subprocess.call("docker-compose up -d", shell=True) + execInteractive('docker-compose up -d --remove-orphans') print("") print("Stack Started") input("Process terminated. Press [Enter] to show menu and continue.") @@ -42,11 +42,12 @@ def restartStack(): print("Restarting Stack...") print("Stop Stack:") print("docker-compose down") + execInteractive('docker-compose down') subprocess.call("docker-compose down", shell=True) print("") print("Start Stack:") print("docker-compose up -d --remove-orphans") - subprocess.call("docker-compose up -d", shell=True) + execInteractive('docker-compose up -d --remove-orphans') # print("docker-compose restart") # subprocess.call("docker-compose restart", shell=True) print("") @@ -58,7 +59,7 @@ def restartStack(): def stopStack(): print("Stop Stack:") print("docker-compose down") - subprocess.call("docker-compose down", shell=True) + execInteractive('docker-compose down') print("") print("Stack Stopped") input("Process terminated. Press [Enter] to show menu and continue.") @@ -66,18 +67,22 @@ def stopStack(): return True def stopAllStack(): + print("Menu containers will shutdown") + time.sleep(0.5) print("Stop All Stack:") + time.sleep(0.2) print("docker container stop $(docker container ls -aq)") - subprocess.call("docker container stop $(docker container ls -aq)", shell=True) + time.sleep(0.2) + execInteractive('docker container stop $(docker container ls -aq)') print("") input("Process terminated. Press [Enter] to show menu and continue.") needsRender = 1 return True def pruneVolumes(): - print("Stop All Stack:") - print("docker container stop $(docker container ls -aq)") - subprocess.call("docker container stop $(docker container ls -aq)", shell=True) + print("Prune Volumes:") + print("docker system prune --volumes") + execInteractive('docker system prune --volumes') print("") input("Process terminated. Press [Enter] to show menu and continue.") needsRender = 1 @@ -86,16 +91,16 @@ def pruneVolumes(): def updateAllContainers(): print("Update All Containers:") print("docker-compose down") - subprocess.call("docker-compose down", shell=True) + execInteractive('docker-compose down') print("") print("docker-compose pull") - subprocess.call("docker-compose pull", shell=True) + execInteractive('docker-compose pull') print("") print("docker-compose build") - subprocess.call("docker-compose build", shell=True) + execInteractive('docker-compose build') print("") print("docker-compose up -d") - subprocess.call("docker-compose up -d", shell=True) + execInteractive('docker-compose up -d') print("") input("Process terminated. Press [Enter] to show menu and continue.") needsRender = 1 @@ -104,7 +109,7 @@ def updateAllContainers(): def deleteAndPruneVolumes(): print("Delete and prune volumes:") print("docker system prune --volumes") - subprocess.call("docker system prune --volumes", shell=True) + execInteractive('docker system prune --volumes') print("") input("Process terminated. Press [Enter] to show menu and continue.") needsRender = 1 @@ -113,7 +118,7 @@ def deleteAndPruneVolumes(): def deleteAndPruneImages(): print("Delete and prune volumes:") print("docker image prune -a") - subprocess.call("docker image prune -a", shell=True) + execInteractive('docker image prune -a') print("") input("Process terminated. Press [Enter] to show menu and continue.") needsRender = 1 @@ -126,7 +131,7 @@ def monitorLogs(): print("") print("docker-compose logs -f") time.sleep(0.5) - subprocess.call("docker-compose logs -f", shell=True) + execInteractive('docker-compose logs -f') print("") time.sleep(0.5) input("Process terminated. Press [Enter] to show menu and continue.") @@ -147,7 +152,7 @@ def goBack(): ["Monitor Logs", monitorLogs], ["Stop ALL running docker containers", stopAllStack], ["Update all containers (may take a long time)", updateAllContainers], - ["Delete all stopped containers and docker volumes (prune volumes)", deleteAndPruneVolumes], + ["Delete all stopped containers and prune volumes", deleteAndPruneVolumes], ["Delete all images not associated with container", deleteAndPruneImages], ["Back", goBack] ] @@ -164,7 +169,7 @@ def goBack(): def renderHotZone(term, menu, selection, hotzoneLocation): print(term.move(hotzoneLocation[0], hotzoneLocation[1])) - lineLengthAtTextStart = 71 + lineLengthAtTextStart = 53 for (index, menuItem) in enumerate(menu): toPrint = "" @@ -195,7 +200,7 @@ def mainRender(needsRender, menu, selection): print("") print(term.center(commonTopBorder(renderMode))) print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Select Docker Command to run {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} Select Docker Command to run {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) print(term.center(commonEmptyLine(renderMode))) print(term.center(commonEmptyLine(renderMode))) print(term.center(commonEmptyLine(renderMode))) @@ -208,15 +213,15 @@ def mainRender(needsRender, menu, selection): if not hideHelpText: if term.height < 30: print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Not enough vertical room to render controls help text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} Not enough vertical room to render controls help text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) print(term.center(commonEmptyLine(renderMode))) else: print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Enter] to run command {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Escape] to go back to main menu {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [Enter] to run command {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [Escape] to go back to main menu {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) print(term.center(commonEmptyLine(renderMode))) print(term.center(commonEmptyLine(renderMode))) print(term.center(commonBottomBorder(renderMode))) diff --git a/.internal/pycli/entry.py b/.internal/pycli/entry.py new file mode 100644 index 000000000..b90c35da5 --- /dev/null +++ b/.internal/pycli/entry.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 + +# export API_ADDR=localhost:32128 && export HOST_CON_API=localhost:32128 && cd .internal/pycli; nodemon --no-stdin --exec python3 entry.py + +import blessed +import yaml +import ruamel.yaml +from deps.host_exec import execSilent +import subprocess +import os +import sys +import traceback + +hostUser = os.getenv('HOSTUSER') +iotstackPwd = os.getenv('IOTSTACKPWD') +hostAddress = os.getenv('HOSTSSH_ADDR') +apiAddress = os.getenv('API_ADDR') +wuiAddress = os.getenv('WUI_ADDR') +hostPort = os.getenv('HOSTSSH_PORT') +doRemoteCheck = True # Don't commit if this is False + +print('blessed Version:', blessed.__version__) +print('ruamel.yaml Version:', ruamel.yaml.__version__) +print('PyYAML Version:', yaml.__version__) +print('') +print('hostUser: ', hostUser) +print('iotstackPwd: ', iotstackPwd) +print('API Address: ', apiAddress) +print('WUI Address: ', wuiAddress) +print('SSH hostAddress: ', hostAddress) +print('SSH hostPort: ', hostPort) + +sshState = '' + +for x in sys.argv[1:]: + if x == '--no-remote': + doRemoteCheck = False + +print('') +if doRemoteCheck: + try: + print('Checking connectivity to host...') + touchRes = execSilent('touch ./.tmp/rtest.file') + touchRes = execSilent('echo "exec success" >> ./.tmp/rtest.file') + readRes = execSilent('cat ./.tmp/rtest.file') + rmRes = execSilent('rm ./.tmp/rtest.file') + if readRes == 'exec success': + sshState = '--ssh' + print('Connection and remote command execution successful') + else: + sshState = '--no-ssh' + print('Error attempting to execute commands on the host. You may need to regenerate SSH keys by running:') + print(' ./menu.sh --run-env-setup') + print('') + print('Or configure SSH to use correct ports.') + input("Press Enter to continue to menu...") + + except Exception: + sshState = '--no-ssh' + print('Error attempting to execute commands on the host. You may need to regenerate SSH keys by running:') + print(' ./menu.sh --run-env-setup') + print('') + print('Check that SSH is running on your host or configure SSH to use correct ports.') + print('') + print('Error reported:') + print(sys.exc_info()) + traceback.print_exc() + print('') + input("Press Enter to continue to menu...") + print('') + else: + print("Skipping remote check") + + print("Loading IOTstack PyCLI menu...") + +os.system('python3 menu_main.py {sshState}'.format(sshState=sshState)) diff --git a/.internal/pycli/install_build.py b/.internal/pycli/install_build.py new file mode 100644 index 000000000..c45a8f39b --- /dev/null +++ b/.internal/pycli/install_build.py @@ -0,0 +1,321 @@ +#!/usr/bin/env python3 +import signal + +checkedMenuItems = [] +results = {} + +def main(): + import os + import time + import math + import sys + import subprocess + from deps.chars import specialChars, commonTopBorder, commonBottomBorder, commonEmptyLine, padText + from deps.host_exec import execInteractive + # from deps.consts import servicesDirectory, templatesDirectory, volumesDirectory, buildCache, envFile, dockerPathOutput, servicesFileName, composeOverrideFile + from deps.api import getPreviousBuildList, checkWuiState, getTemplateBuildBootstrap + from blessed import Terminal + global signal + global renderMode + global term + global paginationSize + global paginationStartIndex + global hideHelpText + global activeMenuLocation + global lastSelection + global apiPreviousBuildList + global wuiState + + # Runtime vars + menu = [] + term = Terminal() + paginationToggle = [3, int(term.height - 24)] # Top text + controls text + paginationStartIndex = 0 + paginationSize = paginationToggle[0] + activeMenuLocation = 0 + lastSelection = 0 + + try: # If not already set, then set it. + hideHelpText = hideHelpText + except: + hideHelpText = False + + def getMenuItem(selectedIndex): + if selectedIndex >= 0: + for (index, menuItem) in enumerate(menu): + if index == selectedIndex: + return menu[selectedIndex] + + def getBuildList(): + global apiPreviousBuildList + apiPreviousBuildList = getPreviousBuildList(os.getenv('API_ADDR')) + return apiPreviousBuildList + + def buildListToMenuItems(): + if 'buildsList' in apiPreviousBuildList['json']: + for (index, build) in enumerate(apiPreviousBuildList['json']['buildsList']): + menu.append([build]) + + def generateLineText(text, textLength=None, paddingBefore=0, lineLength=26): + result = "" + for i in range(paddingBefore): + result += " " + + textPrintableCharactersLength = textLength + + if (textPrintableCharactersLength) == None: + textPrintableCharactersLength = len(text) + + result += text + remainingSpace = lineLength - textPrintableCharactersLength + + for i in range(remainingSpace): + result += " " + + return result + + def renderHotZone(term, renderType, menu, selection, paddingBefore, titleRenderStart, linesBelowHotzone): + global paginationSize + selectedTextLength = len("-> ") + + print(term.move(titleRenderStart + 5, 0)) + + if paginationStartIndex >= 1: + print(term.center("{b} {uaf} {uaf}{uaf}{uaf} {ual} {b}".format( + b=specialChars[renderMode]["borderVertical"], + uaf=specialChars[renderMode]["upArrowFull"], + ual=specialChars[renderMode]["upArrowLine"] + ))) + else: + print(term.center(commonEmptyLine(renderMode))) + + print(term.center(commonEmptyLine(renderMode))) + print(term.center(commonEmptyLine(renderMode))) + print(term.center(commonEmptyLine(renderMode))) + + print(term.move(titleRenderStart + 6, 0)) + + menuItemsActiveRow = term.get_location()[0] + if renderType == 2 or renderType == 1: # Rerender entire hotzone + if len(menu) < 1: + emptyWarningText = "You have no builds." + paddedLineText = generateLineText(emptyWarningText, textLength=len(emptyWarningText), paddingBefore=paddingBefore - 2) + toPrint = paddedLineText + + rightPad = '' + for i in range(30): + rightPad += " " + toPrint = toPrint + rightPad + + toPrint = "{bv} {toPrint} {bv}".format(bv=specialChars[renderMode]["borderVertical"], toPrint=toPrint) # Generate border + toPrint = term.center(toPrint) # Center Text (All lines should have the same amount of printable characters) + print(toPrint) + + emptyWarningText = "A build can be created with the WUI or this menu." + paddedLineText = generateLineText(emptyWarningText, textLength=len(emptyWarningText), paddingBefore=paddingBefore - 2) + toPrint = paddedLineText + toPrint = "{bv} {toPrint} {bv}".format(bv=specialChars[renderMode]["borderVertical"], toPrint=toPrint) # Generate border + toPrint = term.center(toPrint) # Center Text (All lines should have the same amount of printable characters) + print(toPrint) + + emptyWarningText = "Press [Escape] to go back." + paddedLineText = generateLineText(emptyWarningText, textLength=len(emptyWarningText), paddingBefore=paddingBefore - 2) + toPrint = paddedLineText + rightPad = '' + for i in range(29): + rightPad += " " + toPrint = toPrint + rightPad + toPrint = "{bv} {toPrint} {bv}".format(bv=specialChars[renderMode]["borderVertical"], toPrint=toPrint) # Generate border + toPrint = term.center(toPrint) # Center Text (All lines should have the same amount of printable characters) + print(toPrint) + + else: + for (index, menuItem) in enumerate(menu): # Menu loop + if index >= paginationStartIndex and index < paginationStartIndex + paginationSize: + lineText = generateLineText(menuItem[0], paddingBefore=paddingBefore) + + # Menu highlight logic + if index == selection: + activeMenuLocation = term.get_location()[0] + formattedLineText = '-> {t.blue_on_green}{title}{t.normal} <-'.format(t=term, title=menuItem[0]) + paddedLineText = generateLineText(formattedLineText, textLength=len(menuItem[0]) + selectedTextLength, paddingBefore=paddingBefore - selectedTextLength) + toPrint = paddedLineText + else: + toPrint = '{title}{t.normal}'.format(t=term, title=lineText) + + leftPad = '' + rightPad = '' + + for i in range(8): + leftPad += " " + + for i in range(21): + rightPad += " " + + toPrint = leftPad + toPrint + rightPad + + toPrint = "{bv} {toPrint} {bv}".format(bv=specialChars[renderMode]["borderVertical"], toPrint=toPrint) # Generate border + toPrint = term.center(toPrint) # Center Text (All lines should have the same amount of printable characters) + + print(toPrint) + + if paginationStartIndex + paginationSize < len(menu): + print(term.center("{b} {dal} {daf}{daf}{daf} {dal} {b}".format( + b=specialChars[renderMode]["borderVertical"], + daf=specialChars[renderMode]["downArrowFull"], + dal=specialChars[renderMode]["downArrowLine"] + ))) + else: + print(term.center(commonEmptyLine(renderMode))) + + def mainRender(menu, selection, renderType = 1): + global paginationStartIndex + global paginationSize + paddingBefore = 4 + + if selection >= paginationStartIndex + paginationSize: + paginationStartIndex = selection - (paginationSize - 1) + 1 + renderType = 1 + + if selection <= paginationStartIndex - 1: + paginationStartIndex = selection + renderType = 1 + + try: + titleRenderStart = 0 if term.height < 32 else 2 + linesBelowHotzone = 2 if hideHelpText else 12 + + if (renderType == 1): + print(term.clear()) + print(term.move_y(titleRenderStart)) + print(term.black_on_cornsilk4(term.center('IOTstack Build Installer Menu (w: {w}, h: {h})'.format(h=term.height, w=term.width)))) + print("") + print(term.center(commonTopBorder(renderMode))) + + print(term.center(commonEmptyLine(renderMode))) + print(term.center("{bv} Select build to install {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center(commonEmptyLine(renderMode))) + print(term.center(commonEmptyLine(renderMode))) + print(term.center(commonEmptyLine(renderMode))) + print(term.center(commonEmptyLine(renderMode))) + + renderHotZone(term, renderType, menu, selection, paddingBefore, titleRenderStart, linesBelowHotzone) + + if (renderType == 1): + print(term.center(commonEmptyLine(renderMode))) + if not hideHelpText: + room = term.height - (6 + paginationSize + linesBelowHotzone) + if room < 0: + print(term.center(commonEmptyLine(renderMode))) + print(term.center("{bv} Not enough vertical room to render text ({th}, {rm}) {bv}".format(bv=specialChars[renderMode]["borderVertical"], th=padText(str(term.height), 3), rm=padText(str(room), 3)))) + print(term.center(commonEmptyLine(renderMode))) + else: + print(term.center(commonEmptyLine(renderMode))) + print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [Tab] Expand or collapse build menu size {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [R] Reload build list {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [Enter] to install build {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [Escape] to cancel build install {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center(commonEmptyLine(renderMode))) + print(term.center(commonEmptyLine(renderMode))) + print(term.center(commonBottomBorder(renderMode))) + + except Exception as err: + print("There was an error rendering the menu:") + print(err) + print("Press [Esc] to go back") + return + + return + + def downloadBootstrapScript(build): + print(term.clear()) + print('Downloading bootstrap script for build {build}...'.format(build=build)) + time.sleep(1) + scriptToExec = getTemplateBuildBootstrap(os.getenv('HOST_CON_API'), build) + if scriptToExec['text'] == None: + print('Something went wrong getting the script from the API.') + print(scriptToExec) + input("Press Enter to continue to menu...") + mainRender(menu, selection, 1) + return True + print('Install Build. Executing:') + print(scriptToExec['text']) + print('') + input("Press Enter to run this command or ctrl+c to exit") + print('') + print(execInteractive(scriptToExec['text'])) + print('') + print('Install script finished') + input("Press Enter to continue to menu...") + mainRender(menu, selection, 1) + + def onResize(sig, action): + global paginationToggle + global paginationSize + paginationToggle = [3, int(term.height - 24)] + paginationSize = paginationToggle[1] if not paginationSize == paginationToggle[0] else paginationToggle[0] + + mainRender(menu, selection, 1) + + if __name__ == 'builtins': + global results + global signal + needsRender = 1 + signal.signal(signal.SIGWINCH, onResize) + with term.fullscreen(): + print('Loading build list...') + getBuildList() + buildListToMenuItems() + selection = 0 + mainRender(menu, selection, 1) + selectionInProgress = True + with term.cbreak(): + while selectionInProgress: + key = term.inkey(esc_delay=0.05) + if key.is_sequence: + if key.name == 'KEY_TAB': + needsRender = 1 + if paginationSize == paginationToggle[0]: + paginationSize = paginationToggle[1] + paginationStartIndex = 0 + else: + paginationSize = paginationToggle[0] + if key.name == 'KEY_DOWN': + selection += 1 + needsRender = 2 + if key.name == 'KEY_UP': + selection -= 1 + needsRender = 2 + if key.name == 'KEY_ENTER': + downloadBootstrapScript(getMenuItem(selection)[0]) + if key.name == 'KEY_ESCAPE': + results["buildState"] = False + return results["buildState"] + elif key: + if key == 'r': # R pressed + menu = [] + print('Refreshing build list...') + getBuildList() + buildListToMenuItems() + mainRender(menu, selection, 1) + if key == 'h': # H pressed + if hideHelpText: + hideHelpText = False + else: + hideHelpText = True + needsRender = 1 + else: + print(key) + time.sleep(0.5) + + if len(menu) > 0: + selection = selection % len(menu) + + mainRender(menu, selection, needsRender) + +originalSignalHandler = signal.getsignal(signal.SIGINT) +main() +signal.signal(signal.SIGWINCH, originalSignalHandler) diff --git a/scripts/menu_main.py b/.internal/pycli/menu_main.py old mode 100755 new mode 100644 similarity index 52% rename from scripts/menu_main.py rename to .internal/pycli/menu_main.py index b4dfc17ba..b445af9c0 --- a/scripts/menu_main.py +++ b/.internal/pycli/menu_main.py @@ -1,4 +1,7 @@ -#!/usr/bin/python3 +#!/usr/bin/env python3 + +# export API_ADDR=localhost:32128 && export HOST_CON_API=localhost:32128 && cd .internal/pycli; nodemon --no-stdin --exec python3 entry.py + from blessed import Terminal import sys import subprocess @@ -6,24 +9,40 @@ import time import types import signal +from multiprocessing import Process, Manager from deps.chars import specialChars -from deps.version_check import checkVersion +from deps.api import checkApiHealth, checkWuiState term = Terminal() -# Settings/Consts -requiredDockerVersion = "18.2.0" +apiUrl = os.getenv('API_ADDR') +wuiUrl = os.getenv('WUI_ADDR') +sshUri = os.getenv('HOSTSSH_ADDR') # Vars selectionInProgress = True currentMenuItemIndex = 0 menuNavigateDirection = 0 -projectStatusPollRateRefresh = 1 -promptFiles = False -buildComplete = None -hotzoneLocation = [((term.height // 16) + 6), 0] +screenRefreshRate = 1 +hotzoneLocation = [int(term.height ** 0.3), 0] screenActive = True +manager = Manager() + +apiHealthResults = manager.dict() +apiHealthResults['status'] = None + +wuiHealthResults = manager.dict() +wuiHealthResults['status'] = None + +sshState = 'unknown' + +for x in sys.argv[1:]: + if x == '--no-ssh': + sshState = 'no' + if x == '--ssh': + sshState = 'yes' + # Render Modes: # 0 = No render needed # 1 = Full render @@ -83,6 +102,9 @@ def onResize(sig, action): global mainMenuList global currentMenuItemIndex global screenActive + global hotzoneLocation + + hotzoneLocation = [int(term.height ** 0.4), 0] if screenActive: mainRender(1, mainMenuList, currentMenuItemIndex) @@ -98,7 +120,7 @@ def buildStack(): global screenActive buildComplete = None - buildstackFilePath = "./scripts/buildstack_menu.py" + buildstackFilePath = "./buildstack_menu.py" with open(buildstackFilePath, "rb") as pythonDynamicImportFile: code = compile(pythonDynamicImportFile.read(), buildstackFilePath, "exec") execGlobals = { @@ -113,25 +135,26 @@ def buildStack(): screenActive = True needsRender = 1 -def runExampleMenu(): - exampleMenuFilePath = "./.templates/example_template/example_build.py" - with open(exampleMenuFilePath, "rb") as pythonDynamicImportFile: - code = compile(pythonDynamicImportFile.read(), exampleMenuFilePath, "exec") +def installBuild(): + global needsRender + installBuildFilePath = "./install_build.py" + with open(installBuildFilePath, "rb") as pythonDynamicImportFile: + code = compile(pythonDynamicImportFile.read(), installBuildFilePath, "exec") # execGlobals = globals() + # execLocals = locals() execGlobals = { "renderMode": renderMode } - execLocals = locals() - execGlobals["currentServiceName"] = 'SERVICENAME' - execGlobals["toRun"] = 'runOptionsMenu' + execLocals = {} screenActive = False exec(code, execGlobals, execLocals) signal.signal(signal.SIGWINCH, onResize) screenActive = True + needsRender = 1 def dockerCommands(): global needsRender - dockerCommandsFilePath = "./scripts/docker_commands.py" + dockerCommandsFilePath = "./docker_commands.py" with open(dockerCommandsFilePath, "rb") as pythonDynamicImportFile: code = compile(pythonDynamicImportFile.read(), dockerCommandsFilePath, "exec") # execGlobals = globals() @@ -148,7 +171,7 @@ def dockerCommands(): def miscCommands(): global needsRender - dockerCommandsFilePath = "./scripts/misc_commands.py" + dockerCommandsFilePath = "./misc_commands.py" with open(dockerCommandsFilePath, "rb") as pythonDynamicImportFile: code = compile(pythonDynamicImportFile.read(), dockerCommandsFilePath, "exec") # execGlobals = globals() @@ -166,7 +189,7 @@ def miscCommands(): def nativeInstalls(): global needsRender global screenActive - dockerCommandsFilePath = "./scripts/native_installs.py" + dockerCommandsFilePath = "./native_installs.py" with open(dockerCommandsFilePath, "rb") as pythonDynamicImportFile: code = compile(pythonDynamicImportFile.read(), dockerCommandsFilePath, "exec") # currGlobals = globals() @@ -184,7 +207,7 @@ def nativeInstalls(): def backupAndRestore(): global needsRender global screenActive - dockerCommandsFilePath = "./scripts/backup_restore.py" + dockerCommandsFilePath = "./backup_restore.py" with open(dockerCommandsFilePath, "rb") as pythonDynamicImportFile: code = compile(pythonDynamicImportFile.read(), dockerCommandsFilePath, "exec") # currGlobals = globals() @@ -208,38 +231,9 @@ def skipItem(currentMenuItemIndex, direction): currentMenuItemIndex += lastSelectionDirection return currentMenuItemIndex -def deletePromptFiles(): - # global promptFiles - # global currentMenuItemIndex - if os.path.exists(".project_outofdate"): - os.remove(".project_outofdate") - if os.path.exists(".docker_outofdate"): - os.remove(".docker_outofdate") - if os.path.exists(".docker_notinstalled"): - os.remove(".docker_notinstalled") - promptFiles = False - currentMenuItemIndex = 0 - -def installDocker(): - print("Install Docker: curl -fsSL https://get.docker.com | sh && sudo usermod -aG docker $USER") - installDockerProcess = subprocess.Popen(['sudo', 'bash', './install_docker.sh', 'install'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - installDockerProcess.wait() - installDockerResult, stdError = installDockerProcess.communicate() - installDockerResult = installDockerResult.decode("utf-8").rstrip() - - return installDockerResult - -def upgradeDocker(): - print("Upgrade Docker: sudo apt upgrade docker docker-compose") - upgradeDockerProcess = subprocess.Popen(['sudo', 'bash', './install_docker.sh', 'upgrade'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - upgradeDockerProcess.wait() - upgradeDockerResult, stdError = upgradeDockerProcess.communicate() - upgradeDockerResult = upgradeDockerResult.decode("utf-8").rstrip() - - return upgradeDockerResult - baseMenu = [ - ["Build Stack", buildStack], + ["Install Builds", installBuild], + ["Create Build", buildStack], # Not yet ready ["Docker Commands", dockerCommands], ["Miscellaneous Commands", miscCommands], ["Backup and Restore", backupAndRestore], @@ -248,135 +242,81 @@ def upgradeDocker(): ["Exit", exitMenu] ] -# Main Menu -mainMenuList = baseMenu +def apiThreadInit(): + global apiHealthResults + apiCheckThread = Process(target=apiCheckWrapper, args=(apiHealthResults,)) + apiCheckThread.start() + # apiCheckThread.join() + +def apiCheckWrapper(apiHealthResults): + while True: + apiHealthResults = updateApiState() + time.sleep(5) + +def updateApiState(): + global apiHealthResults + apiHealthResults = checkApiHealth(apiUrl) + return apiHealthResults + +def updateWuiState(): + global wuiHealthResults + wuiHealthResults = checkWuiState(wuiUrl) + return wuiHealthResults + +def generateApiState(): + global apiHealthResults + rendered = '' + + if apiHealthResults['status'] == None: + rendered = (term.black_on_cornsilk2(' Checking... ({apiUrl}) '.format(apiUrl=apiUrl))) + return rendered + if apiHealthResults['status'] >= 200 and apiHealthResults['status'] < 299: + rendered = (term.blue_on_green2(' Online ({apiUrl}) '.format(apiUrl=apiUrl))) + else: + rendered = (term.red_on_black(' Offline ({apiUrl}) '.format(apiUrl=apiUrl))) -potentialMenu = { - "projectUpdate": { - "menuItem": ["Update IOTstack", installDocker], - "added": False - }, - "dockerUpdate": { # TODO: Do note use, fix shell issues first - "menuItem": ["Update Docker", upgradeDocker], - "added": False - }, - "dockerNotUpdated": { # TODO: Do note use, fix shell issues first - "menuItem": [term.red_on_black("Docker is not up to date"), doNothing, { "skip": True }], - "added": False - }, - "dockerTerminals": { # TODO: Do note use, not finished - "menuItem": ["Docker Terminals", doNothing], - "added": False - }, - "noProjectUpdate": { - "menuItem": [term.green_on_black("IOTstack is up to date"), doNothing, { "skip": True }], - "added": False - }, - "spacer": { - "menuItem": ["------", doNothing, { "skip": True }], - "added": False - }, - "newLine": { - "menuItem": [" ", doNothing, { "skip": True }], - "added": False - }, - "deletePromptFiles": { - "menuItem": ["Delete 'out of date' prompt files", deletePromptFiles], - "added": False - }, - "updatesCheck": { - "menuItem": [term.blue_on_black("Checking for updates..."), doNothing, { "skip": True }], - "added": False - } -} - -def checkDockerVersion(): - try: - getDockerVersion = subprocess.Popen(['docker', 'version', '-f', '"{{.Server.Version}}"'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - getDockerVersion.wait() - currentDockerVersion, stdError = getDockerVersion.communicate() - currentDockerVersion = currentDockerVersion.decode("utf-8").rstrip().replace('"', '') - except Exception as err: - print("Error attempting to run docker command:", err) - currentDockerVersion = "" - - return checkVersion(requiredDockerVersion, currentDockerVersion) - -def checkProjectUpdates(): - getCurrentBranch = subprocess.Popen(["git", "name-rev", "--name-only", "HEAD"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - getCurrentBranch.wait() - currentBranch, stdError = getCurrentBranch.communicate() - currentBranch = currentBranch.decode("utf-8").rstrip() - projectStatus = subprocess.Popen(["git", "fetch", "origin", currentBranch], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - - return projectStatus - -def addPotentialMenuItem(menuItemName, hasSpacer=True): - if (potentialMenu["newLine"]["added"] == False): - potentialMenu["newLine"]["added"] = True - baseMenu.append(potentialMenu["newLine"]["menuItem"]) - if hasSpacer and potentialMenu["spacer"]["added"] == False: - potentialMenu["spacer"]["added"] = True - baseMenu.append(potentialMenu["spacer"]["menuItem"]) - - if (potentialMenu[menuItemName]["added"] == False): - potentialMenu[menuItemName]["added"] = True - baseMenu.append(potentialMenu[menuItemName]["menuItem"]) - return True - - return False - -def removeMenuItemByLabel(potentialItemKey): - i = -1 - for menuItem in mainMenuList: - i += 1 - if menuItem[0] == potentialMenu[potentialItemKey]["menuItem"][0]: - potentialMenu[potentialItemKey]["added"] = False - mainMenuList.pop(i) - -def doPotentialMenuCheck(projectStatus, dockerVersion=True, promptFiles=False): - global needsRender + return rendered - if (promptFiles == True): - addPotentialMenuItem("deletePromptFiles") - needsRender = 2 +def generateWuiState(): + global wuiHealthResults + rendered = '' + + if wuiHealthResults['status'] == None: + rendered = (term.black_on_cornsilk2(' Checking... ({wuiUrl}) '.format(wuiUrl=wuiUrl))) + return rendered + if wuiHealthResults['status'] >= 200 and wuiHealthResults['status'] < 299: + rendered = (term.blue_on_green2(' Online ({wuiUrl}) '.format(wuiUrl=wuiUrl))) else: - removeMenuItemByLabel("deletePromptFiles") - - # if (projectStatus.poll() == None): - # addPotentialMenuItem("updatesCheck", False) - # needsRender = 2 - # else: - # removeMenuItemByLabel("updatesCheck") - - # if (projectStatus.poll() == 1): - # added = addPotentialMenuItem("projectUpdate") - # projectStatusPollRateRefresh = None - # if (added): - # needsRender = 1 - - # if (projectStatus.poll() == 0): - # added = addPotentialMenuItem("noProjectUpdate") - # projectStatusPollRateRefresh = None - # if (added): - # needsRender = 1 - - if (dockerVersion == False): - added = addPotentialMenuItem("dockerNotUpdated") - if (added): - needsRender = 1 - -def checkIfPromptFilesExist(): - if os.path.exists(".project_outofdate"): - return True - if os.path.exists(".docker_outofdate"): - return True - if os.path.exists(".docker_notinstalled"): - return True - return False + rendered = (term.red_on_black(' Offline ({wuiUrl}) '.format(wuiUrl=wuiUrl))) + + return rendered + +def generateSshState(): + global sshHealthResults + rendered = '' + + if sshState == 'unknown': + rendered = (term.black_on_cornsilk2(' Unknown ({sshUri}) '.format(sshUri=sshUri))) + return rendered + elif sshState == 'yes': + rendered = (term.blue_on_green2(' Online ({sshUri}) '.format(sshUri=sshUri))) + else: + rendered = (term.red_on_black(' Offline ({sshUri}) '.format(sshUri=sshUri))) + + return rendered + +# Main Menu +mainMenuList = baseMenu def renderHotZone(term, menu, selection): print(term.move(hotzoneLocation[0], hotzoneLocation[1])) + + print(term.center(term.black_on_cornsilk3('API {apiResults}'.format(apiResults=generateApiState())))) + print(term.center(term.black_on_cornsilk3('WUI {wuiResults}'.format(wuiResults=generateWuiState())))) + print(term.center(term.black_on_cornsilk3('SSH {sshResults}'.format(sshResults=generateSshState())))) + print('') + print('') + for (index, menuItem) in enumerate(menu): if index == selection: print(term.center('-> {t.blue_on_green}{title}{t.normal} <-'.format(t=term, title=menuItem[0]))) @@ -387,23 +327,13 @@ def mainRender(needsRender, menu, selection): term = Terminal() if needsRender == 1: print(term.clear()) - print(term.move_y(term.height // 16)) - print(term.black_on_cornsilk4(term.center('IOTstack Main Menu'))) + print(term.move_y(int(term.height ** 0.1))) + print(term.black_on_cornsilk4(term.center('IOTstack Main Menu (w: {w}, h: {h})'.format(h=term.height, w=term.width, apiResults=generateApiState())))) print("") if needsRender >= 1: renderHotZone(term, menu, selection) - - if (buildComplete and needsRender == 1): - print("") - print("") - print("") - print(term.center('{t.blue_on_green} {text} {t.normal}{t.white_on_black}{cPath} {t.normal}'.format(t=term, text="Build completed:", cPath=" ./docker-compose.yml"))) - print(term.center('{t.white_on_black}{text}{t.blue_on_green2} {commandString} {t.normal}'.format(t=term, text="You can start the stack from the Docker Commands menu, or from the CLI with: ", commandString="docker-compose up -d"))) - if os.path.exists('./compose-override.yml'): - print("") - print(term.center('{t.grey_on_blue4} {text} {t.normal}{t.white_on_black}{t.normal}'.format(t=term, text="'compose-override.yml' was merged into 'docker-compose.yml'"))) - print("") + checkApiHealth def runSelection(selection): global needsRender @@ -422,29 +352,25 @@ def isMenuItemSelectable(menu, index): # Entrypoint if __name__ == '__main__': - projectStatus = checkProjectUpdates() # Async - dockerVersion, reason, data = checkDockerVersion() - promptFiles = checkIfPromptFilesExist() term = Terminal() - + signal.signal(signal.SIGWINCH, onResize) + updateApiState() + updateWuiState() + with term.fullscreen(): checkRenderOptions() mainRender(needsRender, mainMenuList, currentMenuItemIndex) # Initial Draw with term.cbreak(): while selectionInProgress: menuNavigateDirection = 0 - if (promptFiles): - promptFiles = checkIfPromptFilesExist() if needsRender > 0: # Only rerender when changed to prevent flickering mainRender(needsRender, mainMenuList, currentMenuItemIndex) needsRender = 0 - - doPotentialMenuCheck(projectStatus=projectStatus, dockerVersion=dockerVersion, promptFiles=promptFiles) - key = term.inkey(timeout=projectStatusPollRateRefresh) + key = term.inkey(timeout=screenRefreshRate) if key.is_sequence: if key.name == 'KEY_TAB': menuNavigateDirection = 1 @@ -456,6 +382,16 @@ def isMenuItemSelectable(menu, index): runSelection(currentMenuItemIndex) if key.name == 'KEY_ESCAPE': exitMenu() + elif key: + if key == 'r': # R pressed + print('Refreshing...') + updateApiState() + updateWuiState() + checkRenderOptions() + needsRender = 1 + mainRender(needsRender, mainMenuList, currentMenuItemIndex) + if key == ' ': # Space pressed + runSelection(currentMenuItemIndex) if not menuNavigateDirection == 0: # If a direction was pressed, find next selectable item currentMenuItemIndex += menuNavigateDirection diff --git a/scripts/misc_commands.py b/.internal/pycli/misc_commands.py old mode 100755 new mode 100644 similarity index 86% rename from scripts/misc_commands.py rename to .internal/pycli/misc_commands.py index 8c311c5d6..9503b0c0f --- a/scripts/misc_commands.py +++ b/.internal/pycli/misc_commands.py @@ -5,11 +5,11 @@ def main(): from blessed import Terminal from deps.chars import specialChars, commonTopBorder, commonBottomBorder, commonEmptyLine - global renderMode import time - import subprocess + from deps.host_exec import execInteractive global signal + global renderMode global dockerCommandsSelectionInProgress global mainMenuList global currentMenuItemIndex @@ -25,7 +25,8 @@ def main(): def setSwapinessTo0(): print(term.clear()) print("Set swapiness to 0:") - subprocess.call("./scripts/disable_swap.sh disableswap", shell=True) + print("bash ./scripts/disable_swap.sh disableswap") + execInteractive('bash ./scripts/disable_swap.sh disableswap') print("") input("Process terminated. Press [Enter] to show menu and continue.") return True @@ -35,7 +36,8 @@ def uninstallSwapfile(): print("Disabling swap...") setSwapinessTo0() print("Uninstall Swapfile:") - subprocess.call("./scripts/disable_swap.sh uninstallswap", shell=True) + print("bash ./scripts/disable_swap.sh uninstallswap") + execInteractive('bash ./scripts/disable_swap.sh uninstallswap') print("") input("Process terminated. Press [Enter] to show menu and continue.") return True @@ -43,7 +45,8 @@ def uninstallSwapfile(): def installLog2Ram(): print(term.clear()) print("Install log2ram:") - subprocess.call("./scripts/install_log2ram.sh", shell=True) + print("bash ./scripts/install_log2ram.sh") + execInteractive('bash ./scripts/install_log2ram.sh') print("") input("Process terminated. Press [Enter] to show menu and continue.") return True @@ -52,7 +55,7 @@ def installGithubSshKeys(): print(term.clear()) print("Install Github SSH Keys:") print("bash ./scripts/install_ssh_keys.sh") - subprocess.call("bash ./scripts/install_ssh_keys.sh", shell=True) + execInteractive('bash ./scripts/host_installers/install_ssh_keys.sh') print("") input("Process terminated. Press [Enter] to show menu and continue.") return True @@ -90,7 +93,7 @@ def onResize(sig, action): mainRender(1, mainMenuList, currentMenuItemIndex) def renderHotZone(term, menu, selection, hotzoneLocation): - lineLengthAtTextStart = 71 + lineLengthAtTextStart = 53 print(term.move(hotzoneLocation[0], hotzoneLocation[1])) for (index, menuItem) in enumerate(menu): toPrint = "" @@ -118,7 +121,7 @@ def mainRender(needsRender, menu, selection): print("") print(term.center(commonTopBorder(renderMode))) print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Select Command to run {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} Select Command to run {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) print(term.center(commonEmptyLine(renderMode))) print(term.center(commonEmptyLine(renderMode))) print(term.center(commonEmptyLine(renderMode))) @@ -131,15 +134,15 @@ def mainRender(needsRender, menu, selection): if not hideHelpText: if term.height < 30: print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Not enough vertical room to render controls help text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} Not enough vertical room to render controls help text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) print(term.center(commonEmptyLine(renderMode))) else: print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Enter] to run command {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Escape] to go back to main menu {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [Enter] to run command {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [Escape] to go back to main menu {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) print(term.center(commonEmptyLine(renderMode))) print(term.center(commonEmptyLine(renderMode))) print(term.center(commonBottomBorder(renderMode))) diff --git a/scripts/native_installs.py b/.internal/pycli/native_installs.py old mode 100755 new mode 100644 similarity index 84% rename from scripts/native_installs.py rename to .internal/pycli/native_installs.py index 7e9e4280d..3cb427423 --- a/scripts/native_installs.py +++ b/.internal/pycli/native_installs.py @@ -4,10 +4,10 @@ def main(): from blessed import Terminal from deps.chars import specialChars, commonTopBorder, commonBottomBorder, commonEmptyLine - global renderMode + from deps.host_exec import execInteractive import time - import subprocess + global renderMode global signal global dockerCommandsSelectionInProgress global mainMenuList @@ -33,8 +33,8 @@ def onResize(sig, action): def installHassIo(): print(term.clear()) print("Install Home Assistant Supervisor") - print("./.native/hassio_supervisor.sh") - res = subprocess.call("./.native/hassio_supervisor.sh", shell=True) + print("bash ./scripts/host_installers/hassio_supervisor.sh") + res = execInteractive('bash ./scripts/host_installers/hassio_supervisor.sh') print("") if res == 0: print("Preinstallation complete. Your system may run slow for a few hours as Hass.io installs its services.") @@ -48,8 +48,8 @@ def installHassIo(): def installRtl433(): print(term.clear()) print("Install RTL_433") - print("bash ./.native/rtl_433.sh") - subprocess.call("bash ./.native/rtl_433.sh", shell=True) + print("bash ./scripts/host_installers/rtl_433.sh") + execInteractive('bash ./scripts/host_installers/rtl_433.sh') print("") input("Process terminated. Press [Enter] to show menu and continue.") return True @@ -57,8 +57,8 @@ def installRtl433(): def installRpiEasy(): print(term.clear()) print("Install RPIEasy") - print("bash ./.native/rpieasy.sh") - subprocess.call("bash ./.native/rpieasy.sh", shell=True) + print("bash ./scripts/host_installers/rpieasy.sh") + execInteractive('bash ./scripts/host_installers/rpieasy.sh') print("") input("Process terminated. Press [Enter] to show menu and continue.") return True @@ -67,8 +67,8 @@ def installDockerAndCompose(): print(term.clear()) print("Install docker") print("Install docker-compose") - print("bash ./scripts/install_docker.sh install") - subprocess.call("bash ./scripts/install_docker.sh install", shell=True) + print("bash ./scripts/host_installers/install_docker.sh install") + execInteractive('bash ./scripts/host_installers/install_docker.sh install') print("") input("Process terminated. Press [Enter] to show menu and continue.") return True @@ -77,8 +77,8 @@ def upgradeDockerAndCompose(): print(term.clear()) print("Install docker") print("Install docker-compose") - print("bash ./scripts/install_docker.sh upgrade") - subprocess.call("bash ./scripts/install_docker.sh upgrade", shell=True) + print("bash ./scripts/host_installers/install_docker.sh upgrade") + execInteractive('bash ./scripts/host_installers/install_docker.sh upgrade') print("") input("Process terminated. Press [Enter] to show menu and continue.") return True @@ -113,7 +113,7 @@ def goBack(): def renderHotZone(term, menu, selection, hotzoneLocation): print(term.move(hotzoneLocation[0], hotzoneLocation[1])) - lineLengthAtTextStart = 71 + lineLengthAtTextStart = 53 for (index, menuItem) in enumerate(menu): toPrint = "" @@ -141,7 +141,7 @@ def mainRender(needsRender, menu, selection): print("") print(term.center(commonTopBorder(renderMode))) print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Select service to install {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} Select service to install {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) print(term.center(commonEmptyLine(renderMode))) print(term.center(commonEmptyLine(renderMode))) print(term.center(commonEmptyLine(renderMode))) @@ -154,15 +154,15 @@ def mainRender(needsRender, menu, selection): if not hideHelpText: if term.height < 30: print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Not enough vertical room to render controls help text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} Not enough vertical room to render controls help text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) print(term.center(commonEmptyLine(renderMode))) else: print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Enter] to run command {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Escape] to go back to main menu {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [Enter] to run command {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [Escape] to go back to main menu {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) print(term.center(commonEmptyLine(renderMode))) print(term.center(commonEmptyLine(renderMode))) print(term.center(commonBottomBorder(renderMode))) diff --git a/.internal/pycli/requirements.txt b/.internal/pycli/requirements.txt new file mode 100644 index 000000000..fd0f5bd6d --- /dev/null +++ b/.internal/pycli/requirements.txt @@ -0,0 +1,5 @@ +ruamel.yaml ~= 0.16.12 +blessed ~= 1.17.5 +pyyaml ~= 5.3.1 +requests ~= 2.22.0 +asyncio ~= 3.4.3 diff --git a/.internal/pycli/serviceOptions/options_screen.py b/.internal/pycli/serviceOptions/options_screen.py new file mode 100644 index 000000000..a655f6e79 --- /dev/null +++ b/.internal/pycli/serviceOptions/options_screen.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python3 + +import signal + +buildSettingsConfig = {} +haltOnErrors = True + +# Main wrapper function. Required to make local vars work correctly +def main(): + import os + import time + import sys + import traceback + from blessed import Terminal + from deps.chars import specialChars, commonTopBorder, commonBottomBorder, commonEmptyLine, padText + # from deps.common_functions import getExternalPorts, getInternalPorts, checkPortConflicts, enterPortNumberWithWhiptail + + global signal + global apiBuildDockerCompose # The loaded memory YAML of all checked services + global apiBuildMetadata # The loaded memory YAML of all checked services + global apiBuildOptions # Passed in response from the API for this service + global apiServicesOptions # Passed in response from the API for all services + global currentServiceName # Name of the current service + global haltOnErrors # Turn on to allow erroring + global buildOptions # Store config changes for build + global menu + global renderMode + global hideHelpText + + menu = [] + + global validMenuItems # Used for returning the lits of valid menu items + validMenuItems = [] + + try: # If not already set, then set it. + hideHelpText = hideHelpText + except: + hideHelpText = False + + ############################ + # Menu Logic + ############################ + + global currentMenuItemIndex + global selectionInProgress + global menuNavigateDirection + global needsRender + global toRun + + selectionInProgress = True + currentMenuItemIndex = 0 + menuNavigateDirection = 0 + needsRender = 1 + term = Terminal() + hotzoneLocation = [((term.height // 16) + 6), 0] + + def goBack(): + global selectionInProgress + global needsRender + selectionInProgress = False + needsRender = 1 + return True + + def run_nodered_npmSelection(): + global buildOptions + global menu + execGlobals = { + "validMenuItems": [], + "currentServiceName": currentServiceName, + "apiBuildOptions": apiBuildOptions, + "apiServicesOptions": apiServicesOptions, + "renderMode": renderMode, + "buildOptions": buildOptions + } + execLocals = locals() + optionsScriptPath = "./serviceOptions/service_nodered_addons.py" + with open(optionsScriptPath, "rb") as pythonDynamicImportFile: + code = compile(pythonDynamicImportFile.read(), optionsScriptPath, "exec") + exec(code, execGlobals, execLocals) + mainRender(1, menu, 0) + + + def createMenuOptions(): + global menu + global apiBuildOptions + global validMenuItems + try: + configOptions = apiBuildOptions + if "nodered_npmSelection" in configOptions: + menu.append(["Select Addons", run_nodered_npmSelection]) + validMenuItems.append("nodered_npmSelection") + except: + pass + menu.append(["Go back", goBack]) + + def runOptionsMenu(): + createMenuOptions() + menuEntryPoint() + return True + + def renderHotZone(term, menu, selection, hotzoneLocation): + lineLengthAtTextStart = 53 + print(term.move(hotzoneLocation[0], hotzoneLocation[1])) + for (index, menuItem) in enumerate(menu): + toPrint = "" + if index == selection: + toPrint += ('{bv} -> {t.blue_on_green} {title} {t.normal} <-'.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) + else: + toPrint += ('{bv} {t.normal} {title} '.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) + + for i in range(lineLengthAtTextStart - len(menuItem[0])): + toPrint += " " + + toPrint += "{bv}".format(bv=specialChars[renderMode]["borderVertical"]) + + toPrint = term.center(toPrint) + + print(toPrint) + + def mainRender(needsRender, menu, selection): + global hideHelpText + term = Terminal() + + if needsRender == 1: + print(term.clear()) + print(term.move_y(term.height // 16)) + print(term.black_on_cornsilk4(term.center('IOTstack Grafana Options'))) + print("") + print(term.center(commonTopBorder(renderMode))) + print(term.center(commonEmptyLine(renderMode))) + print(term.center("{bv} Select Option to configure {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center(commonEmptyLine(renderMode))) + + if needsRender >= 1: + renderHotZone(term, menu, selection, hotzoneLocation) + + if needsRender == 1: + print(term.center(commonEmptyLine(renderMode))) + print(term.center(commonEmptyLine(renderMode))) + if not hideHelpText: + print(term.center(commonEmptyLine(renderMode))) + print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [Enter] to run command or save input {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [Escape] to go back to build stack menu {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center(commonEmptyLine(renderMode))) + print(term.center(commonEmptyLine(renderMode))) + print(term.center(commonBottomBorder(renderMode))) + + def runSelection(selection): + import types + global menu + if len(menu[selection]) > 1 and isinstance(menu[selection][1], types.FunctionType): + menu[selection][1]() + else: + print(term.green_reverse('IOTstack Error: No function assigned to menu item: "{}"'.format(menu[selection][0]))) + + def isMenuItemSelectable(menu, index): + if len(menu) > index: + if len(menu[index]) > 2: + if menu[index][2]["skip"] == True: + return False + return True + + def menuEntryPoint(): + # These need to be reglobalised due to eval() + global currentMenuItemIndex + global selectionInProgress + global menuNavigateDirection + global needsRender + global hideHelpText + global menu + term = Terminal() + with term.fullscreen(): + menuNavigateDirection = 0 + mainRender(needsRender, menu, currentMenuItemIndex) + selectionInProgress = True + with term.cbreak(): + while selectionInProgress: + menuNavigateDirection = 0 + + if needsRender: # Only rerender when changed to prevent flickering + mainRender(needsRender, menu, currentMenuItemIndex) + needsRender = 0 + + key = term.inkey(esc_delay=0.05) + if key.is_sequence: + if key.name == 'KEY_TAB': + menuNavigateDirection += 1 + if key.name == 'KEY_DOWN': + menuNavigateDirection += 1 + if key.name == 'KEY_UP': + menuNavigateDirection -= 1 + if key.name == 'KEY_LEFT': + goBack() + if key.name == 'KEY_ENTER': + runSelection(currentMenuItemIndex) + if key.name == 'KEY_ESCAPE': + return True + elif key: + if key == 'h': # H pressed + if hideHelpText: + hideHelpText = False + else: + hideHelpText = True + mainRender(1, menu, currentMenuItemIndex) + + if menuNavigateDirection != 0: # If a direction was pressed, find next selectable item + currentMenuItemIndex += menuNavigateDirection + currentMenuItemIndex = currentMenuItemIndex % len(menu) + needsRender = 2 + + while not isMenuItemSelectable(menu, currentMenuItemIndex): + currentMenuItemIndex += menuNavigateDirection + currentMenuItemIndex = currentMenuItemIndex % len(menu) + return True + + #################### + # End menu section + #################### + + if toRun == 'createMenuOptions' or toRun == 'runOptionsMenu': + if haltOnErrors: + eval(toRun)() + else: + try: + eval(toRun)() + except: + print("Issue running options:") + print(toRun) + print(err) + print(sys.exc_info()) + traceback.print_exc() + input("Press Enter to continue...") + else: + print("Invalid function call: ", toRun) + input("Press Enter to continue...") + + +originalSignalHandler = signal.getsignal(signal.SIGINT) +main() +signal.signal(signal.SIGWINCH, originalSignalHandler) diff --git a/.templates/nodered/addons.py b/.internal/pycli/serviceOptions/service_nodered_addons.py old mode 100755 new mode 100644 similarity index 69% rename from .templates/nodered/addons.py rename to .internal/pycli/serviceOptions/service_nodered_addons.py index 75571382c..8116e6823 --- a/.templates/nodered/addons.py +++ b/.internal/pycli/serviceOptions/service_nodered_addons.py @@ -1,32 +1,26 @@ #!/usr/bin/env python3 import signal +globals_results = {} def main(): from blessed import Terminal from deps.chars import specialChars, commonTopBorder, commonBottomBorder, commonEmptyLine - from deps.consts import servicesDirectory, templatesDirectory import time import subprocess - import ruamel.yaml import os global signal - global currentServiceName - global dockerCommandsSelectionInProgress global mainMenuList global currentMenuItemIndex + global currentServiceName global renderMode + global buildOptions global paginationSize global paginationStartIndex - global addonsFile + global apiBuildOptions global hideHelpText - global installCommand - - yaml = ruamel.yaml.YAML() - yaml.preserve_quotes = True - try: # If not already set, then set it. hideHelpText = hideHelpText except: @@ -38,10 +32,6 @@ def main(): paginationStartIndex = 0 paginationSize = paginationToggle[0] - serviceService = servicesDirectory + currentServiceName - serviceTemplate = templatesDirectory + currentServiceName - addonsFile = serviceTemplate + "/addons.yml" - def goBack(): global dockerCommandsSelectionInProgress global needsRender @@ -68,7 +58,7 @@ def onResize(sig, action): global currentMenuItemIndex mainRender(1, mainMenuList, currentMenuItemIndex) - def generateLineText(text, textLength=None, paddingBefore=0, lineLength=64): + def generateLineText(text, textLength=None, paddingBefore=0, lineLength=46): result = "" for i in range(paddingBefore): result += " " @@ -93,7 +83,7 @@ def renderHotZone(term, renderType, menu, selection, hotzoneLocation, paddingBef print(term.move(hotzoneLocation[0], hotzoneLocation[1])) if paginationStartIndex >= 1: - print(term.center("{b} {uaf} {uaf}{uaf}{uaf} {ual} {b}".format( + print(term.center("{b} {uaf} {uaf}{uaf}{uaf} {ual} {b}".format( b=specialChars[renderMode]["borderVertical"], uaf=specialChars[renderMode]["upArrowFull"], ual=specialChars[renderMode]["upArrowLine"] @@ -126,7 +116,7 @@ def renderHotZone(term, renderType, menu, selection, hotzoneLocation, paddingBef print(toPrint) if paginationStartIndex + paginationSize < len(menu): - print(term.center("{b} {daf} {daf}{daf}{daf} {dal} {b}".format( + print(term.center("{b} {daf} {daf}{daf}{daf} {dal} {b}".format( b=specialChars[renderMode]["borderVertical"], daf=specialChars[renderMode]["downArrowFull"], dal=specialChars[renderMode]["downArrowLine"] @@ -134,7 +124,6 @@ def renderHotZone(term, renderType, menu, selection, hotzoneLocation, paddingBef else: print(term.center(commonEmptyLine(renderMode))) print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) def mainRender(needsRender, menu, selection): @@ -157,7 +146,7 @@ def mainRender(needsRender, menu, selection): print("") print(term.center(commonTopBorder(renderMode))) print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Select NodeRed Addons (npm) to install on initial run {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} Select NodeRed Addons (npm) to install on initial run {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) print(term.center(commonEmptyLine(renderMode))) if needsRender >= 1: @@ -168,22 +157,23 @@ def mainRender(needsRender, menu, selection): if not hideHelpText: if term.height < 32: print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Not enough vertical room to render controls help text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} Not enough vertical room to render controls help text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) print(term.center(commonEmptyLine(renderMode))) else: print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} Note: After initial startup installation, you must use the Palettes menu {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} in the NodeRed WUI to add or remove addons from NodeRed. {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Space] to select or deselect addon {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Tab] Expand or collapse addon menu size {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [S] Switch between sorted by checked and sorted alphabetically {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Enter] to build and save addons list {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Escape] to cancel changes {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} Note: After initial startup installation, you must {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} use the Palettes menu in the NodeRed WUI to add {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} or remove addons from NodeRed. {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [Space] to select or deselect addon {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [Tab] Expand or collapse addon menu size {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [S] Sort between checked and alphabetical {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [Enter] to build and save addons list {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [Escape] to cancel changes {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) print(term.center(commonEmptyLine(renderMode))) print(term.center(commonEmptyLine(renderMode))) print(term.center(commonBottomBorder(renderMode))) @@ -205,40 +195,42 @@ def isMenuItemSelectable(menu, index): def loadAddonsMenu(): global mainMenuList global installCommand - if os.path.exists(addonsFile): - with open(r'%s' % addonsFile) as objAddonsFile: - addonsLoaded = yaml.load(objAddonsFile) - installCommand = addonsLoaded["dockerFileInstallCommand"] - defaultOnAddons = addonsLoaded["addons"]["default_on"] - defaultOffAddons = addonsLoaded["addons"]["default_off"] - if not os.path.exists(serviceService + '/addons_list.yml'): - defaultOnAddons.sort() - for (index, addonName) in enumerate(defaultOnAddons): + global apiBuildOptions + global buildOptions + npmAddons = apiBuildOptions["nodered_npmSelection"] + defaultOnAddons = npmAddons["defaultOn"] + defaultOffAddons = npmAddons["defaultOff"] + essentialAddons = npmAddons["essentials"] # Not used yet + + if "services" in buildOptions and currentServiceName in buildOptions["configurations"]["services"]: + if "addonsList" in buildOptions["configurations"]["services"][currentServiceName]: + selectedAddons = buildOptions["configurations"]["services"][currentServiceName]["addonsList"] + for (index, addonName) in enumerate(defaultOnAddons): + if addonName in selectedAddons: mainMenuList.append([addonName, { "checked": True }]) + else: + mainMenuList.append([addonName, { "checked": False }]) - defaultOffAddons.sort() - for (index, addonName) in enumerate(defaultOffAddons): + for (index, addonName) in enumerate(defaultOffAddons): + if addonName in selectedAddons: + mainMenuList.append([addonName, { "checked": True }]) + else: mainMenuList.append([addonName, { "checked": False }]) - else: - with open(r'%s' % serviceService + '/addons_list.yml') as objSavedAddonsFile: - savedAddonsFile = yaml.load(objSavedAddonsFile) - savedAddons = savedAddonsFile["addons"] - savedAddons.sort() - for (index, addonName) in enumerate(savedAddons): - mainMenuList.append([addonName, { "checked": True }]) - - for (index, addonName) in enumerate(defaultOnAddons): - if not addonName in savedAddons: - mainMenuList.append([addonName, { "checked": False }]) - - for (index, addonName) in enumerate(defaultOffAddons): - if not addonName in savedAddons: - mainMenuList.append([addonName, { "checked": False }]) - sortBy = 0 - mainMenuList.sort(key=lambda x: (x[1]["checked"], x[0]), reverse=True) + else: + for (index, addonName) in enumerate(defaultOnAddons): + mainMenuList.append([addonName, { "checked": True }]) + for (index, addonName) in enumerate(defaultOffAddons): + mainMenuList.append([addonName, { "checked": False }]) else: - print("Error: '{addonsFile}' file doesn't exist.".format(addonsFile=addonsFile)) + for (index, addonName) in enumerate(defaultOnAddons): + mainMenuList.append([addonName, { "checked": True }]) + + for (index, addonName) in enumerate(defaultOffAddons): + mainMenuList.append([addonName, { "checked": False }]) + + sortBy = 0 + mainMenuList.sort(key=lambda x: (x[1]["checked"], x[0]), reverse=True) def checkMenuItem(selection): global mainMenuList @@ -249,29 +241,28 @@ def checkMenuItem(selection): def saveAddonList(): try: - if not os.path.exists(serviceService): - os.makedirs(serviceService, exist_ok=True) - nodeRedYamlAddonsList = { - "version": "1", - "application": "IOTstack", - "service": "nodered", - "comment": "Selected addons", - "dockerFileInstallCommand": installCommand, - "addons": [] - } + global buildOptions + if not "configurations" in buildOptions: + buildOptions["configurations"] = {} + + if not "services" in buildOptions["configurations"]: + buildOptions["configurations"]["services"] = {} + + if not currentServiceName in buildOptions["configurations"]["services"]: + buildOptions["configurations"]["services"][currentServiceName] = {} + + if not "addonsList" in buildOptions["configurations"]["services"][currentServiceName]: + buildOptions["configurations"]["services"][currentServiceName]["addonsList"] = [] + for (index, addon) in enumerate(mainMenuList): if addon[1]["checked"]: - nodeRedYamlAddonsList["addons"].append(addon[0]) - - with open(r'%s/addons_list.yml' % serviceService, 'w') as outputFile: - yaml.dump(nodeRedYamlAddonsList, outputFile) + buildOptions["configurations"]["services"][currentServiceName]["addonsList"].append(addon[0]) except Exception as err: print("Error saving NodeRed Addons list", currentServiceName) print(err) + input("Press Enter to continue...") return False - global hasRebuiltAddons - hasRebuiltAddons = True return True @@ -346,4 +337,4 @@ def saveAddonList(): originalSignalHandler = signal.getsignal(signal.SIGINT) main() -signal.signal(signal.SIGWINCH, originalSignalHandler) +signal.signal(signal.SIGWINCH, originalSignalHandler) \ No newline at end of file diff --git a/.internal/pycli/start_on_host.sh b/.internal/pycli/start_on_host.sh new file mode 100644 index 000000000..fecd3638d --- /dev/null +++ b/.internal/pycli/start_on_host.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# This is for starting the script on the host environment. It is primarily used for development and troubleshooting. It is advised to not use it for other purposes. +# npm module 'nodemon' is required for this to run. + +source ../meta.sh + +API_ADDR=localhost:32128 +HOST_CON_API=localhost:32128 +cd .internal; cd pycli; nodemon --no-stdin --exec python3 entry.py diff --git a/.internal/readme.md b/.internal/readme.md new file mode 100644 index 000000000..ccd306203 --- /dev/null +++ b/.internal/readme.md @@ -0,0 +1,119 @@ +# Developer information: + + +## Starting menu services in developer mode: + +### API: +``` +bash ./.internal/ctrl_api.sh development +``` + +## WUI: +``` +bash ./.internal/ctrl_wui.sh development +``` + +## CLI: +For auto restart (runs on host and requires nodemon): +``` +export API_ADDR=localhost:32128 && export HOST_CON_API=localhost:32128 && cd .internal/pycli; nodemon --no-stdin --exec python3 entry.py +``` + +Inside docker: +``` +bash ./.internal/ctrl_pycli.sh development +``` + +## Enviroment Variable settings: +These are set in `./.internal/meta.sh` + +You can set your own values by running + +``` +export NAME_OF_VAR=VALUE_YOU_WANT_SET +``` + +Eg: + +``` +export API_ADDR=localhost:32128 +``` + +You will need to restart the containers for the changes to take effect. + +### IOTSTACK_IOTSTACKPWD +Used for the interal docker menu instances to know where IOTstack is installed + +* Default: `$(pwd)` +* Internal Variable: `IOTSTACKPWD` + + +### IOTSTACK_HOSTUSER +Used for the internal docker menu instances to know which user to set permissions for, and for SSH connections + +* Default: `$(whoami)` +* Internal Variable: `HOSTUSER` + + +### IOTSTACK_HOSTSSH_ADDR +Used for the interal docker menu instances to know which user to set permissions for, and for SSH connections + +* Default: `"host.docker.internal"` +* Internal Variable: `HOSTSSH_ADDR` + + +### IOTSTACK_HOSTSSH_PORT +SSH port for menus to use to connect to host. + +* Default: `22` +* Internal Variable: `HOSTSSH_PORT` + + +### IOTSTACK_HOST_CON_IP +For the host to know how to connect to itself (or a remote host). Used for testing connectivity to services. + +* Default: `"localhost"` +* Internal Variable: `HOST_CON_IP` + + +### IOTSTACK_API_PORT +API Port + +* Default: `32128` +* Internal Variable: `API_PORT` + + +### IOTSTACK_WUI_PORT +WUI Port + +* Default: `32777` +* Internal Variable: `WUI_PORT` + + +### IOTSTACK_API_INTERFACE +Listen interface for API. 0.0.0.0 is all interfaces + +* Default: `0.0.0.0` +* Internal Variable: `API_INTERFACE` + + +### IOTSTACK_PYCLI_CON_API +Host and port for the docker CLI to know where the API is running + +* Default: `"$HOSTSSH_ADDR:$API_PORT"` +* Internal Variable: `PYCLI_CON_API` + + +### IOTSTACK_HOST_CON_IP +Host and port for the docker CLI to know where the API is running when executing commands via SSH + +* Default: `"$HOST_CON_IP:$API_PORT"` +* Internal Variable: `PYCLI_HOST_CON_API` + + +### IOTSTACK_PYCLI_CON_WUI +Host and port for the docker CLI to know where the WUI is running + +* Default: `"$HOST_CON_IP:$WUI_PORT"` +* Internal Variable: `PYCLI_CON_WUI` + diff --git a/.internal/saved_builds/.gitignore b/.internal/saved_builds/.gitignore new file mode 100644 index 000000000..ad6832660 --- /dev/null +++ b/.internal/saved_builds/.gitignore @@ -0,0 +1,4 @@ +*_build-installer.sh +*_build-options.json +*_build.zip +*_docker-compose-base.yml diff --git a/.internal/templates/networks/iotstack_nw/template.yml b/.internal/templates/networks/iotstack_nw/template.yml new file mode 100644 index 000000000..96855f24b --- /dev/null +++ b/.internal/templates/networks/iotstack_nw/template.yml @@ -0,0 +1,9 @@ +iotstack_nw: # Exposed by your host. + # external: true + name: IOTstack_Net + driver: bridge + ipam: + driver: default + config: + - subnet: 10.77.60.0/24 + # - gateway: 10.77.60.1 diff --git a/.internal/templates/networks/iotstack_nw_internal/template.yml b/.internal/templates/networks/iotstack_nw_internal/template.yml new file mode 100644 index 000000000..613ad9cbe --- /dev/null +++ b/.internal/templates/networks/iotstack_nw_internal/template.yml @@ -0,0 +1,9 @@ +iotstack_nw_internal: # For interservice communication. No access to outside + name: IOTstack_Net_Internal + driver: bridge + internal: true + ipam: + driver: default + config: + - subnet: 10.77.76.0/24 + # - gateway: 10.77.76.1 diff --git a/.internal/templates/networks/nextcloud_internal/template.yml b/.internal/templates/networks/nextcloud_internal/template.yml new file mode 100644 index 000000000..9ec2b4436 --- /dev/null +++ b/.internal/templates/networks/nextcloud_internal/template.yml @@ -0,0 +1,4 @@ +nextcloud_internal: # Network for NextCloud service + name: IOTstack_NextCloud + driver: bridge + internal: true diff --git a/.internal/templates/networks/vpn_nw/template.yml b/.internal/templates/networks/vpn_nw/template.yml new file mode 100644 index 000000000..eb0d2031d --- /dev/null +++ b/.internal/templates/networks/vpn_nw/template.yml @@ -0,0 +1,8 @@ +vpn_nw: # Network specifically for VPN + name: IOTstack_VPN + driver: bridge + ipam: + driver: default + config: + - subnet: 10.77.88.0/24 + # - gateway: 192.18.200.1 diff --git a/.internal/templates/scripts/bootstrap.js b/.internal/templates/scripts/bootstrap.js new file mode 100644 index 000000000..9bf51415e --- /dev/null +++ b/.internal/templates/scripts/bootstrap.js @@ -0,0 +1,58 @@ +const BootstrapScript = ({ + req, + scriptName, + options, + server, + settings, + version, + logger +}) => { + return new Promise((resolve, reject) => { + try { + const result = { + filename: 'scripts', + data: 'No Data' + }; + + if (!options) { + result.data = 'No options given'; + } + result.data = `Options given for ${scriptName} and ${options?.build}`; + + if (options?.nofluff) { + result.data = `curl http://${req.get('host')}/build/get/${options?.build}/zip \\ +--output iotstack_build_${options?.build}.zip \\ +&& unzip -o ./iotstack_build_${options?.build}.zip \\ +&& bash build-installer.sh --from-net --overwrite +`; + } else { + result.data = ` +# Ensure you are in IOTstack's main directory. +# You can use this instead of the CLI menu to install your build +# Download, extract and execute commands: +$ curl http://${req.get('host')}/build/get/${options?.build}/zip \\ +--output iotstack_build_${options?.build}.zip \\ +&& unzip -o ./iotstack_build_${options?.build}.zip \\ +&& bash build-installer.sh --from-net --overwrite +`; + + } + return resolve(result); + } catch (err) { + console.error(err); + console.trace(); + console.trace(); + console.debug("\nParams:"); + console.debug({ scriptName }); + console.debug({ version }); + console.debug({ options }); + return reject({ + component: `BootstrapScript: - '${scriptName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); +}; + +module.exports = BootstrapScript; diff --git a/.internal/templates/scripts/build_installer.js b/.internal/templates/scripts/build_installer.js new file mode 100644 index 000000000..5aaed6c69 --- /dev/null +++ b/.internal/templates/scripts/build_installer.js @@ -0,0 +1,194 @@ +const renderPostbuildScripts = (scripts) => { + let contents = ''; + + if (Array.isArray(scripts)) { + scripts.forEach((script) => { + contents += ` +# Postbuild Service script: +# Injected by: ${script?.serviceName} +# Comment: ${script?.comment}`; + +if (script?.multilineComment) { + contents += ` +: <<'END_COMMENT' + +${script?.multilineComment} + +END_COMMENT + +`; +} +contents += ` +${script?.code} + +# End script (${script?.serviceName})`; + }); + } + + return contents; +}; + +const renderPrebuildScripts = (scripts) => { + let contents = ''; + + if (Array.isArray(scripts)) { + scripts.forEach((script) => { + contents += ` +# Prebuild Service script: +# Injected by: ${script?.serviceName} +# Comment: ${script?.comment}`; + +if (script?.multilineComment) { + contents += ` +: <<'END_COMMENT' + +${script?.multilineComment} + +END_COMMENT + +`; +} +contents += ` +${script?.code} + +# End script (${script?.serviceName})`; + }); + } + + return contents; +}; + +const BuildInstaller = ({ + scriptName, + options, + server, + settings, + version, + logger +}) => { + return new Promise((resolve, reject) => { + try { + const result = { + filename: 'scripts', + data: 'No Data' + }; + + result.data = ` +#!/bin/bash + +# IOTstack build installer +# Build: ${options?.build} +# API Version: ${version} + +# This script is automatically generated during build time +# To be executed at install time. + +FROM_NET="false" +PREREQ_CHECK="true" +OVERWRITE_EXISTING_ASK="true" +CLEAN_CURRENT="false" +DISPLAY_WARNINGS="true" +BAD_OPTION_TRIGGER="false" + +# Process input args +while test $# -gt 0 +do + case "$1" in + --from-net) FROM_NET="true" + ;; + --no-check) PREREQ_CHECK="false" + ;; + --overwrite) OVERWRITE_EXISTING_ASK="false" + ;; + --clean-current) CLEAN_CURRENT="true" + ;; + --no-warnings) DISPLAY_WARNINGS="false" + ;; + --*) echo "bad option $1" && BAD_OPTION_TRIGGER="true" + ;; + esac + shift +done + +if [[ $BAD_OPTION_TRIGGER == "true" ]]; then + if [[ $DISPLAY_WARNINGS == "true" ]]; then + echo "Bad option detected." + read -n 1 -t 5 -s -r -p "Press any key within 5 seconds to cancel build and exit " READIN + if [[ ! -z "$READIN" ]]; then + echo "" + echo "Exiting..." + exit 0 + fi + else + # Automatically exit if bad option detected and warnings are disabled. + exit 1 + fi +fi + +if [[ ! -f ./menu.sh ]]; then + echo "Couldn't detect menu.sh file for IOTstack. Ensure you are in the correct directory:" + pwd + exit 2 +fi + +# Load meta data from installation +source ./.internal/meta.sh + +#### Prebuild service scripts +${renderPrebuildScripts(options?.prebuildScripts)} + +#### End prebuild service scripts + + +# Merge docker-compose and docker-compose-overrides + + +#### Postbuild service scripts +${renderPostbuildScripts(options?.postbuildScripts)} + +#### End postbuild service scripts + +if [[ -f ./compose-override.yml ]]; then + echo "Merging 'compose-override.yml' with 'docker-compose-base.yml':" + echo "docker container run -it -v $(pwd)/compose-override.yml:/usr/iotstack_pycli/compose-override.yml:ro -v $(pwd)/docker-compose-base.yml:/usr/iotstack_pycli/docker-compose-base.yml:ro -v $(pwd)/docker-compose.yml:/usr/iotstack_pycli/docker-compose.yml -e \\"PYCLI_OVERRIDE_YML=compose-override.yml\\" -e \\"PYCLI_BASE_YML=docker-compose-base.yml\\" -e \\"PYCLI_OUTPUT_YML=docker-compose.yml\\" iostack_pycli:$VERSION /usr/local/bin/python3 /usr/iotstack_pycli/compose_override_entry.py" + docker container run -it -v $(pwd)/compose-override.yml:/usr/iotstack_pycli/compose-override.yml:ro -v $(pwd)/docker-compose-base.yml:/usr/iotstack_pycli/docker-compose-base.yml:ro -v $(pwd)/docker-compose.yml:/usr/iotstack_pycli/docker-compose.yml -e "PYCLI_OVERRIDE_YML=compose-override.yml" -e "PYCLI_BASE_YML=docker-compose-base.yml" -e "PYCLI_OUTPUT_YML=docker-compose.yml" iostack_pycli:$VERSION /usr/local/bin/python3 /usr/iotstack_pycli/compose_override_entry.py + echo "" + echo "Merge container exited. You can rerun merge in the future with:" + echo " ./menu.sh --remerge-yaml-override --no-check" +else + [ -f docker-compose.yml ] && rm docker-compose.yml + cp docker-compose-base.yml docker-compose.yml +fi + +if [[ -f ./postbuild.sh ]]; then + echo "Running postbuild script (${(options?.selectedServices ?? []).join(', ')}):" + ./postbuild.sh ${(options?.selectedServices ?? []).join(' ')} +fi + +echo "" +echo "Setup complete. You can start the stack with: " +echo " docker-compose up --remove-orphans" +echo "or" +echo " docker-compose up -d --remove-orphans" + +`; + + return resolve(result); + } catch (err) { + console.error(err); + console.trace(); + console.trace(); + console.debug("\nParams:"); + console.debug({ scriptName }); + console.debug({ version }); + console.debug({ options }); + return reject({ + component: `BuildInstaller: - '${scriptName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); +}; + +module.exports = BuildInstaller; diff --git a/.internal/templates/services/adguardhome/build.js b/.internal/templates/services/adguardhome/build.js new file mode 100644 index 000000000..8f1d9b985 --- /dev/null +++ b/.internal/templates/services/adguardhome/build.js @@ -0,0 +1,189 @@ +const ServiceBuilder = ({ + settings, + version, + logger +}) => { + const retr = {}; + const serviceName = 'adguardhome'; + + const { + setImageTag, + setModifiedPorts, + setLoggingState, + setNetworkMode, + setNetworks, + setVolumes, + setEnvironmentVariables, + setDevices + } = require('../../../src/utils/commonCompileLogic'); + + const { + checkPortConflicts, + checkNetworkConflicts, + checkDependencyServices + } = require('../../../src/utils/commonBuildChecks'); + + /* + Order: + 1. compile() - merges build options into the final JSON output. + 2. issues() - runs checks on the compile()'ed JSON, and can also test for errors. + 3. assume() - sets required default values if they are not specified in compile(). Once defaults are set, it reruns compile(). This function is optional + 4. build() - sets up scripts and files. + */ + + retr.init = () => { + logger.debug(`ServiceBuilder:init() - '${serviceName}'`); + }; + + const createVolumesDirectory = () => { + return ` +mkdir -p ./volumes/adguardhome/workdir +mkdir -p ./volumes/adguardhome/confdir +`; + }; + + const checkVolumesDirectory = () => { + return ` +HAS_ERROR="false" + +if [[ ! -d ./volumes/adguardhome/workdir ]]; then + echo "AdguardHome work directory is missing!" + HAS_ERROR="true" +fi + +if [[ ! -d ./volumes/adguardhome/confdir ]]; then + echo "AdguardHome config directory is missing!" + HAS_ERROR="true" +fi + +if [[ "$HAS_ERROR" == "true" ]]; then + sleep 1 +fi +`; + }; + + retr.compile = ({ + outputTemplateJson, + buildOptions, + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:compile() - '${serviceName}' started`); + + const compileResults = { + modifiedImage: setImageTag({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedPorts: setModifiedPorts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedLogging: setLoggingState({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworkMode: setNetworkMode({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworks: setNetworks({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedVolumes: setVolumes({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedEnvironment: setEnvironmentVariables({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedDevices: setDevices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }) + }; + console.info(`ServiceBuilder:compile() - '${serviceName}' Results:`, compileResults); + + console.info(`ServiceBuilder:compile() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + return reject({ + component: `ServiceBuilder::compile() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.issues = ({ + outputTemplateJson, + buildOptions, + tmpPath + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:issues() - '${serviceName}' started`); + let issues = []; + + const portConflicts = checkPortConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...portConflicts]; + + const serviceDependencies = checkDependencyServices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...serviceDependencies]; + + const networkConflicts = checkNetworkConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + if (networkConflicts) { + issues.push(networkConflicts); + } + + console.info(`ServiceBuilder:issues() - '${serviceName}' Issues found: ${issues.length}`); + console.info(`ServiceBuilder:issues() - '${serviceName}' completed`); + return resolve(issues); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::issues() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.build = ({ + outputTemplateJson, + buildOptions, + tmpPath, + zipList, + prebuildScripts, + postbuildScripts + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:build() - '${serviceName}' started`); + + prebuildScripts.push({ + serviceName, + comment: 'Create required service directory exists for first launch', + multilineComment: null, + code: createVolumesDirectory() + }); + + postbuildScripts.push({ + serviceName, + comment: 'Ensure required service directory exists for launch', + multilineComment: null, + code: checkVolumesDirectory() + }); + + console.info(`ServiceBuilder:build() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::build() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + return retr; +} + +module.exports = ServiceBuilder; diff --git a/.internal/templates/services/adguardhome/config.js b/.internal/templates/services/adguardhome/config.js new file mode 100644 index 000000000..b9940d5ad --- /dev/null +++ b/.internal/templates/services/adguardhome/config.js @@ -0,0 +1,53 @@ +const pihole = () => { + const retr = {}; + + const serviceName = 'pihole'; + + retr.getConfigOptions = () => { + return { + serviceName, // Required + labeledPorts: { + "8089:8089": 'http', + "3001:3001": 'http-setup' + }, + volumes: true, + networks: true, + logging: true + } + }; + + retr.getHelp = () => { + return { + serviceName, // Required + links: { + "Docker": 'https://hub.docker.com/r/adguard/adguardhome', // Docker of service + "Source Code": 'https://github.com/AdguardTeam/AdGuardHome', // Sourcecode of service + community: '', // Community link + communityChat: '', // Discord, gitter etc + other: '', // Other links + rawMarkdownRemote: '', // Usually links to github raw help pages. + rawMarkdownLocal: '', // Relative path to docs locally + "IOTstack Documentation for {$displayName}": 'https://sensorsiot.github.io/IOTstack/Containers/AdGuardHome/' // Usually links to the github page for this service. + } + }; + }; + + retr.getCommands = () => { + return { + commands: {} // Key/value pair of helper commands user can run locally + }; + }; + + retr.getMeta = () => { + return { + serviceName, // Required + displayName: 'AdGuard (Untested)', + serviceTypeTags: ['wui', 'dns', 'dashboard'], + iconUri: '/logos/adguardhome.png' + }; + }; + + return retr; +}; + +module.exports = pihole; diff --git a/.internal/templates/services/adguardhome/template.yml b/.internal/templates/services/adguardhome/template.yml new file mode 100644 index 000000000..61bc5ff6e --- /dev/null +++ b/.internal/templates/services/adguardhome/template.yml @@ -0,0 +1,17 @@ +adguardhome: + container_name: adguardhome + image: adguard/adguardhome + restart: unless-stopped + environment: + - TZ=Etc/UTC + ports: + - "53:53/tcp" + - "53:53/udp" + - "8089:8089/tcp" + - "3001:3000/tcp" + volumes: + - ./volumes/adguardhome/workdir:/opt/adguardhome/work + - ./volumes/adguardhome/confdir:/opt/adguardhome/conf + networks: + - iotstack_nw + - vpn_nw \ No newline at end of file diff --git a/.internal/templates/services/adminer/build.js b/.internal/templates/services/adminer/build.js new file mode 100644 index 000000000..21bdbfb26 --- /dev/null +++ b/.internal/templates/services/adminer/build.js @@ -0,0 +1,144 @@ +const ServiceBuilder = ({ + settings, + version, + logger +}) => { + const retr = {}; + const serviceName = 'adminer'; + + const { + setModifiedPorts, + setLoggingState, + setNetworkMode, + setNetworks, + setDevices, + setEnvironmentVariables + } = require('../../../src/utils/commonCompileLogic'); + + const { + checkPortConflicts, + checkNetworkConflicts, + checkDependencyServices + } = require('../../../src/utils/commonBuildChecks'); + + /* + Order: + 1. compile() - merges build options into the final JSON output. + 2. issues() - runs checks on the compile()'ed JSON, and can also test for errors. + 3. assume() - sets required default values if they are not specified in compile(). Once defaults are set, it reruns compile(). This function is optional + 4. build() - sets up scripts and files. + */ + + retr.init = () => { + logger.debug(`ServiceBuilder:init() - '${serviceName}'`); + }; + + retr.compile = ({ + outputTemplateJson, + buildOptions, + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:compile() - '${serviceName}' started`); + + const compileResults = { + modifiedPorts: setModifiedPorts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedLogging: setLoggingState({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworkMode: setNetworkMode({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworks: setNetworks({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedEnvironment: setEnvironmentVariables({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedDevices: setDevices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }) + }; + console.info(`ServiceBuilder:compile() - '${serviceName}' Results:`, compileResults); + + console.info(`ServiceBuilder:compile() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + return reject({ + component: `ServiceBuilder::compile() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.issues = ({ + outputTemplateJson, + buildOptions, + tmpPath + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:issues() - '${serviceName}' started`); + let issues = []; + + const portConflicts = checkPortConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...portConflicts]; + + const serviceDependencies = checkDependencyServices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...serviceDependencies]; + + const networkConflicts = checkNetworkConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + if (networkConflicts) { + issues.push(networkConflicts); + } + + console.info(`ServiceBuilder:issues() - '${serviceName}' Issues found: ${issues.length}`); + console.info(`ServiceBuilder:issues() - '${serviceName}' completed`); + return resolve(issues); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::issues() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.build = ({ + outputTemplateJson, + buildOptions, + tmpPath, + zipList, + prebuildScripts, + postbuildScripts + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:build() - '${serviceName}' started`); + // Code here + console.info(`ServiceBuilder:build() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::build() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + return retr; +} + +module.exports = ServiceBuilder; diff --git a/.internal/templates/services/adminer/config.js b/.internal/templates/services/adminer/config.js new file mode 100644 index 000000000..fdad30ef6 --- /dev/null +++ b/.internal/templates/services/adminer/config.js @@ -0,0 +1,54 @@ +const adminer = () => { + const retr = {}; + + const serviceName = 'adminer'; + + retr.getConfigOptions = () => { + return { + serviceName, // Required + labeledPorts: { + "9080:8080": 'http' + }, + volumes: false, + networks: false, + logging: true + } + }; + + retr.getHelp = () => { + return { + serviceName, // Required + links: { + "Website": 'https://www.adminer.org/', // Website of service + serviceDocs: '', // Official link to docs of service + "Docker": 'https://hub.docker.com/_/adminer/', // Docker of service + "Source Code": 'https://github.com/vrana/adminer/', // Sourcecode of service + community: '', // Community link + communityChat: '', // Discord, gitter etc + other: '', // Other links + rawMarkdownRemote: '', // Usually links to github raw help pages. + rawMarkdownLocal: '', // Relative path to docs locally + "IOTstack Documentation for {$displayName}": 'https://sensorsiot.github.io/IOTstack/Containers/Adminer/' // Usually links to the github page for this service. + } + }; + }; + + retr.getCommands = () => { + return { + commands: {} // Key/value pair of helper commands user can run locally + }; + }; + + retr.getMeta = () => { + return { + serviceName, // Required + displayName: 'Adminer', + serviceTypeTags: ['wui', 'database manager'], + iconUri: '/logos/adminer.png' + }; + }; + + return retr; +}; + +module.exports = adminer; diff --git a/.templates/adminer/service.yml b/.internal/templates/services/adminer/template.yml similarity index 67% rename from .templates/adminer/service.yml rename to .internal/templates/services/adminer/template.yml index 8c4188c6a..15eb773f0 100644 --- a/.templates/adminer/service.yml +++ b/.internal/templates/services/adminer/template.yml @@ -6,3 +6,7 @@ adminer: - "9080:8080" networks: - iotstack_nw + logging: + options: + max-size: "5m" + max-file: "3" diff --git a/.internal/templates/services/blynk_server/build.js b/.internal/templates/services/blynk_server/build.js new file mode 100644 index 000000000..f6c33b18b --- /dev/null +++ b/.internal/templates/services/blynk_server/build.js @@ -0,0 +1,140 @@ +const ServiceBuilder = ({ + settings, + version, + logger +}) => { + const retr = {}; + const serviceName = 'blynk_server'; + + const { + setModifiedPorts, + setLoggingState, + setNetworkMode, + setNetworks + } = require('../../../src/utils/commonCompileLogic'); + + const { + checkPortConflicts, + checkNetworkConflicts, + checkDependencyServices + } = require('../../../src/utils/commonBuildChecks'); + + /* + Order: + 1. compile() - merges build options into the final JSON output. + 2. issues() - runs checks on the compile()'ed JSON, and can also test for errors. + 3. assume() - sets required default values if they are not specified in compile(). Once defaults are set, it reruns compile(). This function is optional + 4. build() - sets up scripts and files. + */ + + retr.init = () => { + logger.debug(`ServiceBuilder:init() - '${serviceName}'`); + }; + + retr.compile = ({ + outputTemplateJson, + buildOptions, + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:compile() - '${serviceName}' started`); + + const compileResults = { + modifiedPorts: setModifiedPorts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedLogging: setLoggingState({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworkMode: setNetworkMode({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworks: setNetworks({ buildTemplate: outputTemplateJson, buildOptions, serviceName }) + }; + console.info(`ServiceBuilder:compile() - '${serviceName}' Results:`, compileResults); + + console.info(`ServiceBuilder:compile() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + return reject({ + component: `ServiceBuilder::compile() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.issues = ({ + outputTemplateJson, + buildOptions, + tmpPath + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:issues() - '${serviceName}' started`); + let issues = []; + + const portConflicts = checkPortConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...portConflicts]; + + const serviceDependencies = checkDependencyServices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...serviceDependencies]; + + const networkConflicts = checkNetworkConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + if (networkConflicts) { + issues.push(networkConflicts); + } + + console.info(`ServiceBuilder:issues() - '${serviceName}' Issues found: ${issues.length}`); + console.info(`ServiceBuilder:issues() - '${serviceName}' completed`); + return resolve(issues); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::issues() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.build = ({ + outputTemplateJson, + buildOptions, + tmpPath, + zipList, + prebuildScripts, + postbuildScripts + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:build() - '${serviceName}' started`); + // Code here TODO: Finish blynk_server + console.info(`ServiceBuilder:build() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::build() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + return retr; +} + +module.exports = ServiceBuilder; diff --git a/.internal/templates/services/blynk_server/buildFiles/Dockerfile b/.internal/templates/services/blynk_server/buildFiles/Dockerfile new file mode 100644 index 000000000..c936ee1c8 --- /dev/null +++ b/.internal/templates/services/blynk_server/buildFiles/Dockerfile @@ -0,0 +1,75 @@ +# Acknowledgements: +# Based on: +# https://github.com/SensorsIot/IOTstack/blob/master/.templates/blynk_server/Dockerfile +# (as at commit ID 4dff89c1bb6a5b1c01d3c087dcb662256a0c050f) +# Borrows from: +# https://github.com/Peterkn2001/blynk-server/blob/master/server/Docker/Dockerfile +# (as at commit ID 889c7e55161832e21264d993d9fa5abd1c015e1c) + +FROM ubuntu + +# declare the version to be built, defaulting to 0.41.16 (which is +# current as of 2021-10-22) +ARG BLYNK_SERVER_VERSION=0.41.16 + +# form the download URL +ENV BLYNK_SERVER_URL=https://github.com/Peterkn2001/blynk-server/releases/download/v${BLYNK_SERVER_VERSION}/server-${BLYNK_SERVER_VERSION}.jar + +# Add support packages to the base image +RUN apt-get update \ + && apt-get install -y \ + apt-utils \ + libreadline8 \ + libreadline-dev \ + && apt-get install -y \ + curl \ + libxrender1 \ + maven \ + openjdk-11-jdk \ + rsync + +# Add IOTstack-specific support +ENV IOTSTACK_DEFAULTS_DIR="iotstack_defaults" +ENV IOTSTACK_ENTRY_POINT="docker-entrypoint.sh" +COPY ${IOTSTACK_DEFAULTS_DIR} /${IOTSTACK_DEFAULTS_DIR} +COPY ${IOTSTACK_ENTRY_POINT} /${IOTSTACK_ENTRY_POINT} +RUN chmod 755 /${IOTSTACK_ENTRY_POINT} + +# define well-known paths +ENV IOTSTACK_DATA_DIR="/data" +ENV IOTSTACK_CONF_DIR="/config" +ENV IOTSTACK_JAVA_DIR="/blynk" + +# Create and populate expected folders +RUN mkdir -p ${IOTSTACK_DATA_DIR} ${IOTSTACK_JAVA_DIR} \ + && curl -L ${BLYNK_SERVER_URL} >"${IOTSTACK_JAVA_DIR}/server.jar" + +# declare expected mapped volumes +VOLUME ["${IOTSTACK_CONF_DIR}", "${IOTSTACK_DATA_DIR}"] + +# Expose assumed internal ports: +# 8080 http.port +# 8440 hardware.mqtt.port +# 9443 https.port +EXPOSE 8080 8440 9443 + +# set the working directory +WORKDIR ${IOTSTACK_DATA_DIR} + +# define launch procedure +ENTRYPOINT ["/docker-entrypoint.sh"] +CMD ["java", "-jar", "/blynk/server.jar", "-dataFolder", "/data", "-serverConfig", "/config/server.properties", "-mailConfig", "/config/mail.properties"] + +# supplement image metadata +LABEL blynk-server.version=${BLYNK_SERVER_VERSION} +LABEL blynk-server.url=${BLYNK_SERVER_URL} +LABEL com.github.SensorsIot.IOTstack.Dockerfile.maintainer="877dev <877dev@gmail.com>" +LABEL com.github.Peterkn2001.blynk-server.Dockerfile.maintainer="Florian Mauduit " + +# unset variables that are not needed by docker-entrypoint.sh +ENV IOTSTACK_ENTRY_POINT= +ENV IOTSTACK_DATA_DIR= +ENV IOTSTACK_JAVA_DIR= +ENV BLYNK_SERVER_URL= + +# EOF diff --git a/.internal/templates/services/blynk_server/buildFiles/docker-entrypoint.sh b/.internal/templates/services/blynk_server/buildFiles/docker-entrypoint.sh new file mode 100755 index 000000000..15ede7f4c --- /dev/null +++ b/.internal/templates/services/blynk_server/buildFiles/docker-entrypoint.sh @@ -0,0 +1,19 @@ +#!/bin/bash +set -e + +# were we launched as root with defaults available? +if [ "$(id -u)" = "0" -a -d /"$IOTSTACK_DEFAULTS_DIR" ]; then + + # yes! ensure that the IOTSTACK_CONF_DIR exists + mkdir -p "$IOTSTACK_CONF_DIR" + + # populate runtime directory from the defaults + rsync -arp --ignore-existing "/${IOTSTACK_DEFAULTS_DIR}/" "${IOTSTACK_CONF_DIR}" + + # enforce correct ownership + chown -R "${IOTSTACK_UID:-nobody}":"${IOTSTACK_GID:-nobody}" "$IOTSTACK_CONF_DIR" + +fi + +# start the blynk server +exec "$@" diff --git a/.internal/templates/services/blynk_server/buildFiles/iotstack_defaults/mail.properties b/.internal/templates/services/blynk_server/buildFiles/iotstack_defaults/mail.properties new file mode 100644 index 000000000..aa01f9c46 --- /dev/null +++ b/.internal/templates/services/blynk_server/buildFiles/iotstack_defaults/mail.properties @@ -0,0 +1,9 @@ +mail.smtp.auth=true + mail.smtp.starttls.enable=true + mail.smtp.host=smtp.gmail.com + mail.smtp.port=587 + mail.smtp.username=YOUR_GMAIL@gmail.com + mail.smtp.password=YOUR_GMAIL_APP_PASSWORD + mail.smtp.connectiontimeout=30000 + mail.smtp.timeout=120000 + diff --git a/.internal/templates/services/blynk_server/buildFiles/iotstack_defaults/server.properties b/.internal/templates/services/blynk_server/buildFiles/iotstack_defaults/server.properties new file mode 100644 index 000000000..4b5ba80ff --- /dev/null +++ b/.internal/templates/services/blynk_server/buildFiles/iotstack_defaults/server.properties @@ -0,0 +1,40 @@ +hardware.mqtt.port=8440 + http.port=8080 + force.port.80.for.csv=false + force.port.80.for.redirect=true + https.port=9443 + data.folder=./data + logs.folder=./logs + log.level=info + user.devices.limit=10 + user.tags.limit=100 + user.dashboard.max.limit=100 + user.widget.max.size.limit=20 + user.message.quota.limit=100 + notifications.queue.limit=2000 + blocking.processor.thread.pool.limit=6 + notifications.frequency.user.quota.limit=5 + webhooks.frequency.user.quota.limit=1000 + webhooks.response.size.limit=96 + user.profile.max.size=128 + terminal.strings.pool.size=25 + map.strings.pool.size=25 + lcd.strings.pool.size=6 + table.rows.pool.size=100 + profile.save.worker.period=60000 + stats.print.worker.period=60000 + web.request.max.size=524288 + csv.export.data.points.max=43200 + hard.socket.idle.timeout=10 + enable.db=false + enable.raw.db.data.store=false + async.logger.ring.buffer.size=2048 + allow.reading.widget.without.active.app=false + allow.store.ip=true + initial.energy=1000000 + admin.rootPath=/admin + restore.host=blynk-cloud.com + product.name=Blynk + admin.email=admin@blynk.cc + admin.pass=admin + diff --git a/.internal/templates/services/blynk_server/config.js b/.internal/templates/services/blynk_server/config.js new file mode 100644 index 000000000..eafaf28c6 --- /dev/null +++ b/.internal/templates/services/blynk_server/config.js @@ -0,0 +1,56 @@ +const blynk_server = () => { + const retr = {}; + + const serviceName = 'blynk_server'; + + retr.getConfigOptions = () => { + return { + serviceName, // Required + labeledPorts: { + "8180:8080": 'http', + "8441:8441": 'other', + "9443:9443": 'ssl' + }, + volumes: false, + networks: false, + logging: true + } + }; + + retr.getHelp = () => { + return { + serviceName, // Required + links: { + "Website": 'https://blynk.io/', // Website of service + "Official Documentation": 'http://docs.blynk.cc/', // Official link to docs of service + docker: '', // Docker of service + "Source Code": 'https://github.com/blynkkk/blynk-server', // Sourcecode of service + "Community": 'https://community.blynk.cc/', // Community link + communityChat: '', // Discord, gitter etc + other: '', // Other links + rawMarkdownRemote: '', // Usually links to github raw help pages. + rawMarkdownLocal: '', // Relative path to docs locally + "IOTstack Documentation for {$displayName}": 'https://sensorsiot.github.io/IOTstack/Containers/Blynk_server/' // Usually links to the github page for this service. + } + }; + }; + + retr.getCommands = () => { + return { + commands: {} // Key/value pair of helper commands user can run locally + }; + }; + + retr.getMeta = () => { + return { + serviceName, // Required + displayName: 'Blynk Server (untested)', + serviceTypeTags: ['wui', 'iot'], + iconUri: '/logos/blynk.png' + }; + }; + + return retr; +}; + +module.exports = blynk_server; diff --git a/.templates/blynk_server/service.yml b/.internal/templates/services/blynk_server/template.yml similarity index 51% rename from .templates/blynk_server/service.yml rename to .internal/templates/services/blynk_server/template.yml index cbe446958..862fd805b 100644 --- a/.templates/blynk_server/service.yml +++ b/.internal/templates/services/blynk_server/template.yml @@ -1,14 +1,20 @@ blynk_server: - build: ./services/blynk_server/. + build: + context: ./.templates/blynk_server/. + args: + - BLYNK_SERVER_VERSION=0.41.16 container_name: blynk_server restart: unless-stopped + environment: + - TZ=Etc/UTC + - IOTSTACK_UID=1000 + - IOTSTACK_GID=1000 ports: - "8180:8080" - - "8441:8441" - - "9443:9443" + - "8440:8440" + - "9444:9443" volumes: - ./volumes/blynk_server/data:/data - ./volumes/blynk_server/config:/config networks: - iotstack_nw - diff --git a/.internal/templates/services/chronograf/template.yml b/.internal/templates/services/chronograf/template.yml new file mode 100644 index 000000000..9b4539e6c --- /dev/null +++ b/.internal/templates/services/chronograf/template.yml @@ -0,0 +1,21 @@ +chronograf: + container_name: chronograf + image: chronograf:latest + restart: unless-stopped + environment: + - TZ=Etc/UTC + # see https://docs.influxdata.com/chronograf/v1.9/administration/config-options/ + - INFLUXDB_URL=http://influxdb:8086 + # - INFLUXDB_USERNAME= + # - INFLUXDB_PASSWORD= + # - INFLUXDB_ORG= + # - KAPACITOR_URL=http://kapacitor:9092 + ports: + - "8888:8888" + volumes: + - ./volumes/chronograf:/var/lib/chronograf + depends_on: + - influxdb + # - kapacitor + networks: + - iotstack_nw diff --git a/.internal/templates/services/dashmachine/build.js b/.internal/templates/services/dashmachine/build.js new file mode 100644 index 000000000..4f36bc018 --- /dev/null +++ b/.internal/templates/services/dashmachine/build.js @@ -0,0 +1,140 @@ +const ServiceBuilder = ({ + settings, + version, + logger +}) => { + const retr = {}; + const serviceName = 'dashmachine'; + + const { + setModifiedPorts, + setLoggingState, + setNetworkMode, + setNetworks + } = require('../../../src/utils/commonCompileLogic'); + + const { + checkPortConflicts, + checkNetworkConflicts, + checkDependencyServices + } = require('../../../src/utils/commonBuildChecks'); + + /* + Order: + 1. compile() - merges build options into the final JSON output. + 2. issues() - runs checks on the compile()'ed JSON, and can also test for errors. + 3. assume() - sets required default values if they are not specified in compile(). Once defaults are set, it reruns compile(). This function is optional + 4. build() - sets up scripts and files. + */ + + retr.init = () => { + logger.debug(`ServiceBuilder:init() - '${serviceName}'`); + }; + + retr.compile = ({ + outputTemplateJson, + buildOptions, + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:compile() - '${serviceName}' started`); + + const compileResults = { + modifiedPorts: setModifiedPorts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedLogging: setLoggingState({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworkMode: setNetworkMode({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworks: setNetworks({ buildTemplate: outputTemplateJson, buildOptions, serviceName }) + }; + console.info(`ServiceBuilder:compile() - '${serviceName}' Results:`, compileResults); + + console.info(`ServiceBuilder:compile() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + return reject({ + component: `ServiceBuilder::compile() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.issues = ({ + outputTemplateJson, + buildOptions, + tmpPath + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:issues() - '${serviceName}' started`); + let issues = []; + + const portConflicts = checkPortConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...portConflicts]; + + const serviceDependencies = checkDependencyServices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...serviceDependencies]; + + const networkConflicts = checkNetworkConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + if (networkConflicts) { + issues.push(networkConflicts); + } + + console.info(`ServiceBuilder:issues() - '${serviceName}' Issues found: ${issues.length}`); + console.info(`ServiceBuilder:issues() - '${serviceName}' completed`); + return resolve(issues); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::issues() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.build = ({ + outputTemplateJson, + buildOptions, + tmpPath, + zipList, + prebuildScripts, + postbuildScripts + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:build() - '${serviceName}' started`); + // Code here + console.info(`ServiceBuilder:build() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::build() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + return retr; +} + +module.exports = ServiceBuilder; diff --git a/.internal/templates/services/dashmachine/config.js b/.internal/templates/services/dashmachine/config.js new file mode 100644 index 000000000..27564d46a --- /dev/null +++ b/.internal/templates/services/dashmachine/config.js @@ -0,0 +1,54 @@ +const dashmachine = () => { + const retr = {}; + + const serviceName = 'dashmachine'; + + retr.getConfigOptions = () => { + return { + serviceName, // Required + labeledPorts: { + "5000:5000": 'http' + }, + volumes: false, + networks: false, + logging: true + } + }; + + retr.getHelp = () => { + return { + serviceName, // Required + links: { + website: '', // Website of service + serviceDocs: '', // Official link to docs of service + "Docker": 'https://hub.docker.com/r/rmountjoy/dashmachine', // Docker of service + "Source Code": 'https://github.com/rmountjoy92/DashMachine', // Sourcecode of service + community: '', // Community link + communityChat: '', // Discord, gitter etc + other: '', // Other links + rawMarkdownRemote: '', // Usually links to github raw help pages. + rawMarkdownLocal: '', // Relative path to docs locally + "IOTstack Documentation for {$displayName}": 'https://sensorsiot.github.io/IOTstack/Containers/DashMachine/' // Usually links to the github page for this service. + } + }; + }; + + retr.getCommands = () => { + return { + commands: {} // Key/value pair of helper commands user can run locally + }; + }; + + retr.getMeta = () => { + return { + serviceName, // Required + displayName: 'DashMachine', + serviceTypeTags: ['wui', 'dashboard'], + iconUri: '/logos/dashmachine.png' + }; + }; + + return retr; +}; + +module.exports = dashmachine; diff --git a/.templates/dashmachine/service.yml b/.internal/templates/services/dashmachine/template.yml similarity index 91% rename from .templates/dashmachine/service.yml rename to .internal/templates/services/dashmachine/template.yml index 61df2ac28..096cb36b6 100644 --- a/.templates/dashmachine/service.yml +++ b/.internal/templates/services/dashmachine/template.yml @@ -4,5 +4,5 @@ dashmachine: volumes: - ./volumes/dashmachine/user_data:/dashmachine/dashmachine/user_data ports: - - 5000:5000 + - "5000:5000" restart: unless-stopped diff --git a/.internal/templates/services/deconz/build.js b/.internal/templates/services/deconz/build.js new file mode 100644 index 000000000..098085bed --- /dev/null +++ b/.internal/templates/services/deconz/build.js @@ -0,0 +1,229 @@ +const ServiceBuilder = ({ + settings, + version, + logger +}) => { + const retr = {}; + const serviceName = 'deconz'; + + const { byName } = require('../../../src/utils/interpolate'); + + const { + setModifiedPorts, + setLoggingState, + setNetworkMode, + setNetworks, + setVolumes, + setEnvironmentVariables, + setDevices + } = require('../../../src/utils/commonCompileLogic'); + + const { + checkPortConflicts, + checkNetworkConflicts, + checkDependencyServices + } = require('../../../src/utils/commonBuildChecks'); + + + /* + Order: + 1. compile() - merges build options into the final JSON output. + 2. issues() - runs checks on the compile()'ed JSON, and can also test for errors. + 3. assume() - sets required default values if they are not specified in compile(). Once defaults are set, it reruns compile(). This function is optional + 4. build() - sets up scripts and files. + */ + + retr.init = () => { + logger.debug(`ServiceBuilder:init() - '${serviceName}'`); + }; + + const createVolumesDirectory = () => { + return ` +mkdir -p ./volumes/deconz +`; + }; + + const checkVolumesDirectory = () => { + return ` +if [[ ! -d ./volumes/deconz ]]; then + echo "Deconz directory is missing!" + sleep 2 +fi +`; + }; + + const checkDeconzDevice = (devicePath) => { + return ` +if [[ ! -f ${devicePath} ]]; then + echo "Ensure that Deconz has the correct device set in environment and devices list: ${devicePath}" + sleep 2 +fi +`; + }; + + retr.compile = ({ + outputTemplateJson, + buildOptions + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:compile() - '${serviceName}' started`); + + const compileResults = { + modifiedPorts: setModifiedPorts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedLogging: setLoggingState({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworkMode: setNetworkMode({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworks: setNetworks({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedVolumes: setVolumes({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedEnvironment: setEnvironmentVariables({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedDevices: setDevices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }) + }; + + const selectedDevice = buildOptions?.configurations?.services?.[serviceName]?.selectedDevice ?? ''; + + // Set deconz's selected device device + const deconzDevicesList = outputTemplateJson?.services?.[serviceName]?.devices ?? []; + if (Array.isArray(deconzDevicesList) && deconzDevicesList.length > 0) { + deconzDevicesList.forEach((device, index) => { + outputTemplateJson.services[serviceName].devices[index] = byName( + outputTemplateJson.services[serviceName].devices[index], + { + deconzSelectedDevice: selectedDevice + } + ); + }); + } else { + if ((outputTemplateJson?.services?.[serviceName] ?? false) && selectedDevice) { + outputTemplateJson.services[serviceName].devices = [selectedDevice]; + } + } + + // Set deconz's selected device env var + const serviceEnvironmentList = outputTemplateJson?.services?.[serviceName]?.environment ?? []; + if (Array.isArray(serviceEnvironmentList) && serviceEnvironmentList.length > 0) { + serviceEnvironmentList.forEach((envKVP, index) => { + outputTemplateJson.services[serviceName].environment[index] = byName( + outputTemplateJson.services[serviceName].environment[index], + { + deconzSelectedDevice: selectedDevice + } + ); + }); + } + + console.info(`ServiceBuilder:compile() - '${serviceName}' Results:`, compileResults); + + console.info(`ServiceBuilder:compile() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + return reject({ + component: `ServiceBuilder::compile() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.issues = ({ + outputTemplateJson, + buildOptions, + tmpPath + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:issues() - '${serviceName}' started`); + let issues = []; + + const portConflicts = checkPortConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...portConflicts]; + + const serviceDependencies = checkDependencyServices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...serviceDependencies]; + + const networkConflicts = checkNetworkConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + if (networkConflicts) { + issues.push(networkConflicts); + } + + console.info(`ServiceBuilder:issues() - '${serviceName}' Issues found: ${issues.length}`); + console.info(`ServiceBuilder:issues() - '${serviceName}' completed`); + return resolve(issues); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::issues() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.build = ({ + outputTemplateJson, + buildOptions, + tmpPath, + zipList, + prebuildScripts, + postbuildScripts + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:build() - '${serviceName}' started`); + + if (buildOptions?.configurations?.services?.[serviceName]?.selectedDevice ?? false) { + postbuildScripts.push({ + serviceName, + comment: 'Check deconz set env device', + multilineComment: null, + code: checkDeconzDevice(buildOptions?.configurations?.services?.[serviceName]?.selectedDevice ?? '/dev/null') + }); + } + + prebuildScripts.push({ + serviceName, + comment: 'Create required service directory exists for first launch', + multilineComment: null, + code: createVolumesDirectory() + }); + + postbuildScripts.push({ + serviceName, + comment: 'Ensure required service directory exists for launch', + multilineComment: null, + code: checkVolumesDirectory() + }); + + console.info(`ServiceBuilder:build() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::build() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + return retr; +} + +module.exports = ServiceBuilder; diff --git a/.internal/templates/services/deconz/config.js b/.internal/templates/services/deconz/config.js new file mode 100644 index 000000000..679e770ca --- /dev/null +++ b/.internal/templates/services/deconz/config.js @@ -0,0 +1,72 @@ +const deconz = () => { + const retr = {}; + + const serviceName = 'deconz'; + + retr.getConfigOptions = () => { + return { + serviceName, // Required + labeledPorts: { + "8090:80": 'http', + "433:433": 'ssl', + "5900:5900": 'other' + }, + modifyableEnvironment: [ + { + key: 'DECONZ_VNC_MODE', + value: '1' + }, + { + key: 'DECONZ_VNC_PASSWORD', + value: '{$randomPassword}' + }, + { + key: 'DECONZ_DEVICE', + value: '{$deconzSelectedDevice}' + } + ], + devices: true, + volumes: true, + networks: true, + deconzSelectedDevice: true, + logging: true + } + }; + + retr.getHelp = () => { + return { + serviceName, // Required + links: { + "Website": 'https://www.dresden-elektronik.com/wireless/software/deconz.html', // Website of service + serviceDocs: '', // Official link to docs of service + "Docker": 'https://hub.docker.com/r/marthoc/deconz', // Docker of service + "Source Code": 'https://github.com/marthoc/docker-deconz', // Sourcecode of service + community: '', // Community link + communityChat: '', // Discord, gitter etc + other: '', // Other links + rawMarkdownRemote: '', // Usually links to github raw help pages. + rawMarkdownLocal: '', // Relative path to docs locally + "IOTstack Documentation for {$displayName}": 'https://sensorsiot.github.io/IOTstack/Containers/Deconz/' // Usually links to the github page for this service. + } + }; + }; + + retr.getCommands = () => { + return { + commands: {} // Key/value pair of helper commands user can run locally + }; + }; + + retr.getMeta = () => { + return { + serviceName, // Required + displayName: 'Deconz', + serviceTypeTags: ['iot'], + iconUri: '/logos/deconz.png' + }; + }; + + return retr; +}; + +module.exports = deconz; diff --git a/.templates/deconz/service.yml b/.internal/templates/services/deconz/template.yml similarity index 64% rename from .templates/deconz/service.yml rename to .internal/templates/services/deconz/template.yml index b3d243ecb..96bf83a2f 100644 --- a/.templates/deconz/service.yml +++ b/.internal/templates/services/deconz/template.yml @@ -1,5 +1,5 @@ deconz: - image: marthoc/deconz + image: deconzcommunity/deconz container_name: deconz restart: unless-stopped ports: @@ -7,9 +7,9 @@ deconz: - "443:443" - "5901:5900" volumes: - - ./volumes/deconz/:/root/.local/share/dresden-elektronik/deCONZ - devices: # This list is replaced during the build process. Modify the list in "build_settings.yml" to change it. - - /dev/null + - ./volumes/deconz:/opt/deCONZ + devices: # This list is replaced during the build process. Modify the list in "config.js" to change it. + - "{$deconzSelectedDevice}" environment: - DECONZ_VNC_MODE=1 - DECONZ_VNC_PASSWORD=%randomPassword% @@ -18,6 +18,7 @@ deconz: - DEBUG_ZCL=0 - DEBUG_ZDP=0 - DEBUG_OTAU=0 + - DECONZ_DEVICE=/dev/null # This is updated during the build process. networks: - iotstack_nw diff --git a/.internal/templates/services/diyhue/build.js b/.internal/templates/services/diyhue/build.js new file mode 100644 index 000000000..b422c524d --- /dev/null +++ b/.internal/templates/services/diyhue/build.js @@ -0,0 +1,173 @@ +const ServiceBuilder = ({ + settings, + version, + logger +}) => { + const retr = {}; + const serviceName = 'diyhue'; + + const { + setModifiedPorts, + setLoggingState, + setNetworkMode, + setNetworks, + setEnvironmentVariables, + setDevices + } = require('../../../src/utils/commonCompileLogic'); + + const { + checkPortConflicts, + checkNetworkConflicts, + checkDependencyServices + } = require('../../../src/utils/commonBuildChecks'); + + /* + Order: + 1. compile() - merges build options into the final JSON output. + 2. issues() - runs checks on the compile()'ed JSON, and can also test for errors. + 3. assume() - sets required default values if they are not specified in compile(). Once defaults are set, it reruns compile(). This function is optional + 4. build() - sets up scripts and files. + */ + + retr.init = () => { + logger.debug(`ServiceBuilder:init() - '${serviceName}'`); + }; + + const createVolumesDirectory = () => { + return ` +mkdir -p ./volumes/diyhue +`; + }; + + const checkVolumesDirectory = () => { + return ` +if [[ ! -d ./volumes/diyhue ]]; then + echo "diyhue directory is missing!" + sleep 2 +fi +`; + }; + + retr.compile = ({ + outputTemplateJson, + buildOptions, + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:compile() - '${serviceName}' started`); + + const compileResults = { + modifiedPorts: setModifiedPorts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedLogging: setLoggingState({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworkMode: setNetworkMode({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworks: setNetworks({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedEnvironment: setEnvironmentVariables({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedDevices: setDevices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }) + }; + console.info(`ServiceBuilder:compile() - '${serviceName}' Results:`, compileResults); + + console.info(`ServiceBuilder:compile() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + return reject({ + component: `ServiceBuilder::compile() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.issues = ({ + outputTemplateJson, + buildOptions, + tmpPath + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:issues() - '${serviceName}' started`); + let issues = []; + + const portConflicts = checkPortConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...portConflicts]; + + const serviceDependencies = checkDependencyServices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...serviceDependencies]; + + const networkConflicts = checkNetworkConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + if (networkConflicts) { + issues.push(networkConflicts); + } + + console.info(`ServiceBuilder:issues() - '${serviceName}' Issues found: ${issues.length}`); + console.info(`ServiceBuilder:issues() - '${serviceName}' completed`); + return resolve(issues); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::issues() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.build = ({ + outputTemplateJson, + buildOptions, + tmpPath, + zipList, + prebuildScripts, + postbuildScripts + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:build() - '${serviceName}' started`); + + prebuildScripts.push({ + serviceName, + comment: 'Create required service directory exists for first launch', + multilineComment: null, + code: createVolumesDirectory() + }); + + postbuildScripts.push({ + serviceName, + comment: 'Ensure required service directory exists for launch', + multilineComment: null, + code: checkVolumesDirectory() + }); + + console.info(`ServiceBuilder:build() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::build() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + return retr; +} + +module.exports = ServiceBuilder; diff --git a/.internal/templates/services/diyhue/config.js b/.internal/templates/services/diyhue/config.js new file mode 100644 index 000000000..f87f10544 --- /dev/null +++ b/.internal/templates/services/diyhue/config.js @@ -0,0 +1,68 @@ +const diyhue = () => { + const retr = {}; + + const serviceName = 'diyhue'; + + retr.getConfigOptions = () => { + return { + serviceName, // Required + labeledPorts: { + "8070:8070": 'http' + }, + modifyableEnvironment: [ + { + key: 'TZ', + value: 'Etc/UTC' + }, + { + key: 'IP', + value: 'Your.LAN.IP' + }, + { + key: 'MAC', + value: 'MAC:Address:Here' + } + ], + volumes: false, + networks: false, + logging: true + } + }; + + retr.getHelp = () => { + return { + serviceName, // Required + links: { + "Website": 'https://diyhue.org/', // Website of service + serviceDocs: '', // Official link to docs of service + "Docker": 'https://hub.docker.com/r/diyhue/core/', // Docker of service + "Source Code": 'https://github.com/diyhue/diyHue', // Sourcecode of service + community: '', // Community link + communityChat: '', // Discord, gitter etc + other: '', // Other links + rawMarkdownRemote: '', // Usually links to github raw help pages. + rawMarkdownLocal: '', // Relative path to docs locally + "IOTstack Documentation for {$displayName}": 'https://sensorsiot.github.io/IOTstack/Containers/diyhue/' // Usually links to the github page for this service. + } + }; + }; + + retr.getCommands = () => { + return { + commands: {} // Key/value pair of helper commands user can run locally + }; + }; + + retr.getMeta = () => { + return { + serviceName, // Required + displayName: 'diyhue', + serviceTypeTags: ['wui', 'iot'], + iconUri: '/logos/diyhue.png' + }; + }; + + return retr; +}; + +module.exports = diyhue; diff --git a/.templates/diyhue/service.yml b/.internal/templates/services/diyhue/template.yml similarity index 79% rename from .templates/diyhue/service.yml rename to .internal/templates/services/diyhue/template.yml index d986d8c06..ec159b331 100644 --- a/.templates/diyhue/service.yml +++ b/.internal/templates/services/diyhue/template.yml @@ -2,7 +2,7 @@ diyhue: container_name: diyhue image: diyhue/core:latest ports: - - "8070:80/tcp" + - "8060:80/tcp" - "1900:1900/udp" - "1982:1982/udp" - "2100:2100/udp" @@ -10,7 +10,7 @@ diyhue: - IP=%LAN_IP_Address% - MAC=%LAN_MAC_Address% volumes: - - ./volumes/diyhue/:/opt/hue-emulator/export/ + - ./volumes/diyhue:/opt/hue-emulator/export restart: unless-stopped networks: - iotstack_nw diff --git a/.internal/templates/services/domoticz/build.js b/.internal/templates/services/domoticz/build.js new file mode 100644 index 000000000..5dcab82ff --- /dev/null +++ b/.internal/templates/services/domoticz/build.js @@ -0,0 +1,144 @@ +const ServiceBuilder = ({ + settings, + version, + logger +}) => { + const retr = {}; + const serviceName = 'domoticz'; + + const { + setModifiedPorts, + setLoggingState, + setNetworkMode, + setNetworks, + setEnvironmentVariables, + setDevices + } = require('../../../src/utils/commonCompileLogic'); + + const { + checkPortConflicts, + checkNetworkConflicts, + checkDependencyServices + } = require('../../../src/utils/commonBuildChecks'); + + /* + Order: + 1. compile() - merges build options into the final JSON output. + 2. issues() - runs checks on the compile()'ed JSON, and can also test for errors. + 3. assume() - sets required default values if they are not specified in compile(). Once defaults are set, it reruns compile(). This function is optional + 4. build() - sets up scripts and files. + */ + + retr.init = () => { + logger.debug(`ServiceBuilder:init() - '${serviceName}'`); + }; + + retr.compile = ({ + outputTemplateJson, + buildOptions, + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:compile() - '${serviceName}' started`); + + const compileResults = { + modifiedPorts: setModifiedPorts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedLogging: setLoggingState({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworkMode: setNetworkMode({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworks: setNetworks({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedEnvironment: setEnvironmentVariables({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedDevices: setDevices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }) + }; + console.info(`ServiceBuilder:compile() - '${serviceName}' Results:`, compileResults); + + console.info(`ServiceBuilder:compile() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + return reject({ + component: `ServiceBuilder::compile() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.issues = ({ + outputTemplateJson, + buildOptions, + tmpPath + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:issues() - '${serviceName}' started`); + let issues = []; + + const portConflicts = checkPortConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...portConflicts]; + + const serviceDependencies = checkDependencyServices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...serviceDependencies]; + + const networkConflicts = checkNetworkConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + if (networkConflicts) { + issues.push(networkConflicts); + } + + console.info(`ServiceBuilder:issues() - '${serviceName}' Issues found: ${issues.length}`); + console.info(`ServiceBuilder:issues() - '${serviceName}' completed`); + return resolve(issues); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::issues() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.build = ({ + outputTemplateJson, + buildOptions, + tmpPath, + zipList, + prebuildScripts, + postbuildScripts + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:build() - '${serviceName}' started`); + // Code here + console.info(`ServiceBuilder:build() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::build() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + return retr; +} + +module.exports = ServiceBuilder; diff --git a/.internal/templates/services/domoticz/config.js b/.internal/templates/services/domoticz/config.js new file mode 100644 index 000000000..4c837d0ef --- /dev/null +++ b/.internal/templates/services/domoticz/config.js @@ -0,0 +1,55 @@ +const domoticz = () => { + const retr = {}; + + const serviceName = 'domoticz'; + + retr.getConfigOptions = () => { + return { + serviceName, // Required + labeledPorts: { + "8080:8080": 'http', + "1443:1443": 'ssl' + }, + volumes: false, + networks: false, + logging: true + } + }; + + retr.getHelp = () => { + return { + serviceName, // Required + links: { + "Website": 'https://www.domoticz.com/', // Website of service + serviceDocs: '', // Official link to docs of service + "Docker": 'https://hub.docker.com/r/linuxserver/domoticz/', // Docker of service + "Source Code": 'https://github.com/linuxserver/docker-domoticz', // Sourcecode of service + community: '', // Community link + "Community Chat (Discord)": 'https://discord.gg/YWrKVTn', // Discord, gitter etc + other: '', // Other links + rawMarkdownRemote: '', // Usually links to github raw help pages. + rawMarkdownLocal: '', // Relative path to docs locally + "IOTstack Documentation for {$displayName}": 'https://sensorsiot.github.io/IOTstack/Containers/Domoticz/' // Usually links to the github page for this service. + } + }; + }; + + retr.getCommands = () => { + return { + commands: {} // Key/value pair of helper commands user can run locally + }; + }; + + retr.getMeta = () => { + return { + serviceName, // Required + displayName: 'Domoticz', + serviceTypeTags: ['wui', 'dashboard', 'home automation', 'iot', 'z-wave'], + iconUri: '/logos/domoticz.png' + }; + }; + + return retr; +}; + +module.exports = domoticz; diff --git a/.templates/domoticz/service.yml b/.internal/templates/services/domoticz/template.yml similarity index 71% rename from .templates/domoticz/service.yml rename to .internal/templates/services/domoticz/template.yml index ceefd7762..86419b604 100644 --- a/.templates/domoticz/service.yml +++ b/.internal/templates/services/domoticz/template.yml @@ -1,16 +1,16 @@ domoticz: container_name: domoticz - image: linuxserver/domoticz:stable + image: lscr.io/linuxserver/domoticz:latest ports: - - "8080:8080" + - "8083:8080" - "6144:6144" - "1443:1443" volumes: - ./volumes/domoticz/data:/config restart: unless-stopped - network_mode: bridge environment: - PUID=1000 - PGID=1000 - # - TZ= + # - TZ=Etc/UTC # - WEBROOT=domoticz + diff --git a/.internal/templates/services/dozzle/build.js b/.internal/templates/services/dozzle/build.js new file mode 100644 index 000000000..3e4eac5e9 --- /dev/null +++ b/.internal/templates/services/dozzle/build.js @@ -0,0 +1,144 @@ +const ServiceBuilder = ({ + settings, + version, + logger +}) => { + const retr = {}; + const serviceName = 'dozzle'; + + const { + setModifiedPorts, + setLoggingState, + setNetworkMode, + setNetworks, + setEnvironmentVariables, + setDevices + } = require('../../../src/utils/commonCompileLogic'); + + const { + checkPortConflicts, + checkNetworkConflicts, + checkDependencyServices + } = require('../../../src/utils/commonBuildChecks'); + + /* + Order: + 1. compile() - merges build options into the final JSON output. + 2. issues() - runs checks on the compile()'ed JSON, and can also test for errors. + 3. assume() - sets required default values if they are not specified in compile(). Once defaults are set, it reruns compile(). This function is optional + 4. build() - sets up scripts and files. + */ + + retr.init = () => { + logger.debug(`ServiceBuilder:init() - '${serviceName}'`); + }; + + retr.compile = ({ + outputTemplateJson, + buildOptions, + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:compile() - '${serviceName}' started`); + + const compileResults = { + modifiedPorts: setModifiedPorts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedLogging: setLoggingState({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworkMode: setNetworkMode({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworks: setNetworks({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedEnvironment: setEnvironmentVariables({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedDevices: setDevices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }) + }; + console.info(`ServiceBuilder:compile() - '${serviceName}' Results:`, compileResults); + + console.info(`ServiceBuilder:compile() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + return reject({ + component: `ServiceBuilder::compile() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.issues = ({ + outputTemplateJson, + buildOptions, + tmpPath + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:issues() - '${serviceName}' started`); + let issues = []; + + const portConflicts = checkPortConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...portConflicts]; + + const serviceDependencies = checkDependencyServices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...serviceDependencies]; + + const networkConflicts = checkNetworkConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + if (networkConflicts) { + issues.push(networkConflicts); + } + + console.info(`ServiceBuilder:issues() - '${serviceName}' Issues found: ${issues.length}`); + console.info(`ServiceBuilder:issues() - '${serviceName}' completed`); + return resolve(issues); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::issues() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.build = ({ + outputTemplateJson, + buildOptions, + tmpPath, + zipList, + prebuildScripts, + postbuildScripts + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:build() - '${serviceName}' started`); + // Code here + console.info(`ServiceBuilder:build() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::build() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + return retr; +} + +module.exports = ServiceBuilder; diff --git a/.internal/templates/services/dozzle/config.js b/.internal/templates/services/dozzle/config.js new file mode 100644 index 000000000..9fa65ed69 --- /dev/null +++ b/.internal/templates/services/dozzle/config.js @@ -0,0 +1,54 @@ +const dozzle = () => { + const retr = {}; + + const serviceName = 'dozzle'; + + retr.getConfigOptions = () => { + return { + serviceName, // Required + labeledPorts: { + "8888:8080": 'http' + }, + volumes: false, + networks: false, + logging: true + } + }; + + retr.getHelp = () => { + return { + serviceName, // Required + links: { + website: '', // Website of service + serviceDocs: '', // Official link to docs of service + "Docker": 'https://hub.docker.com/u/amir20', // Docker of service + "Source Code": 'https://github.com/amir20/dozzle', // Sourcecode of service + community: '', // Community link + communityChat: '', // Discord, gitter etc + other: '', // Other links + rawMarkdownRemote: '', // Usually links to github raw help pages. + rawMarkdownLocal: '', // Relative path to docs locally + "IOTstack Documentation for {$displayName}": 'https://sensorsiot.github.io/IOTstack/Containers/Dozzle/' // Usually links to the github page for this service. + } + }; + }; + + retr.getCommands = () => { + return { + commands: {} // Key/value pair of helper commands user can run locally + }; + }; + + retr.getMeta = () => { + return { + serviceName, // Required + displayName: 'Dozzle', + serviceTypeTags: ['logs', 'docker', 'container manager', 'wui'], + iconUri: '/logos/dozzle.svg' + }; + }; + + return retr; +}; + +module.exports = dozzle; diff --git a/.templates/dozzle/service.yml b/.internal/templates/services/dozzle/template.yml similarity index 72% rename from .templates/dozzle/service.yml rename to .internal/templates/services/dozzle/template.yml index 35209827f..d9c423478 100644 --- a/.templates/dozzle/service.yml +++ b/.internal/templates/services/dozzle/template.yml @@ -3,7 +3,9 @@ dozzle: image: amir20/dozzle:latest restart: unless-stopped network_mode: host - #ports: - # - "8888:8080" volumes: - /var/run/docker.sock:/var/run/docker.sock + logging: + options: + max-size: "5m" + max-file: "3" diff --git a/.internal/templates/services/espruinohub/build.js b/.internal/templates/services/espruinohub/build.js new file mode 100644 index 000000000..9894611d4 --- /dev/null +++ b/.internal/templates/services/espruinohub/build.js @@ -0,0 +1,140 @@ +const ServiceBuilder = ({ + settings, + version, + logger +}) => { + const retr = {}; + const serviceName = 'espruinohub'; + + const { + setModifiedPorts, + setLoggingState, + setNetworkMode, + setNetworks + } = require('../../../src/utils/commonCompileLogic'); + + const { + checkPortConflicts, + checkNetworkConflicts, + checkDependencyServices + } = require('../../../src/utils/commonBuildChecks'); + + /* + Order: + 1. compile() - merges build options into the final JSON output. + 2. issues() - runs checks on the compile()'ed JSON, and can also test for errors. + 3. assume() - sets required default values if they are not specified in compile(). Once defaults are set, it reruns compile(). This function is optional + 4. build() - sets up scripts and files. + */ + + retr.init = () => { + logger.debug(`ServiceBuilder:init() - '${serviceName}'`); + }; + + retr.compile = ({ + outputTemplateJson, + buildOptions, + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:compile() - '${serviceName}' started`); + + const compileResults = { + modifiedPorts: setModifiedPorts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedLogging: setLoggingState({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworkMode: setNetworkMode({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworks: setNetworks({ buildTemplate: outputTemplateJson, buildOptions, serviceName }) + }; + console.info(`ServiceBuilder:compile() - '${serviceName}' Results:`, compileResults); + + console.info(`ServiceBuilder:compile() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + return reject({ + component: `ServiceBuilder::compile() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.issues = ({ + outputTemplateJson, + buildOptions, + tmpPath + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:issues() - '${serviceName}' started`); + let issues = []; + + const portConflicts = checkPortConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...portConflicts]; + + const serviceDependencies = checkDependencyServices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...serviceDependencies]; + + const networkConflicts = checkNetworkConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + if (networkConflicts) { + issues.push(networkConflicts); + } + + console.info(`ServiceBuilder:issues() - '${serviceName}' Issues found: ${issues.length}`); + console.info(`ServiceBuilder:issues() - '${serviceName}' completed`); + return resolve(issues); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::issues() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.build = ({ + outputTemplateJson, + buildOptions, + tmpPath, + zipList, + prebuildScripts, + postbuildScripts + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:build() - '${serviceName}' started`); + // Code here + console.info(`ServiceBuilder:build() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::build() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + return retr; +} + +module.exports = ServiceBuilder; diff --git a/.internal/templates/services/espruinohub/config.js b/.internal/templates/services/espruinohub/config.js new file mode 100644 index 000000000..1f857082f --- /dev/null +++ b/.internal/templates/services/espruinohub/config.js @@ -0,0 +1,54 @@ +const espruinohub = () => { + const retr = {}; + + const serviceName = 'espruinohub'; + + retr.getConfigOptions = () => { + return { + serviceName, // Required + labeledPorts: { + "1888:1888": 'http' + }, + volumes: false, + networks: false, + logging: true + } + }; + + retr.getHelp = () => { + return { + serviceName, // Required + links: { + "Website": '', // Website of service + serviceDocs: '', // Official link to docs of service + "Docker": 'https://hub.docker.com/u/amir20', // Docker of service + "Source Code": 'https://github.com/amir20/dozzle', // Sourcecode of service + community: '', // Community link + communityChat: '', // Discord, gitter etc + other: '', // Other links + rawMarkdownRemote: '', // Usually links to github raw help pages. + rawMarkdownLocal: '', // Relative path to docs locally + "IOTstack Documentation for {$displayName}": 'https://sensorsiot.github.io/IOTstack/Containers/EspruinoHub/' // Usually links to the github page for this service. + } + }; + }; + + retr.getCommands = () => { + return { + commands: {} // Key/value pair of helper commands user can run locally + }; + }; + + retr.getMeta = () => { + return { + serviceName, // Required + displayName: 'EspruinoHub', + serviceTypeTags: ['mqtt', 'ble', 'rpi only'], + iconUri: '/logos/espruinohub.png' + }; + }; + + return retr; +}; + +module.exports = espruinohub; diff --git a/.templates/espruinohub/service.yml b/.internal/templates/services/espruinohub/template.yml similarity index 70% rename from .templates/espruinohub/service.yml rename to .internal/templates/services/espruinohub/template.yml index 21a5a1385..572ba46fe 100644 --- a/.templates/espruinohub/service.yml +++ b/.internal/templates/services/espruinohub/template.yml @@ -4,3 +4,7 @@ espruinohub: network_mode: host privileged: true restart: unless-stopped + logging: + options: + max-size: "5m" + max-file: "3" diff --git a/.internal/templates/services/gitea/build.js b/.internal/templates/services/gitea/build.js new file mode 100644 index 000000000..ced3d08d9 --- /dev/null +++ b/.internal/templates/services/gitea/build.js @@ -0,0 +1,168 @@ +const ServiceBuilder = ({ + settings, + version, + logger +}) => { + const retr = {}; + const serviceName = 'gitea'; + + const { + setModifiedPorts, + setLoggingState, + setNetworkMode, + setNetworks + } = require('../../../src/utils/commonCompileLogic'); + + const { + checkPortConflicts, + checkNetworkConflicts, + checkDependencyServices + } = require('../../../src/utils/commonBuildChecks'); + + /* + Order: + 1. compile() - merges build options into the final JSON output. + 2. issues() - runs checks on the compile()'ed JSON, and can also test for errors. + 3. assume() - sets required default values if they are not specified in compile(). Once defaults are set, it reruns compile(). This function is optional + 4. build() - sets up scripts and files. + */ + + retr.init = () => { + logger.debug(`ServiceBuilder:init() - '${serviceName}'`); + }; + + const createVolumesDirectory = () => { + return ` +mkdir -p ./volumes/gitea/data +`; + }; + + const checkVolumesDirectory = () => { + return ` +if [[ ! -d ./volumes/gitea/data ]]; then + echo "Gitea data directory is missing!" + sleep 2 +fi +`; + }; + + retr.compile = ({ + outputTemplateJson, + buildOptions, + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:compile() - '${serviceName}' started`); + const compileResults = { + modifiedPorts: setModifiedPorts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedLogging: setLoggingState({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworkMode: setNetworkMode({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworks: setNetworks({ buildTemplate: outputTemplateJson, buildOptions, serviceName }) + }; + console.info(`ServiceBuilder:compile() - '${serviceName}' Results:`, compileResults); + + console.info(`ServiceBuilder:compile() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + return reject({ + component: `ServiceBuilder::compile() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.issues = ({ + outputTemplateJson, + buildOptions, + tmpPath + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:issues() - '${serviceName}' started`); + let issues = []; + + const portConflicts = checkPortConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...portConflicts]; + + const serviceDependencies = checkDependencyServices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...serviceDependencies]; + + const networkConflicts = checkNetworkConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + if (networkConflicts) { + issues.push(networkConflicts); + } + + console.info(`ServiceBuilder:issues() - '${serviceName}' Issues found: ${issues.length}`); + console.info(`ServiceBuilder:issues() - '${serviceName}' completed`); + return resolve(issues); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::issues() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.build = ({ + outputTemplateJson, + buildOptions, + tmpPath, + zipList, + prebuildScripts, + postbuildScripts + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:build() - '${serviceName}' started`); + + prebuildScripts.push({ + serviceName, + comment: 'Create required service directory exists for first launch', + multilineComment: null, + code: createVolumesDirectory() + }); + + postbuildScripts.push({ + serviceName, + comment: 'Ensure required service directory exists for launch', + multilineComment: null, + code: checkVolumesDirectory() + }); + + console.info(`ServiceBuilder:build() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::build() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + return retr; +} + +module.exports = ServiceBuilder; diff --git a/.internal/templates/services/gitea/config.js b/.internal/templates/services/gitea/config.js new file mode 100644 index 000000000..7e97ba09d --- /dev/null +++ b/.internal/templates/services/gitea/config.js @@ -0,0 +1,55 @@ +const gitea = () => { + const retr = {}; + + const serviceName = 'gitea'; + + retr.getConfigOptions = () => { + return { + serviceName, // Required + labeledPorts: { + "9080:8080": 'http' + }, + volumes: false, + networks: false, + logging: true + } + }; + + retr.getHelp = () => { + return { + serviceName, // Required + links: { + "Website": 'https://gitea.io/', // Website of service + "{$displayName} Swagger": 'https://try.gitea.io/api/swagger', // Official link to docs of service + "{$displayName} Documentation": 'https://docs.gitea.io/en-us/', // Official link to docs of service + "Docker": 'https://hub.docker.com/r/kunde21/gitea-arm', // Docker of service + "Source Code": 'https://github.com/go-gitea/', // Sourcecode of service + "Community": 'https://discourse.gitea.io/', // Community link + "Community Chat": 'https://discord.gg/Gitea', // Discord, gitter etc + other: '', // Other links + rawMarkdownRemote: '', // Usually links to github raw help pages. + rawMarkdownLocal: '', // Relative path to docs locally + "IOTstack Documentation for {$displayName}": 'https://sensorsiot.github.io/IOTstack/Containers/Gitea/' // Usually links to the github page for this service. + } + }; + }; + + retr.getCommands = () => { + return { + commands: {} // Key/value pair of helper commands user can run locally + }; + }; + + retr.getMeta = () => { + return { + serviceName, // Required + displayName: 'Gitea', + serviceTypeTags: ['wui', 'git'], + iconUri: '/logos/gitea.png' + }; + }; + + return retr; +}; + +module.exports = gitea; diff --git a/.templates/gitea/service.yml b/.internal/templates/services/gitea/template.yml similarity index 69% rename from .templates/gitea/service.yml rename to .internal/templates/services/gitea/template.yml index b9c720eb2..d851d2de8 100644 --- a/.templates/gitea/service.yml +++ b/.internal/templates/services/gitea/template.yml @@ -1,6 +1,7 @@ gitea: container_name: gitea image: "kunde21/gitea-arm:latest" + # image: "gitea/gitea" # x64 restart: unless-stopped ports: - "7920:3000/tcp" @@ -11,5 +12,10 @@ gitea: volumes: - ./volumes/gitea/data:/data - /etc/timezone:/etc/timezone:ro + # - /etc/localtime:/etc/localtime:ro networks: - iotstack_nw + logging: + options: + max-size: "5m" + max-file: "3" diff --git a/.internal/templates/services/grafana/build.js b/.internal/templates/services/grafana/build.js new file mode 100644 index 000000000..f31d9b3e5 --- /dev/null +++ b/.internal/templates/services/grafana/build.js @@ -0,0 +1,139 @@ +const ServiceBuilder = ({ + settings, + version, + logger +}) => { + const retr = {}; + const serviceName = 'grafana'; + + const { + setModifiedPorts, + setLoggingState, + setNetworkMode, + setNetworks + } = require('../../../src/utils/commonCompileLogic'); + + const { + checkPortConflicts, + checkNetworkConflicts, + checkDependencyServices + } = require('../../../src/utils/commonBuildChecks'); + + /* + Order: + 1. compile() - merges build options into the final JSON output. + 2. issues() - runs checks on the compile()'ed JSON, and can also test for errors. + 3. assume() - sets required default values if they are not specified in compile(). Once defaults are set, it reruns compile(). This function is optional + 4. build() - sets up scripts and files. + */ + + retr.init = () => { + logger.debug(`ServiceBuilder:init() - '${serviceName}'`); + }; + + retr.compile = ({ + outputTemplateJson, + buildOptions, + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:compile() - '${serviceName}' started`); + const compileResults = { + modifiedPorts: setModifiedPorts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedLogging: setLoggingState({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworkMode: setNetworkMode({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworks: setNetworks({ buildTemplate: outputTemplateJson, buildOptions, serviceName }) + }; + console.info(`ServiceBuilder:compile() - '${serviceName}' Results:`, compileResults); + + console.info(`ServiceBuilder:compile() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + return reject({ + component: `ServiceBuilder::compile() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.issues = ({ + outputTemplateJson, + buildOptions, + tmpPath + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:issues() - '${serviceName}' started`); + let issues = []; + + const portConflicts = checkPortConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...portConflicts]; + + const serviceDependencies = checkDependencyServices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...serviceDependencies]; + + const networkConflicts = checkNetworkConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + if (networkConflicts) { + issues.push(networkConflicts); + } + + console.info(`ServiceBuilder:issues() - '${serviceName}' Issues found: ${issues.length}`); + console.info(`ServiceBuilder:issues() - '${serviceName}' completed`); + return resolve(issues); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::issues() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.build = ({ + outputTemplateJson, + buildOptions, + tmpPath, + zipList, + prebuildScripts, + postbuildScripts + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:build() - '${serviceName}' started`); + // Code here + console.info(`ServiceBuilder:build() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::build() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + return retr; +} + +module.exports = ServiceBuilder; diff --git a/.internal/templates/services/grafana/config.js b/.internal/templates/services/grafana/config.js new file mode 100644 index 000000000..877dc5af7 --- /dev/null +++ b/.internal/templates/services/grafana/config.js @@ -0,0 +1,63 @@ +const grafana = () => { + const retr = {}; + + const serviceName = 'grafana'; + + retr.getConfigOptions = () => { + return { + serviceName, // Required + labeledPorts: { + "3000:3000": 'http' + }, + modifyableEnvironment: [ + { + key: 'GF_PATHS_DATA', + value: '/var/lib/grafana' + }, + { + key: 'GF_PATHS_LOGS', + value: '/var/log/grafana' + } + ], + volumes: true, + networks: true, + logging: true + } + }; + + retr.getHelp = () => { + return { + serviceName, // Required + links: { + "Website": 'https://grafana.com/', // Website of service + "Docker": 'https://hub.docker.com/r/grafana/grafana', // Docker of service + "Source Code": 'https://github.com/grafana/grafana', // Sourcecode of service + "Community": 'https://community.grafana.com/', // Community link + "Tutorials": 'https://grafana.com/tutorials/', // Discord, gitter etc + other: '', // Other links + rawMarkdownRemote: '', // Usually links to github raw help pages. + rawMarkdownLocal: '', // Relative path to docs locally + "IOTstack Documentation for {$displayName}": 'https://sensorsiot.github.io/IOTstack/Containers/Grafana/' // Usually links to the github page for this service. + } + }; + }; + + retr.getCommands = () => { + return { + commands: {} // Key/value pair of helper commands user can run locally + }; + }; + + retr.getMeta = () => { + return { + serviceName, // Required + displayName: 'Grafana', + serviceTypeTags: ['aggregator', 'wui', 'graphs', 'dashboard'], + iconUri: '/logos/grafana.svg' + }; + }; + + return retr; +}; + +module.exports = grafana; diff --git a/.internal/templates/services/grafana/template.yml b/.internal/templates/services/grafana/template.yml new file mode 100644 index 000000000..98298430c --- /dev/null +++ b/.internal/templates/services/grafana/template.yml @@ -0,0 +1,34 @@ +grafana: + container_name: grafana + image: grafana/grafana + restart: unless-stopped + user: "0" + ports: + - "3000:3000" + environment: + - GF_PATHS_DATA=/var/lib/grafana + - GF_PATHS_LOGS=/var/log/grafana + # - TZ=Etc/UTC + ## [SERVER] + # - GF_SERVER_ROOT_URL=http://localhost:3000/grafana + # - GF_SERVER_SERVE_FROM_SUB_PATH=true + ## [SECURITY] + # - GF_SECURITY_ADMIN_USER=admin + # - GF_SECURITY_ADMIN_PASSWORD=admin + # - GF_SECURITY_ALLOW_EMBEDDING=true # Allows embedding externally + # - GF_AUTH_ANONYMOUS_ENABLED=true # Allows public read access to graphs + volumes: + - ./volumes/grafana/data:/var/lib/grafana + - ./volumes/grafana/log:/var/log/grafana + networks: + - iotstack_nw + logging: + options: + max-size: "5m" + max-file: "3" + healthcheck: + test: ["CMD", "wget", "-O", "/dev/null", "http://localhost:3000"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s diff --git a/.internal/templates/services/heimdall/build.js b/.internal/templates/services/heimdall/build.js new file mode 100644 index 000000000..9929a6a78 --- /dev/null +++ b/.internal/templates/services/heimdall/build.js @@ -0,0 +1,146 @@ +const ServiceBuilder = ({ + settings, + version, + logger +}) => { + const retr = {}; + const serviceName = 'heimdall'; + + const { + setImageTag, + setModifiedPorts, + setLoggingState, + setNetworkMode, + setNetworks, + setEnvironmentVariables, + setDevices + } = require('../../../src/utils/commonCompileLogic'); + + const { + checkPortConflicts, + checkNetworkConflicts, + checkDependencyServices + } = require('../../../src/utils/commonBuildChecks'); + + /* + Order: + 1. compile() - merges build options into the final JSON output. + 2. issues() - runs checks on the compile()'ed JSON, and can also test for errors. + 3. assume() - sets required default values if they are not specified in compile(). Once defaults are set, it reruns compile(). This function is optional + 4. build() - sets up scripts and files. + */ + + retr.init = () => { + logger.debug(`ServiceBuilder:init() - '${serviceName}'`); + }; + + retr.compile = ({ + outputTemplateJson, + buildOptions, + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:compile() - '${serviceName}' started`); + + const compileResults = { + modifiedImage: setImageTag({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedPorts: setModifiedPorts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedLogging: setLoggingState({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworkMode: setNetworkMode({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworks: setNetworks({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedEnvironment: setEnvironmentVariables({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedDevices: setDevices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }) + }; + console.info(`ServiceBuilder:compile() - '${serviceName}' Results:`, compileResults); + + console.info(`ServiceBuilder:compile() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + return reject({ + component: `ServiceBuilder::compile() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.issues = ({ + outputTemplateJson, + buildOptions, + tmpPath + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:issues() - '${serviceName}' started`); + let issues = []; + + const portConflicts = checkPortConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...portConflicts]; + + const serviceDependencies = checkDependencyServices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...serviceDependencies]; + + const networkConflicts = checkNetworkConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + if (networkConflicts) { + issues.push(networkConflicts); + } + + console.info(`ServiceBuilder:issues() - '${serviceName}' Issues found: ${issues.length}`); + console.info(`ServiceBuilder:issues() - '${serviceName}' completed`); + return resolve(issues); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::issues() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.build = ({ + outputTemplateJson, + buildOptions, + tmpPath, + zipList, + prebuildScripts, + postbuildScripts + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:build() - '${serviceName}' started`); + // Code here + console.info(`ServiceBuilder:build() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::build() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + return retr; +} + +module.exports = ServiceBuilder; diff --git a/.internal/templates/services/heimdall/config.js b/.internal/templates/services/heimdall/config.js new file mode 100644 index 000000000..b4d4afb83 --- /dev/null +++ b/.internal/templates/services/heimdall/config.js @@ -0,0 +1,54 @@ +const heimdall = () => { + const retr = {}; + + const serviceName = 'heimdall'; + + retr.getConfigOptions = () => { + return { + serviceName, // Required + modifyableEnvironment: [ + { + key: 'TZ', + value: 'Etc/UTC2' + } + ], + volumes: true, + networks: false, + logging: true + } + }; + + retr.getHelp = () => { + return { + serviceName, // Required + links: { + website: '', // Website of service + "Docker": 'https://hub.docker.com/r/linuxserver/heimdall', // Docker of service + "Source Code": 'https://github.com/linuxserver/Heimdall', // Sourcecode of service + other: '', // Other links + rawMarkdownRemote: '', // Usually links to github raw help pages. + rawMarkdownLocal: '', // Relative path to docs locally + "IOTstack Documentation for {$displayName}": 'https://sensorsiot.github.io/IOTstack/Containers/Heimdall/' + } + }; + }; + + retr.getCommands = () => { + return { + commands: {} // Key/value pair of helper commands user can run locally + }; + }; + + retr.getMeta = () => { + return { + serviceName, // Required + displayName: 'Heimdall', + serviceTypeTags: ['wui', 'database manager'], + iconUri: '/logos/heimdall.png' + }; + }; + + return retr; +}; + +module.exports = heimdall; diff --git a/.templates/heimdall/service.yml b/.internal/templates/services/heimdall/template.yml similarity index 80% rename from .templates/heimdall/service.yml rename to .internal/templates/services/heimdall/template.yml index 17cc79725..0162ca3fa 100644 --- a/.templates/heimdall/service.yml +++ b/.internal/templates/services/heimdall/template.yml @@ -1,13 +1,13 @@ heimdall: image: ghcr.io/linuxserver/heimdall container_name: heimdall + volumes: + - ./volumes/heimdall/config:/config environment: - PUID=1000 - PGID=1000 - - TZ=Europe/Paris - volumes: - - ./volumes/heimdall/config:/config + - TZ=Etc/UTC ports: - - 8880:80 - - 8883:443 + - "8880:80" + - "8883:443" restart: unless-stopped diff --git a/.internal/templates/services/home_assistant/build.js b/.internal/templates/services/home_assistant/build.js new file mode 100644 index 000000000..069417f78 --- /dev/null +++ b/.internal/templates/services/home_assistant/build.js @@ -0,0 +1,168 @@ +const ServiceBuilder = ({ + settings, + version, + logger +}) => { + const retr = {}; + const serviceName = 'home_assistant'; + + const { + setModifiedPorts, + setLoggingState, + setNetworkMode, + setNetworks + } = require('../../../src/utils/commonCompileLogic'); + + const { + checkPortConflicts, + checkNetworkConflicts, + checkDependencyServices + } = require('../../../src/utils/commonBuildChecks'); + + /* + Order: + 1. compile() - merges build options into the final JSON output. + 2. issues() - runs checks on the compile()'ed JSON, and can also test for errors. + 3. assume() - sets required default values if they are not specified in compile(). Once defaults are set, it reruns compile(). This function is optional + 4. build() - sets up scripts and files. + */ + + retr.init = () => { + logger.debug(`ServiceBuilder:init() - '${serviceName}'`); + }; + + const createVolumesDirectory = () => { + return ` +mkdir -p ./volumes/home_assistant +`; + }; + + const checkVolumesDirectory = () => { + return ` +if [[ ! -d ./volumes/home_assistant ]]; then + echo "Home Assistant directory is missing!" + sleep 2 +fi +`; + }; + + retr.compile = ({ + outputTemplateJson, + buildOptions, + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:compile() - '${serviceName}' started`); + const compileResults = { + modifiedPorts: setModifiedPorts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedLogging: setLoggingState({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworkMode: setNetworkMode({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworks: setNetworks({ buildTemplate: outputTemplateJson, buildOptions, serviceName }) + }; + console.info(`ServiceBuilder:compile() - '${serviceName}' Results:`, compileResults); + + console.info(`ServiceBuilder:compile() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + return reject({ + component: `ServiceBuilder::compile() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.issues = ({ + outputTemplateJson, + buildOptions, + tmpPath + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:issues() - '${serviceName}' started`); + let issues = []; + + const portConflicts = checkPortConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...portConflicts]; + + const serviceDependencies = checkDependencyServices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...serviceDependencies]; + + const networkConflicts = checkNetworkConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + if (networkConflicts) { + issues.push(networkConflicts); + } + + console.info(`ServiceBuilder:issues() - '${serviceName}' Issues found: ${issues.length}`); + console.info(`ServiceBuilder:issues() - '${serviceName}' completed`); + return resolve(issues); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::issues() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.build = ({ + outputTemplateJson, + buildOptions, + tmpPath, + zipList, + prebuildScripts, + postbuildScripts + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:build() - '${serviceName}' started`); + + prebuildScripts.push({ + serviceName, + comment: 'Create required service directory exists for first launch', + multilineComment: null, + code: createVolumesDirectory() + }); + + postbuildScripts.push({ + serviceName, + comment: 'Ensure required service directory exists for launch', + multilineComment: null, + code: checkVolumesDirectory() + }); + + console.info(`ServiceBuilder:build() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::build() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + return retr; +} + +module.exports = ServiceBuilder; diff --git a/.internal/templates/services/home_assistant/config.js b/.internal/templates/services/home_assistant/config.js new file mode 100644 index 000000000..57c1e9e39 --- /dev/null +++ b/.internal/templates/services/home_assistant/config.js @@ -0,0 +1,49 @@ +const home_assistant = () => { + const retr = {}; + + const serviceName = 'home_assistant'; + + retr.getConfigOptions = () => { + return { + serviceName, // Required + labeledPorts: { + "8123:8123": 'http' + }, + volumes: true, + networks: true, + logging: true + } + }; + + retr.getHelp = () => { + return { + serviceName, // Required + links: { + "Website": 'https://www.home-assistant.io/', // Website of service + "Docker": 'https://hub.docker.com/r/linuxserver/homeassistant', // Docker of service + rawMarkdownRemote: '', // Usually links to github raw help pages. + rawMarkdownLocal: '', // Relative path to docs locally + "IOTstack Documentation for {$displayName}": 'https://sensorsiot.github.io/IOTstack/Containers/Home-Assistant/' + } + }; + }; + + retr.getCommands = () => { + return { + commands: {} // Key/value pair of helper commands user can run locally + }; + }; + + retr.getMeta = () => { + return { + serviceName, // Required + displayName: 'Home Assistant (untested)', + serviceTypeTags: ['wui', 'dashboard', 'home automation', 'iot'], + iconUri: '/logos/homeassistant.png' + }; + }; + + return retr; +}; + +module.exports = home_assistant; diff --git a/.internal/templates/services/home_assistant/template.yml b/.internal/templates/services/home_assistant/template.yml new file mode 100644 index 000000000..cafb94cc0 --- /dev/null +++ b/.internal/templates/services/home_assistant/template.yml @@ -0,0 +1,10 @@ +home_assistant: + container_name: home_assistant + image: ghcr.io/home-assistant/home-assistant:stable + #image: ghcr.io/home-assistant/raspberrypi3-homeassistant:stable + #image: ghcr.io/home-assistant/raspberrypi4-homeassistant:stable + restart: unless-stopped + network_mode: host + volumes: + - /etc/localtime:/etc/localtime:ro + - ./volumes/home_assistant:/config diff --git a/.internal/templates/services/homebridge/build.js b/.internal/templates/services/homebridge/build.js new file mode 100644 index 000000000..52c1b1fa1 --- /dev/null +++ b/.internal/templates/services/homebridge/build.js @@ -0,0 +1,140 @@ +const ServiceBuilder = ({ + settings, + version, + logger +}) => { + const retr = {}; + const serviceName = 'homebridge'; + + const { + setModifiedPorts, + setLoggingState, + setNetworkMode, + setNetworks + } = require('../../../src/utils/commonCompileLogic'); + + const { + checkPortConflicts, + checkNetworkConflicts, + checkDependencyServices + } = require('../../../src/utils/commonBuildChecks'); + + /* + Order: + 1. compile() - merges build options into the final JSON output. + 2. issues() - runs checks on the compile()'ed JSON, and can also test for errors. + 3. assume() - sets required default values if they are not specified in compile(). Once defaults are set, it reruns compile(). This function is optional + 4. build() - sets up scripts and files. + */ + + retr.init = () => { + logger.debug(`ServiceBuilder:init() - '${serviceName}'`); + }; + + retr.compile = ({ + outputTemplateJson, + buildOptions, + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:compile() - '${serviceName}' started`); + + const compileResults = { + modifiedPorts: setModifiedPorts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedLogging: setLoggingState({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworkMode: setNetworkMode({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworks: setNetworks({ buildTemplate: outputTemplateJson, buildOptions, serviceName }) + }; + console.info(`ServiceBuilder:compile() - '${serviceName}' Results:`, compileResults); + + console.info(`ServiceBuilder:compile() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + return reject({ + component: `ServiceBuilder::compile() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.issues = ({ + outputTemplateJson, + buildOptions, + tmpPath + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:issues() - '${serviceName}' started`); + let issues = []; + + const portConflicts = checkPortConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...portConflicts]; + + const serviceDependencies = checkDependencyServices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...serviceDependencies]; + + const networkConflicts = checkNetworkConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + if (networkConflicts) { + issues.push(networkConflicts); + } + + console.info(`ServiceBuilder:issues() - '${serviceName}' Issues found: ${issues.length}`); + console.info(`ServiceBuilder:issues() - '${serviceName}' completed`); + return resolve(issues); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::issues() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.build = ({ + outputTemplateJson, + buildOptions, + tmpPath, + zipList, + prebuildScripts, + postbuildScripts + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:build() - '${serviceName}' started`); + // Code here + console.info(`ServiceBuilder:build() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::build() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + return retr; +} + +module.exports = ServiceBuilder; diff --git a/.internal/templates/services/homebridge/config.js b/.internal/templates/services/homebridge/config.js new file mode 100644 index 000000000..ae73f77c1 --- /dev/null +++ b/.internal/templates/services/homebridge/config.js @@ -0,0 +1,50 @@ +const homebridge = () => { + const retr = {}; + + const serviceName = 'homebridge'; + + retr.getConfigOptions = () => { + return { + serviceName, // Required + labeledPorts: { + "8581:8581": 'http' + }, + volumes: false, + networks: false, + logging: true + } + }; + + retr.getHelp = () => { + return { + serviceName, // Required + links: { + "Website": 'https://homebridge.io/', // Website of service + "Docker": "https://hub.docker.com/r/oznu/homebridge/dockerfile", + "Source Code": "https://github.com/homebridge/homebridge/", + rawMarkdownRemote: '', // Usually links to github raw help pages. + rawMarkdownLocal: '', // Relative path to docs locally + "IOTstack Documentation for {$displayName}": 'https://sensorsiot.github.io/IOTstack/Containers/HomeBridge/' // Usually links to the github page for this service. + } + }; + }; + + retr.getCommands = () => { + return { + commands: {} // Key/value pair of helper commands user can run locally + }; + }; + + retr.getMeta = () => { + return { + serviceName, // Required + displayName: 'Home Bridge', + serviceTypeTags: ['wui', 'iot'], + iconUri: '/logos/homebridge.png' + }; + }; + + return retr; +}; + +module.exports = homebridge; diff --git a/.templates/homebridge/service.yml b/.internal/templates/services/homebridge/template.yml similarity index 91% rename from .templates/homebridge/service.yml rename to .internal/templates/services/homebridge/template.yml index 89bb23d47..5f2ddb4a9 100644 --- a/.templates/homebridge/service.yml +++ b/.internal/templates/services/homebridge/template.yml @@ -10,6 +10,4 @@ homebridge: - HOMEBRIDGE_CONFIG_UI_PORT=%WUIPort% volumes: - ./volumes/homebridge:/homebridge - #ports: - # - "4040:4040" network_mode: host diff --git a/.internal/templates/services/homer/build.js b/.internal/templates/services/homer/build.js new file mode 100644 index 000000000..f021ba53d --- /dev/null +++ b/.internal/templates/services/homer/build.js @@ -0,0 +1,168 @@ +const ServiceBuilder = ({ + settings, + version, + logger +}) => { + const retr = {}; + const serviceName = 'homer'; + + const { + setModifiedPorts, + setLoggingState, + setNetworkMode, + setNetworks + } = require('../../../src/utils/commonCompileLogic'); + + const { + checkPortConflicts, + checkNetworkConflicts, + checkDependencyServices + } = require('../../../src/utils/commonBuildChecks'); + + /* + Order: + 1. compile() - merges build options into the final JSON output. + 2. issues() - runs checks on the compile()'ed JSON, and can also test for errors. + 3. assume() - sets required default values if they are not specified in compile(). Once defaults are set, it reruns compile(). This function is optional + 4. build() - sets up scripts and files. + */ + + retr.init = () => { + logger.debug(`ServiceBuilder:init() - '${serviceName}'`); + }; + + const createVolumesDirectory = () => { + return ` +mkdir -p ./volumes/homer/assets +`; + }; + + const checkVolumesDirectory = () => { + return ` +if [[ ! -d ./volumes/homer/assets ]]; then + echo "Homer assets directory is missing!" + sleep 2 +fi +`; + }; + + retr.compile = ({ + outputTemplateJson, + buildOptions, + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:compile() - '${serviceName}' started`); + const compileResults = { + modifiedPorts: setModifiedPorts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedLogging: setLoggingState({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworkMode: setNetworkMode({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworks: setNetworks({ buildTemplate: outputTemplateJson, buildOptions, serviceName }) + }; + console.info(`ServiceBuilder:compile() - '${serviceName}' Results:`, compileResults); + + console.info(`ServiceBuilder:compile() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + return reject({ + component: `ServiceBuilder::compile() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.issues = ({ + outputTemplateJson, + buildOptions, + tmpPath + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:issues() - '${serviceName}' started`); + let issues = []; + + const portConflicts = checkPortConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...portConflicts]; + + const serviceDependencies = checkDependencyServices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...serviceDependencies]; + + const networkConflicts = checkNetworkConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + if (networkConflicts) { + issues.push(networkConflicts); + } + + console.info(`ServiceBuilder:issues() - '${serviceName}' Issues found: ${issues.length}`); + console.info(`ServiceBuilder:issues() - '${serviceName}' completed`); + return resolve(issues); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::issues() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.build = ({ + outputTemplateJson, + buildOptions, + tmpPath, + zipList, + prebuildScripts, + postbuildScripts + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:build() - '${serviceName}' started`); + + prebuildScripts.push({ + serviceName, + comment: 'Create required service directory exists for first launch', + multilineComment: null, + code: createVolumesDirectory() + }); + + postbuildScripts.push({ + serviceName, + comment: 'Ensure required service directory exists for launch', + multilineComment: null, + code: checkVolumesDirectory() + }); + + console.info(`ServiceBuilder:build() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::build() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + return retr; +} + +module.exports = ServiceBuilder; diff --git a/.internal/templates/services/homer/config.js b/.internal/templates/services/homer/config.js new file mode 100644 index 000000000..244ada7c3 --- /dev/null +++ b/.internal/templates/services/homer/config.js @@ -0,0 +1,50 @@ +const homer = () => { + const retr = {}; + + const serviceName = 'homer'; + + retr.getConfigOptions = () => { + return { + serviceName, // Required + labeledPorts: { + "8881:8080": 'http' + }, + volumes: true, + networks: false, + logging: true + } + }; + + retr.getHelp = () => { + return { + serviceName, // Required + links: { + "Docker": 'https://hub.docker.com/r/b4bz/homer', // Website of service + "Source Code": 'https://github.com/bastienwirtz/homer', // Website of service + "Community Chat": 'https://gitter.im/homer-dashboard/community', // Discord, gitter etc + rawMarkdownRemote: '', // Usually links to github raw help pages. + rawMarkdownLocal: '', // Relative path to docs locally + "IOTstack Documentation for {$displayName}": 'https://sensorsiot.github.io/IOTstack/Containers/Homer/' // Usually links to the github page for this service. + } + }; + }; + + retr.getCommands = () => { + return { + commands: {} // Key/value pair of helper commands user can run locally + }; + }; + + retr.getMeta = () => { + return { + serviceName, // Required + displayName: 'Homer', + serviceTypeTags: ['wui', 'dashboard'], + iconUri: '/logos/homer.png' + }; + }; + + return retr; +}; + +module.exports = homer; diff --git a/.templates/homer/service.yml b/.internal/templates/services/homer/template.yml similarity index 100% rename from .templates/homer/service.yml rename to .internal/templates/services/homer/template.yml diff --git a/.internal/templates/services/influxdb/build.js b/.internal/templates/services/influxdb/build.js new file mode 100644 index 000000000..d8ba52997 --- /dev/null +++ b/.internal/templates/services/influxdb/build.js @@ -0,0 +1,192 @@ +const ServiceBuilder = ({ + settings, + version, + logger +}) => { + const retr = {}; + const serviceName = 'influxdb'; + + const { + setImageTag, + setModifiedPorts, + setLoggingState, + setNetworkMode, + setNetworks, + setDevices, + setEnvironmentVariables + } = require('../../../src/utils/commonCompileLogic'); + + const { + checkPortConflicts, + checkNetworkConflicts, + checkDependencyServices + } = require('../../../src/utils/commonBuildChecks'); + + /* + Order: + 1. compile() - merges build options into the final JSON output. + 2. issues() - runs checks on the compile()'ed JSON, and can also test for errors. + 3. assume() - sets required default values if they are not specified in compile(). Once defaults are set, it reruns compile(). This function is optional + 4. build() - sets up scripts and files. + */ + + retr.init = () => { + logger.debug(`ServiceBuilder:init() - '${serviceName}'`); + }; + + retr.compile = ({ + outputTemplateJson, + buildOptions, + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:compile() - '${serviceName}' started`); + const compileResults = { + modifiedImage: setImageTag({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedPorts: setModifiedPorts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedLogging: setLoggingState({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworkMode: setNetworkMode({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworks: setNetworks({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedEnvironment: setEnvironmentVariables({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedDevices: setDevices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }) + }; + console.info(`ServiceBuilder:compile() - '${serviceName}' Results:`, compileResults); + + console.info(`ServiceBuilder:compile() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + return reject({ + component: `ServiceBuilder::compile() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.issues = ({ + outputTemplateJson, + buildOptions, + tmpPath + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:issues() - '${serviceName}' started`); + let issues = []; + + const portConflicts = checkPortConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...portConflicts]; + + const serviceDependencies = checkDependencyServices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...serviceDependencies]; + + const networkConflicts = checkNetworkConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + if (networkConflicts) { + issues.push(networkConflicts); + } + + console.info(`ServiceBuilder:issues() - '${serviceName}' Issues found: ${issues.length}`); + console.info(`ServiceBuilder:issues() - '${serviceName}' completed`); + return resolve(issues); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::issues() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.assume = ({ + outputTemplateJson, + buildOptions, + tmpPath, + zipList, + prebuildScripts, + postbuildScripts + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:assume() - '${serviceName}' started`); + let assumptionsMade = 0; + const { getConfigOptions } = require('./config')({}); + const servicesConfig = buildOptions?.configurations?.services; + if (servicesConfig[serviceName] === undefined) { + servicesConfig[serviceName] = {}; + } + if (servicesConfig?.[serviceName]?.tag === undefined) { + assumptionsMade++; + servicesConfig[serviceName].tag = getConfigOptions().imageTags[0]; + } + + if (assumptionsMade > 0) { + retr.compile({ + outputTemplateJson, + buildOptions + }); + } + + console.info(`ServiceBuilder:issues() - '${serviceName}' Configuration Assumptions: ${assumptionsMade}`); + console.info(`ServiceBuilder:assume() - '${serviceName}' completed`); + return resolve({ assumptionsMade }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + return reject({ + component: `ServiceBuilder::assume() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.build = ({ + outputTemplateJson, + buildOptions, + tmpPath, + zipList, + prebuildScripts, + postbuildScripts + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:build() - '${serviceName}' started`); + // Code here + console.info(`ServiceBuilder:build() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::build() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + return retr; +} + +module.exports = ServiceBuilder; diff --git a/.internal/templates/services/influxdb/config.js b/.internal/templates/services/influxdb/config.js new file mode 100644 index 000000000..cb4c535c0 --- /dev/null +++ b/.internal/templates/services/influxdb/config.js @@ -0,0 +1,56 @@ +const influxDb = () => { + const retr = {}; + + const serviceName = 'influxdb'; + + retr.getConfigOptions = () => { + return { + serviceName, // Required + labeledPorts: { + "8086:8086": 'http' + }, + modifyableEnvironment: [ + { + key: 'INFLUXDB_UDP_BIND_ADDRESS', + value: '0.0.0.0:8086' + } + ], + volumes: true, + imageTags: ['1.8', '1.8.4', 'latest'], + networks: true, + logging: true + } + }; + + retr.getHelp = () => { + return { + serviceName, // Required + links: { + "Website": 'https://www.influxdata.com/', // Website of service + "Docker": 'https://hub.docker.com/_/influxdb', // Website of service + rawMarkdownRemote: '', // Usually links to github raw help pages. + rawMarkdownLocal: '', // Relative path to docs locally + "IOTstack Documentation for {$displayName}": 'https://sensorsiot.github.io/IOTstack/Containers/InfluxDB/' // Usually links to the github page for this service. + } + }; + }; + + retr.getCommands = () => { + return { + commands: {} // Key/value pair of helper commands user can run locally + }; + }; + + retr.getMeta = () => { + return { + serviceName, // Required + displayName: 'InfluxDB', + serviceTypeTags: ['database', 'timeseries', 'sql'], + iconUri: '/logos/influxdb.svg' + }; + }; + + return retr; +}; + +module.exports = influxDb; diff --git a/.templates/influxdb/service.yml b/.internal/templates/services/influxdb/template.yml similarity index 69% rename from .templates/influxdb/service.yml rename to .internal/templates/services/influxdb/template.yml index 29bb22bdb..59c68992a 100644 --- a/.templates/influxdb/service.yml +++ b/.internal/templates/services/influxdb/template.yml @@ -1,6 +1,6 @@ influxdb: container_name: influxdb - image: "influxdb:1.8.4" + image: "influxdb:{$tag}" restart: unless-stopped ports: - "8086:8086" @@ -18,4 +18,13 @@ influxdb: - ./backups/influxdb/db:/var/lib/influxdb/backup networks: - iotstack_nw - + logging: + options: + max-size: "5m" + max-file: "3" + healthcheck: + test: ["CMD", "curl", "http://localhost:8086"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s diff --git a/.internal/templates/services/influxdb2/template.yml b/.internal/templates/services/influxdb2/template.yml new file mode 100644 index 000000000..a922be47b --- /dev/null +++ b/.internal/templates/services/influxdb2/template.yml @@ -0,0 +1,26 @@ +influxdb2: + container_name: influxdb2 + image: "influxdb:latest" + restart: unless-stopped + environment: + - TZ=Etc/UTC + - DOCKER_INFLUXDB_INIT_USERNAME=me + - DOCKER_INFLUXDB_INIT_PASSWORD=mypassword + - DOCKER_INFLUXDB_INIT_ORG=myorg + - DOCKER_INFLUXDB_INIT_BUCKET=mybucket + - DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=my-super-secret-auth-token + - DOCKER_INFLUXDB_INIT_MODE=setup + # - DOCKER_INFLUXDB_INIT_MODE=upgrade + ports: + - "8087:8086" + volumes: + - ./volumes/influxdb2/data:/var/lib/influxdb2 + - ./volumes/influxdb2/config:/etc/influxdb2 + - ./volumes/influxdb2/backup:/var/lib/backup + # - ./volumes/influxdb.migrate/data:/var/lib/influxdb:ro + healthcheck: + test: ["CMD", "influx", "ping"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s diff --git a/.internal/templates/services/kapacitor/template.yml b/.internal/templates/services/kapacitor/template.yml new file mode 100644 index 000000000..59635103f --- /dev/null +++ b/.internal/templates/services/kapacitor/template.yml @@ -0,0 +1,21 @@ +kapacitor: + container_name: kapacitor + image: kapacitor:1.5 + restart: unless-stopped + environment: + - TZ=Etc/UTC + # see https://docs.influxdata.com/kapacitor/v1.6/administration/configuration/#kapacitor-environment-variables + - KAPACITOR_INFLUXDB_0_URLS_0=http://influxdb:8086 + # - KAPACITOR_INFLUXDB_USERNAME= + # - KAPACITOR_INFLUXDB_PASSWORD= + # - KAPACITOR_HOSTNAME=kapacitor + # - KAPACITOR_LOGGING_LEVEL=INFO + # - KAPACITOR_REPORTING_ENABLED=false + ports: + - "9092:9092" + volumes: + - ./volumes/kapacitor:/var/lib/kapacitor + depends_on: + - influxdb + networks: + - iotstack_nw diff --git a/.internal/templates/services/mariadb/build.js b/.internal/templates/services/mariadb/build.js new file mode 100644 index 000000000..c4660e253 --- /dev/null +++ b/.internal/templates/services/mariadb/build.js @@ -0,0 +1,211 @@ +const ServiceBuilder = ({ + settings, + version, + logger +}) => { + const retr = {}; + const serviceName = 'mariadb'; + + const { + setModifiedPorts, + setLoggingState, + setNetworkMode, + setNetworks, + setDevices, + setEnvironmentVariables + } = require('../../../src/utils/commonCompileLogic'); + + const { + checkPortConflicts, + checkNetworkConflicts, + checkDependencyServices + } = require('../../../src/utils/commonBuildChecks'); + + /* + Order: + 1. compile() - merges build options into the final JSON output. + 2. issues() - runs checks on the compile()'ed JSON, and can also test for errors. + 3. assume() - sets required default values if they are not specified in compile(). Once defaults are set, it reruns compile(). This function is optional + 4. build() - sets up scripts and files. + */ + + retr.init = () => { + logger.debug(`ServiceBuilder:init() - '${serviceName}'`); + }; + + const createVolumesDirectory = () => { + return ` +mkdir -p ./volumes/mariadb/config +`; + }; + + const checkVolumesDirectory = () => { + return ` +if [[ ! -d ./volumes/mariadb/config ]]; then + echo "MariaDB config directory is missing!" + sleep 2 +fi +`; + }; + + retr.compile = ({ + outputTemplateJson, + buildOptions, + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:compile() - '${serviceName}' started`); + + const compileResults = { + modifiedPorts: setModifiedPorts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedLogging: setLoggingState({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworkMode: setNetworkMode({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworks: setNetworks({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedEnvironment: setEnvironmentVariables({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedDevices: setDevices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }) + }; + console.info(`ServiceBuilder:compile() - '${serviceName}' Results:`, compileResults); + + console.info(`ServiceBuilder:compile() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + return reject({ + component: `ServiceBuilder::compile() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.issues = ({ + outputTemplateJson, + buildOptions, + tmpPath + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:issues() - '${serviceName}' started`); + let issues = []; + + const portConflicts = checkPortConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...portConflicts]; + + const serviceDependencies = checkDependencyServices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...serviceDependencies]; + + const networkConflicts = checkNetworkConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + if (networkConflicts) { + issues.push(networkConflicts); + } + + const environmentList = outputTemplateJson?.services?.[serviceName]?.environment ?? []; + if (Array.isArray(environmentList) && environmentList.length > 0) { + let pwKeysFound = false; + environmentList.forEach((envKVP) => { + const envKey = envKVP.split('=')[0]; + const envValue = envKVP.split('=')[1]; + if (envKey && envValue) { + if (envKey === 'MYSQL_ROOT_PASSWORD' || envKey === 'MYSQL_PASSWORD') { + pwKeysFound = true; + if (envValue === 'Unset' || envValue === 'Unset') { + issues.push({ + type: 'service', + name: serviceName, + issueType: 'environment', + message: `Ensure database passwords are set in environment variables.` + }); + } + } + } + }); + + if (!pwKeysFound) { + issues.push({ + type: 'service', + name: serviceName, + issueType: 'environment', + message: `No environment variables found. Database may not start unless they are set.` + }); + } + } else { + issues.push({ + type: 'service', + name: serviceName, + issueType: 'environment', + message: `No environment variables found. Database may not start unless they are set.` + }); + } + + console.info(`ServiceBuilder:issues() - '${serviceName}' Issues found: ${issues.length}`); + console.info(`ServiceBuilder:issues() - '${serviceName}' completed`); + return resolve(issues); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::issues() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.build = ({ + outputTemplateJson, + buildOptions, + tmpPath, + zipList, + prebuildScripts, + postbuildScripts + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:build() - '${serviceName}' started`); + + prebuildScripts.push({ + serviceName, + comment: 'Create required service directory exists for first launch', + multilineComment: null, + code: createVolumesDirectory() + }); + + postbuildScripts.push({ + serviceName, + comment: 'Ensure required service directory exists for launch', + multilineComment: null, + code: checkVolumesDirectory() + }); + + console.info(`ServiceBuilder:build() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::build() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + return retr; +} + +module.exports = ServiceBuilder; diff --git a/.internal/templates/services/mariadb/buildFiles/Dockerfile b/.internal/templates/services/mariadb/buildFiles/Dockerfile new file mode 100644 index 000000000..36c4fa7c1 --- /dev/null +++ b/.internal/templates/services/mariadb/buildFiles/Dockerfile @@ -0,0 +1,31 @@ +# Download base image +FROM ghcr.io/linuxserver/mariadb + +# candidates for customisation are +ENV CANDIDATES="/defaults/my.cnf /defaults/custom.cnf" + +# apply stability patches recommended in +# +# https://discord.com/channels/638610460567928832/638610461109256194/825049573520965703 +# https://stackoverflow.com/questions/61809270/how-to-discover-why-mariadb-crashes +RUN for CNF in ${CANDIDATES} ; do [ -f ${CNF} ] && break ; done ; \ + sed -i.bak \ + -e "s/^thread_cache_size/# thread_cache_size/" \ + -e "s/^read_buffer_size/# read_buffer_size/" \ + ${CNF} + +# copy the health-check script into place +ENV HEALTHCHECK_SCRIPT "iotstack_healthcheck.sh" +COPY ${HEALTHCHECK_SCRIPT} /usr/local/bin/${HEALTHCHECK_SCRIPT} + +# define the health check +HEALTHCHECK \ + --start-period=30s \ + --interval=30s \ + --timeout=10s \ + --retries=3 \ + CMD ${HEALTHCHECK_SCRIPT} || exit 1 + +ENV CANDIDATES= + +# EOF diff --git a/.internal/templates/services/mariadb/buildFiles/iotstack_healthcheck.sh b/.internal/templates/services/mariadb/buildFiles/iotstack_healthcheck.sh new file mode 100755 index 000000000..980d31a78 --- /dev/null +++ b/.internal/templates/services/mariadb/buildFiles/iotstack_healthcheck.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env sh + +# set a default for the port +# (refer https://mariadb.com/kb/en/mariadb-environment-variables/ ) +HEALTHCHECK_PORT="${MYSQL_TCP_PORT:-3306}" + +# the expected response is? +EXPECTED="mysqld is alive" + +# run the check +if [ -z "$MYSQL_ROOT_PASSWORD" ] ; then + RESPONSE=$(mysqladmin ping -h localhost) +else + # note - there is NO space between "-p" and the password. This is + # intentional. It is part of the mysql and mysqladmin API. + RESPONSE=$(mysqladmin -p${MYSQL_ROOT_PASSWORD} ping -h localhost) +fi + +# did the mysqladmin command succeed? +if [ $? -eq 0 ] ; then + + # yes! is the response as expected? + if [ "$RESPONSE" = "$EXPECTED" ] ; then + + # yes! this could still be a false positive so probe the port + if nc -w 1 localhost $HEALTHCHECK_PORT >/dev/null 2>&1; then + + # port responding - that defines healthy + exit 0 + + fi + + fi + +fi + +# otherwise the check fails +exit 1 diff --git a/.internal/templates/services/mariadb/config.js b/.internal/templates/services/mariadb/config.js new file mode 100644 index 000000000..77679bb52 --- /dev/null +++ b/.internal/templates/services/mariadb/config.js @@ -0,0 +1,69 @@ +const mariadb = () => { + const retr = {}; + + const serviceName = 'mariadb'; + + retr.getConfigOptions = () => { + return { + serviceName, // Required + modifyableEnvironment: [ + { + key: 'TZ', + value: 'Etc/UTC' + }, + { + key: 'MYSQL_ROOT_PASSWORD', + value: '{$randomPassword}' + }, + { + key: 'MYSQL_DATABASE', + value: 'default' + }, + { + key: 'MYSQL_USER', + value: 'mariadbuser' + }, + { + key: 'MYSQL_PASSWORD', + value: '{$randomPassword}' + } + ], + volumes: true, + networks: true, + logging: true + } + }; + + retr.getHelp = () => { + return { + serviceName, // Required + links: { + "Website": 'https://mariadb.org/', // Website of service + "Docker": 'https://hub.docker.com/r/linuxserver/mariadb/', + "{$displayName} Documentation": 'https://mariadb.org/documentation/', + rawMarkdownRemote: '', // Usually links to github raw help pages. + rawMarkdownLocal: '', // Relative path to docs locally + "IOTstack Documentation for {$displayName}": 'https://sensorsiot.github.io/IOTstack/Containers/MariaDB/' // Usually links to the github page for this service. + } + }; + }; + + retr.getCommands = () => { + return { + commands: {} // Key/value pair of helper commands user can run locally + }; + }; + + retr.getMeta = () => { + return { + serviceName, // Required + displayName: 'MariaDB', + serviceTypeTags: ['database', 'sql'], + iconUri: '/logos/mariadb.png' + }; + }; + + return retr; +}; + +module.exports = mariadb; diff --git a/.templates/mariadb/service.yml b/.internal/templates/services/mariadb/template.yml similarity index 54% rename from .templates/mariadb/service.yml rename to .internal/templates/services/mariadb/template.yml index 4f44dc588..0e7caf3dd 100644 --- a/.templates/mariadb/service.yml +++ b/.internal/templates/services/mariadb/template.yml @@ -1,18 +1,24 @@ mariadb: - image: linuxserver/mariadb + build: ./.templates/mariadb/. container_name: mariadb environment: - TZ=Etc/UTC - PUID=1000 - PGID=1000 - - MYSQL_ROOT_PASSWORD=%randomAdminPassword% + - MYSQL_ROOT_PASSWORD=Unset - MYSQL_DATABASE=default - - MYSQL_USER=mariadbuser - - MYSQL_PASSWORD=%randomPassword% + - MYSQL_USER=Unset + - MYSQL_PASSWORD=Unset volumes: - ./volumes/mariadb/config:/config + - ./volumes/mariadb/db_backup:/backup ports: - "3306:3306" restart: unless-stopped networks: - iotstack_nw + logging: + options: + max-size: "5m" + max-file: "3" + diff --git a/.internal/templates/services/mosquitto/build.js b/.internal/templates/services/mosquitto/build.js new file mode 100644 index 000000000..91e02a205 --- /dev/null +++ b/.internal/templates/services/mosquitto/build.js @@ -0,0 +1,252 @@ +const ServiceBuilder = ({ + settings, + version, + logger +}) => { + const path = require('path'); + const retr = {}; + const serviceName = 'mosquitto'; + + const { + setImageTag, + setModifiedPorts, + setLoggingState, + setNetworkMode, + setNetworks, + setVolumes, + setEnvironmentVariables, + setDevices + } = require('../../../src/utils/commonCompileLogic'); + + const { + checkPortConflicts, + checkNetworkConflicts, + checkDependencyServices + } = require('../../../src/utils/commonBuildChecks'); + + /* + Order: + 1. compile() - merges build options into the final JSON output. + 2. issues() - runs checks on the compile()'ed JSON, and can also test for errors. + 3. assume() - sets required default values if they are not specified in compile(). Once defaults are set, it reruns compile(). This function is optional + 4. build() - sets up scripts and files. + */ + + retr.init = () => { + logger.debug(`ServiceBuilder:init() - '${serviceName}'`); + }; + + const checkServiceFilesCopied = () => { + return ` +if [[ ! -f ./services/mosquitto/config/mosquitto.conf ]]; then + echo "Mosquitto config file is missing!" + sleep 2 +fi +`; + }; + + const createVolumesDirectory = () => { + return ` +mkdir -p ./volumes/mosquitto/data +mkdir -p ./volumes/mosquitto/config +mkdir -p ./volumes/mosquitto/pwfile +mkdir -p ./volumes/mosquitto/log +`; + }; + + const checkVolumesDirectory = () => { + return ` +HAS_ERROR="false" +if [[ ! -d ./volumes/mosquitto/data ]]; then + echo "Mosquitto data directory is missing!" + HAS_ERROR="true" +fi + +if [[ ! -d ./volumes/mosquitto/config ]]; then + echo "Mosquitto config directory is missing!" + HAS_ERROR="true" +fi + +if [[ ! -d ./volumes/mosquitto/log ]]; then + echo "Mosquitto log directory is missing!" + HAS_ERROR="true" +fi + +if [[ ! -d ./volumes/mosquitto/pwfile ]]; then + echo "Mosquitto pwfile directory is missing!" + HAS_ERROR="true" +fi + +if [[ "$HAS_ERROR" == "true" ]]; then + echo "Errors were detected when setting up Mosquitto" + sleep 1 +fi +`; + }; + + retr.compile = ({ + outputTemplateJson, + buildOptions, + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:compile() - '${serviceName}' started`); + + const compileResults = { + modifiedImage: setImageTag({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedPorts: setModifiedPorts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedLogging: setLoggingState({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworkMode: setNetworkMode({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworks: setNetworks({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedVolumes: setVolumes({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedEnvironment: setEnvironmentVariables({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedDevices: setDevices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }) + }; + console.info(`ServiceBuilder:compile() - '${serviceName}' Results:`, compileResults); + + console.info(`ServiceBuilder:compile() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + return reject({ + component: `ServiceBuilder::compile() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.issues = ({ + outputTemplateJson, + buildOptions, + tmpPath + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:issues() - '${serviceName}' started`); + let issues = []; + + const portConflicts = checkPortConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...portConflicts]; + + const serviceDependencies = checkDependencyServices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...serviceDependencies]; + + const networkConflicts = checkNetworkConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + if (networkConflicts) { + issues.push(networkConflicts); + } + + console.info(`ServiceBuilder:issues() - '${serviceName}' Issues found: ${issues.length}`); + console.info(`ServiceBuilder:issues() - '${serviceName}' completed`); + return resolve(issues); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::issues() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.build = ({ + outputTemplateJson, + buildOptions, + tmpPath, + zipList, + prebuildScripts, + postbuildScripts + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:build() - '${serviceName}' started`); + const mosquittoConfFilePath = path.join(__dirname, settings.paths.serviceFiles, 'config', 'mosquitto.conf'); + zipList.push({ + fullPath: mosquittoConfFilePath, + zipName: '/services/mosquitto/config/mosquitto.conf' + }); + console.debug(`ServiceBuilder:build() - '${serviceName}' Added '${mosquittoConfFilePath}' to zip`); + + const mosquittoAclFilePath = path.join(__dirname, settings.paths.serviceFiles, 'config', 'filter.acl'); + zipList.push({ + fullPath: mosquittoAclFilePath, + zipName: '/services/mosquitto/config/filter.acl' + }); + console.debug(`ServiceBuilder:build() - '${serviceName}' Added '${mosquittoAclFilePath}' to zip`); + + const mosquittoPwFilePath = path.join(__dirname, settings.paths.serviceFiles, 'pwfile', 'pwfile'); + zipList.push({ + fullPath: mosquittoPwFilePath, + zipName: '/services/mosquitto/pwfile/pwfile' + }); + console.debug(`ServiceBuilder:build() - '${serviceName}' Added '${mosquittoPwFilePath}' to zip`); + + const mosquittoDockerFilePath = path.join(__dirname, settings.paths.buildFiles, 'Dockerfile'); + zipList.push({ + fullPath: mosquittoDockerFilePath, + zipName: '/services/mosquitto/Dockerfile' + }); + console.debug(`ServiceBuilder:build() - '${serviceName}' Added '${mosquittoDockerFilePath}' to zip`); + + const mosquittoDockerEntryPointFilePath = path.join(__dirname, settings.paths.buildFiles, 'docker-entrypoint.sh'); + zipList.push({ + fullPath: mosquittoDockerEntryPointFilePath, + zipName: '/services/mosquitto/docker-entrypoint.sh' + }); + console.debug(`ServiceBuilder:build() - '${serviceName}' Added '${mosquittoDockerEntryPointFilePath}' to zip`); + + postbuildScripts.push({ + serviceName, + comment: 'Ensure required service files exist for launch', + multilineComment: null, + code: checkServiceFilesCopied() + }); + + prebuildScripts.push({ + serviceName, + comment: 'Create required service directory exists for first launch', + multilineComment: null, + code: createVolumesDirectory() + }); + + postbuildScripts.push({ + serviceName, + comment: 'Ensure required service directory exists for launch', + multilineComment: null, + code: checkVolumesDirectory() + }); + + console.info(`ServiceBuilder:build() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::build() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + return retr; +} + +module.exports = ServiceBuilder; diff --git a/.templates/mosquitto/Dockerfile b/.internal/templates/services/mosquitto/buildFiles/Dockerfile similarity index 59% rename from .templates/mosquitto/Dockerfile rename to .internal/templates/services/mosquitto/buildFiles/Dockerfile index 8eb6c900a..7285e065a 100644 --- a/.templates/mosquitto/Dockerfile +++ b/.internal/templates/services/mosquitto/buildFiles/Dockerfile @@ -1,6 +1,9 @@ # Download base image FROM eclipse-mosquitto:latest +# see https://github.com/alpinelinux/docker-alpine/issues/98 +RUN sed -i 's/https/http/' /etc/apk/repositories + # Add support tools RUN apk update && apk add --no-cache rsync tzdata @@ -10,6 +13,18 @@ ENV IOTSTACK_DEFAULTS_DIR="iotstack_defaults" # copy template files to image COPY --chown=mosquitto:mosquitto ${IOTSTACK_DEFAULTS_DIR} /${IOTSTACK_DEFAULTS_DIR} +# copy the health-check script into place +ENV HEALTHCHECK_SCRIPT "iotstack_healthcheck.sh" +COPY ${HEALTHCHECK_SCRIPT} /usr/local/bin/${HEALTHCHECK_SCRIPT} + +# define the health check +HEALTHCHECK \ + --start-period=30s \ + --interval=30s \ + --timeout=10s \ + --retries=3 \ + CMD ${HEALTHCHECK_SCRIPT} || exit 1 + # replace the docker entry-point script ENV IOTSTACK_ENTRY_POINT="docker-entrypoint.sh" COPY ${IOTSTACK_ENTRY_POINT} /${IOTSTACK_ENTRY_POINT} diff --git a/.internal/templates/services/mosquitto/buildFiles/docker-entrypoint.sh b/.internal/templates/services/mosquitto/buildFiles/docker-entrypoint.sh new file mode 100644 index 000000000..cd14b0eea --- /dev/null +++ b/.internal/templates/services/mosquitto/buildFiles/docker-entrypoint.sh @@ -0,0 +1,19 @@ +#!/bin/ash +set -e + +# Set permissions +user="$(id -u)" +if [ "$user" = '0' -a -d "/mosquitto" ]; then + + echo "[IOTstack] begin self-repair" + + rsync -arpv --ignore-existing /${IOTSTACK_DEFAULTS_DIR}/ "/mosquitto" + + chown -Rc mosquitto:mosquitto /mosquitto + + echo "[IOTstack] end self-repair" + +fi + +exec "$@" + diff --git a/.templates/mosquitto/iotstack_defaults/config/filter.acl b/.internal/templates/services/mosquitto/buildFiles/iotstack_defaults/config/filter.acl similarity index 100% rename from .templates/mosquitto/iotstack_defaults/config/filter.acl rename to .internal/templates/services/mosquitto/buildFiles/iotstack_defaults/config/filter.acl diff --git a/.templates/mosquitto/iotstack_defaults/config/mosquitto.conf b/.internal/templates/services/mosquitto/buildFiles/iotstack_defaults/config/mosquitto.conf similarity index 100% rename from .templates/mosquitto/iotstack_defaults/config/mosquitto.conf rename to .internal/templates/services/mosquitto/buildFiles/iotstack_defaults/config/mosquitto.conf diff --git a/.templates/python/app/requirements.txt b/.internal/templates/services/mosquitto/buildFiles/iotstack_defaults/pwfile/pwfile old mode 100755 new mode 100644 similarity index 100% rename from .templates/python/app/requirements.txt rename to .internal/templates/services/mosquitto/buildFiles/iotstack_defaults/pwfile/pwfile diff --git a/.internal/templates/services/mosquitto/buildFiles/iotstack_healthcheck.sh b/.internal/templates/services/mosquitto/buildFiles/iotstack_healthcheck.sh new file mode 100755 index 000000000..8f4d51308 --- /dev/null +++ b/.internal/templates/services/mosquitto/buildFiles/iotstack_healthcheck.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env sh + +# assume the following environment variables, all of which may be null +# HEALTHCHECK_PORT +# HEALTHCHECK_USER +# HEALTHCHECK_PASSWORD +# HEALTHCHECK_TOPIC + +# set a default for the port +HEALTHCHECK_PORT="${HEALTHCHECK_PORT:-1883}" + +# strip any quotes from username and password +HEALTHCHECK_USER="$(eval echo $HEALTHCHECK_USER)" +HEALTHCHECK_PASSWORD="$(eval echo $HEALTHCHECK_PASSWORD)" + +# set a default for the topic +HEALTHCHECK_TOPIC="${HEALTHCHECK_TOPIC:-iotstack/mosquitto/healthcheck}" +HEALTHCHECK_TOPIC="$(eval echo $HEALTHCHECK_TOPIC)" + +# record the current date and time for the test payload +PUBLISH=$(date) + +# publish a retained message containing the timestamp +mosquitto_pub \ + -h localhost \ + -p "$HEALTHCHECK_PORT" \ + -t "$HEALTHCHECK_TOPIC" \ + -m "$PUBLISH" \ + -u "$HEALTHCHECK_USER" \ + -P "$HEALTHCHECK_PASSWORD" \ + -r + +# did that succeed? +if [ $? -eq 0 ] ; then + + # yes! now, subscribe to that same topic with a 2-second timeout + # plus returning on the first message + SUBSCRIBE=$(mosquitto_sub \ + -h localhost \ + -p "$HEALTHCHECK_PORT" \ + -t "$HEALTHCHECK_TOPIC" \ + -u "$HEALTHCHECK_USER" \ + -P "$HEALTHCHECK_PASSWORD" \ + -W 2 \ + -C 1 \ + ) + + # did the subscribe succeed? + if [ $? -eq 0 ] ; then + + # yes! do the publish and subscribe payloads compare equal? + if [ "$PUBLISH" = "$SUBSCRIBE" ] ; then + + # yes! return success + exit 0 + + fi + + fi + +fi + +# otherwise, return failure +exit 1 diff --git a/.internal/templates/services/mosquitto/config.js b/.internal/templates/services/mosquitto/config.js new file mode 100644 index 000000000..d80d2286d --- /dev/null +++ b/.internal/templates/services/mosquitto/config.js @@ -0,0 +1,50 @@ +const mosquitto = () => { + const retr = {}; + + const serviceName = 'mosquitto'; + + retr.getConfigOptions = () => { + return { + serviceName, // Required + labeledPorts: { + "1883:1883": 'mosquitto' + }, + volumes: true, + networks: true, + logging: true + }; + }; + + retr.getHelp = () => { + return { + serviceName, // Required + links: { + "Website": 'https://mosquitto.org/', // Website of service + "Docker": 'https://hub.docker.com/_/eclipse-mosquitto', + "{$displayName} Documentation": 'https://mosquitto.org/documentation/', + rawMarkdownRemote: '', // Usually links to github raw help pages. + rawMarkdownLocal: '', // Relative path to docs locally + "IOTstack Documentation for {$displayName}": 'https://sensorsiot.github.io/IOTstack/Containers/Mosquitto/' // Usually links to the github page for this service. + } + }; + }; + + retr.getCommands = () => { + return { + commands: {} // Key/value pair of helper commands user can run locally + }; + }; + + retr.getMeta = () => { + return { + serviceName, // Required + displayName: 'Mosquitto', + serviceTypeTags: ['mqtt', 'server'], + iconUri: '/logos/mosquitto.png' + }; + }; + + return retr; +}; + +module.exports = mosquitto; diff --git a/.internal/templates/services/mosquitto/serviceFiles/config/filter.acl b/.internal/templates/services/mosquitto/serviceFiles/config/filter.acl new file mode 100644 index 000000000..e16823110 --- /dev/null +++ b/.internal/templates/services/mosquitto/serviceFiles/config/filter.acl @@ -0,0 +1,6 @@ +user admin +topic read # +topic write # + +pattern read # +pattern write # diff --git a/.internal/templates/services/mosquitto/serviceFiles/config/mosquitto.conf b/.internal/templates/services/mosquitto/serviceFiles/config/mosquitto.conf new file mode 100644 index 000000000..e2c092f85 --- /dev/null +++ b/.internal/templates/services/mosquitto/serviceFiles/config/mosquitto.conf @@ -0,0 +1,33 @@ +# required by https://mosquitto.org/documentation/migrating-to-2-0/ +# +listener 1883 + +# persistence enabled for remembering retain flag across restarts +# +persistence true +persistence_location /mosquitto/data + +# logging options: +# enable one of the following (stdout = less wear on SD cards but +# logs do not persist across restarts) +#log_dest file /mosquitto/log/mosquitto.log +log_dest stdout +log_timestamp_format %Y-%m-%dT%H:%M:%S + +# password handling: +# password_file commented-out allow_anonymous true = +# open access +# password_file commented-out allow_anonymous false = +# no access +# password_file activated allow_anonymous true = +# passwords omitted is permitted but +# passwords provided must match pwfile +# password_file activated allow_anonymous false = +# no access without passwords +# passwords provided must match pwfile +# +#password_file /mosquitto/pwfile/pwfile +allow_anonymous true + +# Uncomment to enable filters +#acl_file /mosquitto/config/filter.acl diff --git a/.templates/wireguard/wg0.conf b/.internal/templates/services/mosquitto/serviceFiles/pwfile/pwfile similarity index 100% rename from .templates/wireguard/wg0.conf rename to .internal/templates/services/mosquitto/serviceFiles/pwfile/pwfile diff --git a/.templates/mosquitto/service.yml b/.internal/templates/services/mosquitto/template.yml similarity index 78% rename from .templates/mosquitto/service.yml rename to .internal/templates/services/mosquitto/template.yml index 1def87407..beb28b71c 100644 --- a/.templates/mosquitto/service.yml +++ b/.internal/templates/services/mosquitto/template.yml @@ -1,6 +1,6 @@ mosquitto: container_name: mosquitto - build: ./.templates/mosquitto/. + build: ./services/mosquitto/. restart: unless-stopped environment: - TZ=Etc/UTC @@ -13,3 +13,7 @@ mosquitto: - ./volumes/mosquitto/pwfile:/mosquitto/pwfile networks: - iotstack_nw + logging: + options: + max-size: "5m" + max-file: "3" diff --git a/.internal/templates/services/motioneye/build.js b/.internal/templates/services/motioneye/build.js new file mode 100644 index 000000000..5f45cb45e --- /dev/null +++ b/.internal/templates/services/motioneye/build.js @@ -0,0 +1,180 @@ +const ServiceBuilder = ({ + settings, + version, + logger +}) => { + const retr = {}; + const serviceName = 'motioneye'; + + const { + setModifiedPorts, + setLoggingState, + setNetworkMode, + setNetworks, + setDevices + } = require('../../../src/utils/commonCompileLogic'); + + const { + checkPortConflicts, + checkNetworkConflicts, + checkDependencyServices + } = require('../../../src/utils/commonBuildChecks'); + + /* + Order: + 1. compile() - merges build options into the final JSON output. + 2. issues() - runs checks on the compile()'ed JSON, and can also test for errors. + 3. assume() - sets required default values if they are not specified in compile(). Once defaults are set, it reruns compile(). This function is optional + 4. build() - sets up scripts and files. + */ + + retr.init = () => { + logger.debug(`ServiceBuilder:init() - '${serviceName}'`); + }; + + const checkVideoDevices = (devicesList) => { + let scriptResult = ""; + devicesList.forEach((device) => { + scriptResult = `${scriptResult} +if [[ ! -f ${device} ]]; then + MOTIONEYE_PAUSE_DEVICES_ERROR="true" + echo "MotionEye Device ${device} doesn't exist. May cause errors on startup." +fi +` + }); + + return ` +MOTIONEYE_PAUSE_DEVICES_ERROR="false" +${scriptResult} +if [[ "$MOTIONEYE_PAUSE_DEVICES_ERROR" == "true" ]]; then + sleep 2 +fi +`; + }; + + retr.compile = ({ + outputTemplateJson, + buildOptions, + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:compile() - '${serviceName}' started`); + + const compileResults = { + modifiedPorts: setModifiedPorts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedLogging: setLoggingState({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworkMode: setNetworkMode({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworks: setNetworks({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedDevices: setDevices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }) + }; + console.info(`ServiceBuilder:compile() - '${serviceName}' Results:`, compileResults); + + console.info(`ServiceBuilder:compile() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + return reject({ + component: `ServiceBuilder::compile() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.issues = ({ + outputTemplateJson, + buildOptions, + tmpPath + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:issues() - '${serviceName}' started`); + let issues = []; + + const portConflicts = checkPortConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...portConflicts]; + + const serviceDependencies = checkDependencyServices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...serviceDependencies]; + + const networkConflicts = checkNetworkConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + if (networkConflicts) { + issues.push(networkConflicts); + } + + if ((outputTemplateJson?.services?.[serviceName]?.devices ?? []).length === 0) { + issues.push({ + type: 'service', + name: serviceName, + issueType: 'missingDevices', + message: `No devices set, cannot record from local hardware. MotionEye will only work as a remote recorder.` + }); + } + + console.info(`ServiceBuilder:issues() - '${serviceName}' Issues found: ${issues.length}`); + console.info(`ServiceBuilder:issues() - '${serviceName}' completed`); + return resolve(issues); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::issues() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.build = ({ + outputTemplateJson, + buildOptions, + tmpPath, + zipList, + prebuildScripts, + postbuildScripts + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:build() - '${serviceName}' started`); + + if ((outputTemplateJson?.services?.[serviceName]?.devices ?? []).length > 0) { + postbuildScripts.push({ + serviceName, + comment: 'Ensure MotionEye can see devices', + multilineComment: null, + code: checkVideoDevices(outputTemplateJson.services[serviceName].devices) + }); + } + + console.info(`ServiceBuilder:build() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::build() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + return retr; +} + +module.exports = ServiceBuilder; diff --git a/.internal/templates/services/motioneye/config.js b/.internal/templates/services/motioneye/config.js new file mode 100644 index 000000000..a68bd8b04 --- /dev/null +++ b/.internal/templates/services/motioneye/config.js @@ -0,0 +1,55 @@ +const motioneye = () => { + const retr = {}; + + const serviceName = 'motioneye'; + + retr.getConfigOptions = () => { + return { + serviceName, // Required + labeledPorts: { + "8765:8765": 'http', + "8081:60081": 'streaming1', + "8082:60082": 'streaming2', + "8083:60083": 'streaming3' + }, + volumes: true, + devices: true, + networks: true, + logging: true + } + }; + + retr.getHelp = () => { + return { + serviceName, // Required + links: { + "Website": 'https://motion-project.github.io/', // Website of service + "Docker": 'https://hub.docker.com/r/jshridha/motioneye', + "Source Code": 'https://github.com/ccrisan/motioneye', + "{$displayName} Documentation": 'https://github.com/ccrisan/motioneye/wiki', + rawMarkdownRemote: '', // Usually links to github raw help pages. + rawMarkdownLocal: '', // Relative path to docs locally + "IOTstack Documentation for {$displayName}": 'https://sensorsiot.github.io/IOTstack/Containers/MotionEye/' // Usually links to the github page for this service. + } + }; + }; + + retr.getCommands = () => { + return { + commands: {} // Key/value pair of helper commands user can run locally + }; + }; + + retr.getMeta = () => { + return { + serviceName, // Required + displayName: 'Motion Eye', + serviceTypeTags: ['wui', 'physical security', 'video'], + iconUri: '/logos/motioneye.png' + }; + }; + + return retr; +}; + +module.exports = motioneye; diff --git a/.templates/motioneye/service.yml b/.internal/templates/services/motioneye/template.yml similarity index 68% rename from .templates/motioneye/service.yml rename to .internal/templates/services/motioneye/template.yml index 9c80dad7d..2af40a151 100644 --- a/.templates/motioneye/service.yml +++ b/.internal/templates/services/motioneye/template.yml @@ -4,11 +4,18 @@ motioneye: restart: unless-stopped ports: - "8765:8765" - - "8081:8081" + - "8081:60081" + - "8082:60082" + - "8083:60083" volumes: - /etc/localtime:/etc/localtime:ro - ./volumes/motioneye/etc_motioneye:/etc/motioneye - ./volumes/motioneye/var_lib_motioneye:/var/lib/motioneye + devices: + - "/dev/video0" networks: - iotstack_nw - + logging: + options: + max-size: "5m" + max-file: "3" diff --git a/.internal/templates/services/nextcloud/build.js b/.internal/templates/services/nextcloud/build.js new file mode 100644 index 000000000..df864b0ee --- /dev/null +++ b/.internal/templates/services/nextcloud/build.js @@ -0,0 +1,363 @@ +const ServiceBuilder = ({ + settings, + version, + logger +}) => { + const retr = {}; + const serviceName = 'nextcloud'; + + const { generatePassword } = require('../../../src/utils/stringGenerate'); + const { byName } = require('../../../src/utils/interpolate'); + + const { + setImageTag, + setModifiedPorts, + setLoggingState, + setNetworkMode, + setNetworks, + setVolumes, + setEnvironmentVariables, + setDevices + } = require('../../../src/utils/commonCompileLogic'); + + const { + checkPortConflicts, + checkNetworkConflicts, + checkDependencyServices + } = require('../../../src/utils/commonBuildChecks'); + + + /* + Order: + 1. compile() - merges build options into the final JSON output. + 2. issues() - runs checks on the compile()'ed JSON, and can also test for errors. + 3. assume() - sets required default values if they are not specified in compile(). Once defaults are set, it reruns compile(). This function is optional + 4. build() - sets up scripts and files. + */ + + retr.init = () => { + logger.debug(`ServiceBuilder:init() - '${serviceName}', 'nextcloud_db'`); + }; + + const createVolumesDirectory = () => { + return ` +mkdir -p ./volumes/nextcloud/html +mkdir -p ./volumes/nextcloud/db +`; + }; + + const checkVolumesDirectory = () => { + return ` +HAS_ERROR="false" +if [[ ! -d ./volumes/nextcloud/html ]]; then + echo "Nextcloud html directory is missing!" + HAS_ERROR="true" +fi + +if [[ ! -d ./volumes/nextcloud/db ]]; then + echo "Nextcloud db directory is missing!" + HAS_ERROR="true" +fi + +if [[ "$HAS_ERROR" == "true" ]]; then + sleep 1 +fi +`; + }; + + retr.compile = ({ + outputTemplateJson, + buildOptions + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:compile() - '${serviceName}', 'nextcloud_db' started`); + + const compileResults = { + modifiedImage: setImageTag({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedPorts: setModifiedPorts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedLogging: setLoggingState({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworkMode: setNetworkMode({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworks: setNetworks({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedVolumes: setVolumes({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedEnvironment: setEnvironmentVariables({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedDevices: setDevices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }) + }; + + // Generate and sync relevant env vars + ncMariaDbRootPw = generatePassword(); + ncMariaDbPw = generatePassword(); + const syncDbSettings = { + MYSQL_PASSWORD: ncMariaDbPw, + MYSQL_ROOT_PASSWORD: ncMariaDbRootPw + }; + + const serviceEnvironmentList = outputTemplateJson?.services?.[serviceName]?.environment ?? []; + const dbEnvironmentList = outputTemplateJson?.services?.nextcloud_db?.environment ?? []; + + if (Array.isArray(serviceEnvironmentList) && serviceEnvironmentList.length > 0) { + serviceEnvironmentList.forEach((envKVP, index) => { + const envKey = envKVP.split('=')[0]; + const envValue = envKVP.split('=')[1]; + + if (envKey === 'MYSQL_PASSWORD') { + outputTemplateJson.services[serviceName].environment[index] = byName( + outputTemplateJson.services[serviceName].environment[index], + { + nextcloudRandomPassword: syncDbSettings.MYSQL_PASSWORD + } + ); + syncDbSettings.MYSQL_PASSWORD = outputTemplateJson.services[serviceName].environment[index]; + } + + if (envKey === 'MYSQL_ROOT_PASSWORD') { + outputTemplateJson.services[serviceName].environment[index] = byName( + outputTemplateJson.services[serviceName].environment[index], + { + nextcloudRootRandomPassword: syncDbSettings.MYSQL_ROOT_PASSWORD + } + ); + syncDbSettings.MYSQL_ROOT_PASSWORD = outputTemplateJson.services[serviceName].environment[index]; + } + + if (envKey === 'MYSQL_USER') { + syncDbSettings.MYSQL_USER = `${envKey}=${envValue}`; + } + + if (envKey === 'MYSQL_DATABASE') { + syncDbSettings.MYSQL_DATABASE = `${envKey}=${envValue}`; + } + }); + + outputTemplateJson.services[serviceName].environment = outputTemplateJson.services[serviceName].environment.filter((envKVP) => { // Remove unneeded env var + if (envKVP.split('=')[0] === 'MYSQL_ROOT_PASSWORD') { + return false; + } + + return true; + }); + } + + if (Array.isArray(dbEnvironmentList) && dbEnvironmentList.length > 0) { + dbEnvironmentList.forEach((envKVP, index) => { + const envKey = envKVP.split('=')[0]; + + if (envKey === 'MYSQL_PASSWORD') { + outputTemplateJson.services.nextcloud_db.environment[index] = syncDbSettings.MYSQL_PASSWORD; + } + + if (envKey === 'MYSQL_ROOT_PASSWORD') { + outputTemplateJson.services.nextcloud_db.environment[index] = syncDbSettings.MYSQL_ROOT_PASSWORD; + } + + if (envKey === 'MYSQL_USER') { + outputTemplateJson.services.nextcloud_db.environment[index] = syncDbSettings.MYSQL_USER; + } + + if (envKey === 'MYSQL_DATABASE') { + outputTemplateJson.services.nextcloud_db.environment[index] = syncDbSettings.MYSQL_DATABASE; + } + }); + } + + // Move db volume to db service. + const serviceVolumeList = outputTemplateJson?.services?.[serviceName]?.volumes ?? []; + let dbVol = ''; + if (Array.isArray(serviceVolumeList) && serviceVolumeList.length > 0) { + serviceVolumeList.forEach((vol, index) => { + const internalVol = vol.split(':')[1]; + + if (internalVol === '/var/lib/mysql') { + dbVol = vol; + } + }); + + outputTemplateJson.services[serviceName].volumes = outputTemplateJson.services[serviceName].volumes.filter((volume) => { // Remove unneeded volume + if (volume.split(':')[1] === '/var/lib/mysql') { + return false; + } + + return true; + }); + } + + let foundVolEntry = false; + if (Array.isArray(outputTemplateJson?.services?.nextcloud_db?.volumes ?? false) && outputTemplateJson.services.nextcloud_db.volumes.length > 0 && dbVol) { + outputTemplateJson.services.nextcloud_db.volumes.forEach((vol, index) => { + const internalVol = vol.split(':')[1]; + + if (internalVol === '/var/lib/mysql') { + foundVolEntry = true; + outputTemplateJson.services.nextcloud_db.volumes[index] = dbVol; + } + }); + } + + if (!foundVolEntry && dbVol && (outputTemplateJson?.services?.nextcloud_db?.volumes ?? false)) { + outputTemplateJson.services.nextcloud_db.volumes.push(dbVol); + } + + console.info(`ServiceBuilder:compile() - '${serviceName}', 'nextcloud_db' Results:`, compileResults); + + console.info(`ServiceBuilder:compile() - '${serviceName}', 'nextcloud_db' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + return reject({ + component: `ServiceBuilder::compile() - '${serviceName}', 'nextcloud_db'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.issues = ({ + outputTemplateJson, + buildOptions, + tmpPath + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:issues() - '${serviceName}', 'nextcloud_db' started`); + let issues = []; + + const portConflicts = checkPortConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...portConflicts]; + + const serviceDependencies = checkDependencyServices({ buildTemplate: outputTemplateJson, buildOptions, serviceName, ignoreDependencies: ['nextcloud_db'] }); + issues = [...issues, ...serviceDependencies]; + + const networkConflicts = checkNetworkConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + if (networkConflicts) { + issues.push(networkConflicts); + } + + const environmentList = outputTemplateJson?.services?.[serviceName]?.environment ?? []; + if (Array.isArray(environmentList) && environmentList.length > 0) { + let pwKeysFound = false; + environmentList.forEach((envKVP) => { + const envKey = envKVP.split('=')[0]; + const envValue = envKVP.split('=')[1]; + if (envKey && envValue) { + if (envKey === 'MYSQL_ROOT_PASSWORD' || envKey === 'MYSQL_PASSWORD') { + pwKeysFound = true; + if (envValue === 'Unset' || envValue === 'Unset') { + issues.push({ + type: 'service', + name: serviceName, + issueType: 'environment', + message: `Database passwords are not set in environment variables.` + }); + } + } + } + }); + + if (!pwKeysFound) { + issues.push({ + type: 'service', + name: serviceName, + issueType: 'environment', + message: `No database environment variables found.` + }); + } + } else { + issues.push({ + type: 'service', + name: serviceName, + issueType: 'environment', + message: `No environment variables found. Service may not start unless they are set.` + }); + } + + const volumeList = outputTemplateJson?.services?.[serviceName]?.volumes ?? []; + if (Array.isArray(environmentList) && environmentList.length > 0) { + const databaseVolumeSet = false; + volumeList.forEach((vol) => { + const internalVol = vol.split(':')[1]; + const externalVol = vol.split(':')[0]; + + if (internalVol === '/var/lib/mysql' && externalVol === 'Unset') { + issues.push({ + type: 'service', + name: serviceName, + issueType: 'volumes', + message: `Volumes are not set` + }); + } + }); + + } + + console.info(`ServiceBuilder:issues() - '${serviceName}', 'nextcloud_db' Issues found: ${issues.length}`); + console.info(`ServiceBuilder:issues() - '${serviceName}', 'nextcloud_db' completed`); + return resolve(issues); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::issues() - '${serviceName}', 'nextcloud_db'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.build = ({ + outputTemplateJson, + buildOptions, + tmpPath, + zipList, + prebuildScripts, + postbuildScripts + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:build() - '${serviceName}', 'nextcloud_db' started`); + + prebuildScripts.push({ + serviceName, + comment: 'Create required service directory exists for first launch', + multilineComment: null, + code: createVolumesDirectory() + }); + + postbuildScripts.push({ + serviceName, + comment: 'Ensure required service directory exists for launch', + multilineComment: null, + code: checkVolumesDirectory() + }); + + console.info(`ServiceBuilder:build() - '${serviceName}', 'nextcloud_db' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::build() - '${serviceName}', 'nextcloud_db'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + return retr; +} + +module.exports = ServiceBuilder; diff --git a/.internal/templates/services/nextcloud/config.js b/.internal/templates/services/nextcloud/config.js new file mode 100644 index 000000000..af4082554 --- /dev/null +++ b/.internal/templates/services/nextcloud/config.js @@ -0,0 +1,75 @@ +const nextcloud = () => { + const retr = {}; + + const serviceName = 'nextcloud'; + + retr.getConfigOptions = () => { + return { + serviceName, // Required + modifyableEnvironment: [ + { + key: 'TZ', + value: 'Etc/UTC' + }, + { + key: 'MYSQL_HOST', + value: 'nextcloud_db' + }, + { + key: 'MYSQL_PASSWORD', + value: '{$nextcloudRandomPassword}' + }, + { + key: 'MYSQL_DATABASE', + value: 'nextcloud' + }, + { + key: 'MYSQL_USER', + value: 'nextcloud' + }, + { + key: 'MYSQL_ROOT_PASSWORD', + value: '{$nextcloudRootRandomPassword}' + } + ], + labeledPorts: { + "9321:80": 'http' + }, + volumes: true, + networks: true, + logging: true + } + }; + + retr.getHelp = () => { + return { + serviceName, // Required + links: { + "Docker": 'https://hub.docker.com/_/nextcloud', + "Source Code": 'https://github.com/nextcloud/docker', + rawMarkdownRemote: '', // Usually links to github raw help pages. + rawMarkdownLocal: '', // Relative path to docs locally + "IOTstack Documentation for {$displayName}": 'https://sensorsiot.github.io/IOTstack/Containers/NextCloud/' // Usually links to the github page for this service. + } + }; + }; + + retr.getCommands = () => { + return { + commands: {} // Key/value pair of helper commands user can run locally + }; + }; + + retr.getMeta = () => { + return { + serviceName, // Required + displayName: 'NextCloud', + serviceTypeTags: ['wui'], + iconUri: '/logos/nextcloud.png' + }; + }; + + return retr; +}; + +module.exports = nextcloud; diff --git a/.templates/nextcloud/service.yml b/.internal/templates/services/nextcloud/template.yml similarity index 60% rename from .templates/nextcloud/service.yml rename to .internal/templates/services/nextcloud/template.yml index 0aa35d4cb..999d23a6b 100644 --- a/.templates/nextcloud/service.yml +++ b/.internal/templates/services/nextcloud/template.yml @@ -1,35 +1,36 @@ nextcloud: - image: nextcloud container_name: nextcloud + image: nextcloud + restart: unless-stopped + environment: + - MYSQL_HOST=nextcloud_db + - MYSQL_PASSWORD=Unset # removed during compile + - MYSQL_DATABASE=nextcloud + - MYSQL_USER=nextcloud ports: - "9321:80" volumes: - - ./volumes/nextcloud/html:/var/www/html:rw - restart: unless-stopped - depends_on: - - nextcloud_db - links: + - ./volumes/nextcloud/html:/var/www/html + depends_on: - nextcloud_db networks: - iotstack_nw - nextcloud_internal - environment: - - MYSQL_HOST=nextcloud_db - - MYSQL_PASSWORD=%randomMySqlPassword% - - MYSQL_DATABASE=nextcloud - - MYSQL_USER=nextcloud nextcloud_db: - image: linuxserver/mariadb container_name: nextcloud_db - volumes: - - ./volumes/nextcloud/db:/config + build: ./.templates/mariadb/. + restart: unless-stopped environment: - - MYSQL_ROOT_PASSWORD=%randomPassword% - - MYSQL_PASSWORD=%randomMySqlPassword% + - TZ=Etc/UTC + - PUID=1000 + - PGID=1000 + - MYSQL_ROOT_PASSWORD=Unset # removed during compile + - MYSQL_PASSWORD=Unset # removed during compile - MYSQL_DATABASE=nextcloud - MYSQL_USER=nextcloud - restart: unless-stopped + volumes: + - ./volumes/nextcloud/db:/config + - ./volumes/nextcloud/db_backup:/backup networks: - nextcloud_internal - diff --git a/.internal/templates/services/nodered/build.js b/.internal/templates/services/nodered/build.js new file mode 100644 index 000000000..0af418d99 --- /dev/null +++ b/.internal/templates/services/nodered/build.js @@ -0,0 +1,260 @@ +const fs = require('fs'); +const path = require('path'); +const { byName } = require('../../../src/utils/interpolate'); + +const ServiceBuilder = ({ + settings, + version, + logger +}) => { + const retr = {}; + const serviceName = 'nodered'; + + const { + setImageTag, + setModifiedPorts, + setLoggingState, + setNetworkMode, + setNetworks, + setVolumes, + setEnvironmentVariables, + setDevices + } = require('../../../src/utils/commonCompileLogic'); + + const { + checkPortConflicts, + checkNetworkConflicts, + checkDependencyServices + } = require('../../../src/utils/commonBuildChecks'); + + /* + Order: + 1. compile() - merges build options into the final JSON output. + 2. issues() - runs checks on the compile()'ed JSON, and can also test for errors. + 3. assume() - sets required default values if they are not specified in compile(). Once defaults are set, it reruns compile(). This function is optional + 4. build() - sets up scripts and files. + */ + + retr.init = () => { + logger.debug(`ServiceBuilder:init() - '${serviceName}'`); + }; + + const createVolumesDirectory = () => { + return ` +mkdir -p ./volumes/nodered/data +`; + }; + + const checkVolumesDirectory = () => { + return ` +if [[ ! -d ./volumes/nodered/data ]]; then + echo "Nodered data directory is missing!" + sleep 2 +fi +`; + }; + + retr.compile = ({ + outputTemplateJson, + buildOptions, + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:compile() - '${serviceName}' started`); + + const compileResults = { + modifiedImage: setImageTag({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedPorts: setModifiedPorts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedLogging: setLoggingState({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworkMode: setNetworkMode({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworks: setNetworks({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedVolumes: setVolumes({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedEnvironment: setEnvironmentVariables({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedDevices: setDevices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }) + }; + console.info(`ServiceBuilder:compile() - '${serviceName}' Results:`, compileResults); + + console.info(`ServiceBuilder:compile() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + return reject({ + component: `ServiceBuilder::compile() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.issues = ({ + outputTemplateJson, + buildOptions, + tmpPath + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:issues() - '${serviceName}' started`); + let issues = []; + + const portConflicts = checkPortConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...portConflicts]; + + const serviceDependencies = checkDependencyServices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...serviceDependencies]; + + const networkConflicts = checkNetworkConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + if (networkConflicts) { + issues.push(networkConflicts); + } + + let addonsSelected = false; + const addonsList = buildOptions?.configurations?.services?.nodered?.addonsList ?? []; + if (addonsList.length > 0) { + addonsSelected = true; + } + console.info(`ServiceBuilder:issues() - '${serviceName}' Addons selected: ${addonsList.length}`); + console.info(`ServiceBuilder:issues() - '${serviceName}' completed`); + if (!addonsSelected) { + issues.push({ + type: 'service', + name: serviceName, + issueType: 'no addons', + message: 'No pallette addons selected for NodeRed. Select addons in options to remove this warning. Default modules will be installed.' + }); + } + + console.info(`ServiceBuilder:issues() - '${serviceName}' Issues found: ${issues.length}`); + console.info(`ServiceBuilder:issues() - '${serviceName}' completed`); + return resolve(issues); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::issues() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.build = ({ + outputTemplateJson, + fileTimePrefix, + buildOptions, + tmpPath, + zipList, + prebuildScripts, + postbuildScripts + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:build() - '${serviceName}' started`); + const addonsList = buildOptions?.configurations?.services?.nodered?.addonsList ?? false; + const noderedDockerfileTemplate = path.join(__dirname, settings.paths.buildFiles, 'Dockerfile.template'); + const noderedDockerfileCommandTemplate = require(path.join(__dirname, settings.paths.buildFiles, 'addons.json')); + const tempDockerfileName = `${fileTimePrefix}_Dockerfile.template`; + + const templateData = fs.readFileSync(noderedDockerfileTemplate, { encoding: 'utf8', flag: 'r' }); + let addonDockerCommandOutput = noderedDockerfileCommandTemplate.data.dockerFileInstallCommand; + let addonDockerUnsafeCommandOutput = noderedDockerfileCommandTemplate.data.dockerFileInstallUnsafeCommand; + + let unsafeAddonsCount = 0; + let npmAddonsCount = 0; + + if (Array.isArray(addonsList)) { + if (addonsList.length > 0) { + addonsList.forEach((addon) => { + if (noderedDockerfileCommandTemplate.data.unsafeAddons.includes(addon)) { + addonDockerUnsafeCommandOutput += `${addon} `; + unsafeAddonsCount++; + } else { + addonDockerCommandOutput += `${addon} `; + npmAddonsCount++; + } + }); + } + + } else { + // Use default addons + const defaultAddons = require("./buildFiles/addons.json"); + (defaultAddons?.data?.addons?.defaultOn ?? []).forEach((addon) => { + if (noderedDockerfileCommandTemplate.data.unsafeAddons.includes(addon)) { + addonDockerUnsafeCommandOutput += `${addon} `; + unsafeAddonsCount++; + } else { + addonDockerCommandOutput += `${addon} `; + npmAddonsCount++; + } + }); + } + + if (unsafeAddonsCount < 1) { + addonDockerUnsafeCommandOutput = ''; + } + + if (npmAddonsCount < 1) { + addonDockerCommandOutput = ''; + } + + const outputDockerFile = byName(templateData, { + 'npmInstallModulesList': addonDockerCommandOutput, + 'npmInstallUnsafeModulesList': addonDockerUnsafeCommandOutput + }); + + const tempBuildFile = path.join(tmpPath, tempDockerfileName); + + fs.writeFileSync(tempBuildFile, outputDockerFile); + + zipList.push({ + fullPath: tempBuildFile, + zipName: '/services/nodered/Dockerfile' + }); + + prebuildScripts.push({ + serviceName, + comment: 'Create required service directory exists for first launch', + multilineComment: null, + code: createVolumesDirectory() + }); + + postbuildScripts.push({ + serviceName, + comment: 'Ensure required service directory exists for launch', + multilineComment: null, + code: checkVolumesDirectory() + }); + + console.info(`ServiceBuilder:build() - '${serviceName}' Addons: ${Array.isArray(addonsList) ? 'custom list' : 'default list'}`); + console.info(`ServiceBuilder:build() - '${serviceName}' Safe Addons: ${npmAddonsCount}, Unsafe addons: ${unsafeAddonsCount}`); + console.info(`ServiceBuilder:build() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::build() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + return retr; +} + +module.exports = ServiceBuilder; diff --git a/.templates/nodered/Dockerfile.template b/.internal/templates/services/nodered/buildFiles/Dockerfile.template old mode 100755 new mode 100644 similarity index 65% rename from .templates/nodered/Dockerfile.template rename to .internal/templates/services/nodered/buildFiles/Dockerfile.template index 6f461b105..efb02177e --- a/.templates/nodered/Dockerfile.template +++ b/.internal/templates/services/nodered/buildFiles/Dockerfile.template @@ -3,4 +3,5 @@ USER root RUN apk update && apk add --no-cache eudev-dev USER node-red -%run npm install modules list% +{$npmInstallModulesList} +{$npmInstallUnsafeModulesList} diff --git a/.internal/templates/services/nodered/buildFiles/addons.json b/.internal/templates/services/nodered/buildFiles/addons.json new file mode 100644 index 000000000..a4df599dd --- /dev/null +++ b/.internal/templates/services/nodered/buildFiles/addons.json @@ -0,0 +1,83 @@ +{ + "version": "0.0.1", + "application": "IOTstack", + "service": "nodered", + "comment": "Addons available for NodeRed", + "data": { + "dockerFileInstallCommand": "RUN cd /usr/src/node-red && npm install --save ", + "dockerFileInstallUnsafeCommand": "RUN cd /usr/src/node-red && npm install --save --unsafe-perm ", + "unsafeAddons": [ + "node-red-node-sqlite" + ], + "addons": { + "defaultOn": [ + "node-red-node-pi-gpiod", + "node-red-contrib-self-healing", + "node-red-contrib-influxdb", + "node-red-contrib-boolean-logic", + "node-red-node-rbe", + "node-red-configurable-ping", + "node-red-dashboard" + ], + "essentials": [ + "node-red-contrib-redis", + "node-red-node-pi-gpiod", + "node-red-contrib-self-healing", + "node-red-contrib-influxdb", + "node-red-contrib-boolean-logic", + "node-red-node-rbe", + "node-red-dashboard", + "node-red-node-sqlite", + "node-red-contrib-file-function", + "node-red-contrib-themes/midnight-red", + "node-red-contrib-home-assistant-websocket", + "node-red-node-openweathermap", + "node-red-contrib-discord" + ], + "defaultOff": [ + "node-red-node-openweathermap", + "node-red-contrib-discord", + "node-red-node-email", + "node-red-node-google", + "node-red-node-emoncms", + "node-red-node-geofence", + "node-red-node-ping", + "node-red-node-random", + "node-red-node-smooth", + "node-red-node-darksky", + "node-red-node-sqlite", + "node-red-node-serialport@0.15.0", + "node-red-contrib-config", + "node-red-contrib-grove", + "node-red-contrib-diode", + "node-red-contrib-sunevents", + "node-red-contrib-bigtimer", + "node-red-contrib-esplogin", + "node-red-contrib-timeout", + "node-red-contrib-moment", + "node-red-contrib-telegrambot", + "node-red-contrib-particle", + "node-red-contrib-web-worldmap", + "node-red-contrib-ramp-thermostat", + "node-red-contrib-isonline", + "node-red-contrib-npm", + "node-red-contrib-file-function", + "node-red-contrib-home-assistant-websocket", + "node-red-contrib-blynk-ws", + "node-red-contrib-owntracks", + "node-red-contrib-alexa-local", + "node-red-contrib-heater-controller", + "node-red-contrib-deconz", + "node-red-contrib-generic-ble", + "node-red-contrib-zigbee2mqtt", + "node-red-contrib-vcgencmd", + "node-red-contrib-themes/midnight-red", + "node-red-contrib-tf-function", + "node-red-contrib-tf-model", + "node-red-contrib-post-object-detection", + "node-red-contrib-bert-tokenizer", + "node-red-contrib-redis" + ] + } + } +} diff --git a/.internal/templates/services/nodered/config.js b/.internal/templates/services/nodered/config.js new file mode 100644 index 000000000..a80136ea3 --- /dev/null +++ b/.internal/templates/services/nodered/config.js @@ -0,0 +1,65 @@ +const path = require('path'); + +const nodered = ({ + settings, + version, + logger +}) => { + const retr = {}; + + const serviceName = 'nodered'; + + retr.getConfigOptions = () => { + const noderedModules = require(path.join(__dirname, settings.paths.buildFiles, 'addons.json')); + + return { + serviceName, // Required + labeledPorts: { + "1880:1880": 'http' + }, + volumes: true, + networks: true, + devices: true, + nodered_npmSelection: noderedModules?.data?.addons ?? [], + logging: true + } + }; + + retr.getHelp = () => { + return { + serviceName, // Required + links: { + "Website": 'https://nodered.org/', // Website of service + "Docker": 'https://hub.docker.com/r/nodered/node-red', + "Source Code": 'https://github.com/node-red', + "{$displayName} Documentation": 'https://nodered.org/docs/', + "Community": 'https://discourse.nodered.org/', + "{$displayName} Flows": 'https://flows.nodered.org/', + "Youtube": 'https://www.youtube.com/channel/UCQaB8NXBEPod7Ab8PPCLLAA', + "Blog": 'https://nodered.org/blog/', + rawMarkdownRemote: '', // Usually links to github raw help pages. + rawMarkdownLocal: '', // Relative path to docs locally + "IOTstack Documentation for {$displayName}": 'https://sensorsiot.github.io/IOTstack/Containers/Node-RED/' // Usually links to the github page for this service. + } + }; + }; + + retr.getCommands = () => { + return { + commands: {} // Key/value pair of helper commands user can run locally + }; + }; + + retr.getMeta = () => { + return { + serviceName, // Required + displayName: 'NodeRed', + serviceTypeTags: ['wui', 'dashboard', 'low code', 'graphs', 'aggregator', 'iot', 'server'], + iconUri: '/logos/nodered.png' + }; + }; + + return retr; +}; + +module.exports = nodered; diff --git a/.templates/nodered/service.yml b/.internal/templates/services/nodered/template.yml similarity index 88% rename from .templates/nodered/service.yml rename to .internal/templates/services/nodered/template.yml index bf0e5213d..54df8e450 100644 --- a/.templates/nodered/service.yml +++ b/.internal/templates/services/nodered/template.yml @@ -18,3 +18,7 @@ nodered: - "/dev/gpiomem:/dev/gpiomem" networks: - iotstack_nw + logging: + options: + max-size: "5m" + max-file: "3" diff --git a/.internal/templates/services/octoprint/build.js b/.internal/templates/services/octoprint/build.js new file mode 100644 index 000000000..4a09b75ba --- /dev/null +++ b/.internal/templates/services/octoprint/build.js @@ -0,0 +1,222 @@ +const ServiceBuilder = ({ + settings, + version, + logger +}) => { + const retr = {}; + const serviceName = 'deconz'; + + const { byName } = require('../../../src/utils/interpolate'); + + const { + setImageTag, + setModifiedPorts, + setLoggingState, + setNetworkMode, + setNetworks, + setVolumes, + setEnvironmentVariables, + setDevices + } = require('../../../src/utils/commonCompileLogic'); + + const { + checkPortConflicts, + checkNetworkConflicts, + checkDependencyServices + } = require('../../../src/utils/commonBuildChecks'); + + + /* + Order: + 1. compile() - merges build options into the final JSON output. + 2. issues() - runs checks on the compile()'ed JSON, and can also test for errors. + 3. assume() - sets required default values if they are not specified in compile(). Once defaults are set, it reruns compile(). This function is optional + 4. build() - sets up scripts and files. + */ + + retr.init = () => { + logger.debug(`ServiceBuilder:init() - '${serviceName}'`); + }; + + const createVolumesDirectory = () => { + return ` +mkdir -p ./volumes/octoprint +`; + }; + + const checkVolumesDirectory = () => { + return ` +if [[ ! -d ./volumes/octoprint ]]; then + echo "Octoprint directory is missing!" + sleep 2 +fi +`; + }; + + retr.compile = ({ + outputTemplateJson, + buildOptions + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:compile() - '${serviceName}' started`); + + const compileResults = { + modifiedImage: setImageTag({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedPorts: setModifiedPorts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedLogging: setLoggingState({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworkMode: setNetworkMode({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworks: setNetworks({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedVolumes: setVolumes({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedEnvironment: setEnvironmentVariables({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedDevices: setDevices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }) + }; + + const selectedDevice = buildOptions?.configurations?.services?.[serviceName]?.selectedDevice ?? ''; + + // Set deconz's selected device device + const deconzDevicesList = outputTemplateJson?.services?.[serviceName]?.devices ?? []; + if (Array.isArray(deconzDevicesList) && deconzDevicesList.length > 0) { + deconzDevicesList.forEach((device, index) => { + outputTemplateJson.services[serviceName].devices[index] = byName( + outputTemplateJson.services[serviceName].devices[index], + { + deconzSelectedDevice: selectedDevice + } + ); + }); + } else { + if ((outputTemplateJson?.services?.[serviceName] ?? false) && selectedDevice) { + outputTemplateJson.services[serviceName].devices = [selectedDevice]; + } + } + + // Set deconz's selected device env var + const serviceEnvironmentList = outputTemplateJson?.services?.[serviceName]?.environment ?? []; + if (Array.isArray(serviceEnvironmentList) && serviceEnvironmentList.length > 0) { + serviceEnvironmentList.forEach((envKVP, index) => { + outputTemplateJson.services[serviceName].environment[index] = byName( + outputTemplateJson.services[serviceName].environment[index], + { + deconzSelectedDevice: selectedDevice + } + ); + }); + } + + console.info(`ServiceBuilder:compile() - '${serviceName}' Results:`, compileResults); + + console.info(`ServiceBuilder:compile() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + return reject({ + component: `ServiceBuilder::compile() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.issues = ({ + outputTemplateJson, + buildOptions, + tmpPath + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:issues() - '${serviceName}' started`); + let issues = []; + + const portConflicts = checkPortConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...portConflicts]; + + const serviceDependencies = checkDependencyServices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...serviceDependencies]; + + const networkConflicts = checkNetworkConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + if (networkConflicts) { + issues.push(networkConflicts); + } + + console.info(`ServiceBuilder:issues() - '${serviceName}' Issues found: ${issues.length}`); + console.info(`ServiceBuilder:issues() - '${serviceName}' completed`); + return resolve(issues); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::issues() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.build = ({ + outputTemplateJson, + buildOptions, + tmpPath, + zipList, + prebuildScripts, + postbuildScripts + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:build() - '${serviceName}' started`); + + if (buildOptions?.configurations?.services?.[serviceName]?.selectedDevice ?? false) { + postbuildScripts.push({ + serviceName, + comment: 'Check deconz set env device', + multilineComment: null, + code: checkDeconzDevice(buildOptions?.configurations?.services?.[serviceName]?.selectedDevice ?? '/dev/null') + }); + } + + prebuildScripts.push({ + serviceName, + comment: 'Create required service directory exists for first launch', + multilineComment: null, + code: createVolumesDirectory() + }); + + postbuildScripts.push({ + serviceName, + comment: 'Ensure required service directory exists for launch', + multilineComment: null, + code: checkVolumesDirectory() + }); + + console.info(`ServiceBuilder:build() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::build() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + return retr; +} + +module.exports = ServiceBuilder; diff --git a/.internal/templates/services/octoprint/config.js b/.internal/templates/services/octoprint/config.js new file mode 100644 index 000000000..3e99f84ab --- /dev/null +++ b/.internal/templates/services/octoprint/config.js @@ -0,0 +1,73 @@ +const deconz = () => { + const retr = {}; + + const serviceName = 'deconz'; + + retr.getConfigOptions = () => { + return { + serviceName, // Required + labeledPorts: { + "9980:80": 'http', + "9981:8080": 'other' + }, + modifyableEnvironment: [ + { + key: 'TZ', + value: 'Etc/UTC' + }, + { + key: 'ENABLE_MJPG_STREAMER', + value: '' + }, + { + key: 'MJPG_STREAMER_INPUT', + value: '' + }, + { + key: 'CAMERA_DEV', + value: '' + } + ], + devices: true, + volumes: true, + networks: true, + logging: true + } + }; + + retr.getHelp = () => { + return { + serviceName, // Required + links: { + "Website": 'https://octoprint.org/', // Website of service + "Docker": 'https://hub.docker.com/r/octoprint/octoprint', + "Source Code": 'https://github.com/OctoPrint/', + "{$displayName} Documentation": 'https://docs.octoprint.org/en/master/', + "Community": 'https://community.octoprint.org/', + "Community Chat (Discord)": 'https://discord.octoprint.org/', + rawMarkdownRemote: '', // Usually links to github raw help pages. + rawMarkdownLocal: '', // Relative path to docs locally + "IOTstack Documentation for {$displayName}": 'https://sensorsiot.github.io/IOTstack/Containers/Octoprint/' + } + }; + }; + + retr.getCommands = () => { + return { + commands: {} // Key/value pair of helper commands user can run locally + }; + }; + + retr.getMeta = () => { + return { + serviceName, // Required + displayName: 'Octoprint (Untested)', + serviceTypeTags: ['3d printer'], + iconUri: '/logos/octoprint.png' + }; + }; + + return retr; +}; + +module.exports = deconz; diff --git a/.templates/octoprint/service.yml b/.internal/templates/services/octoprint/template.yml similarity index 85% rename from .templates/octoprint/service.yml rename to .internal/templates/services/octoprint/template.yml index f2dfa91ad..c0b7e466a 100644 --- a/.templates/octoprint/service.yml +++ b/.internal/templates/services/octoprint/template.yml @@ -11,10 +11,9 @@ octoprint: - "9980:80" - "9981:8080" devices: - - /dev/ttyAMA0:/dev/ttyACM0 - # - /dev/video0:/dev/video0 + - "/dev/ttyACM0" + - "/dev/video0" volumes: - ./volumes/octoprint:/octoprint networks: - iotstack_nw - diff --git a/.internal/templates/services/openhab/build.js b/.internal/templates/services/openhab/build.js new file mode 100644 index 000000000..78f698598 --- /dev/null +++ b/.internal/templates/services/openhab/build.js @@ -0,0 +1,146 @@ +const ServiceBuilder = ({ + settings, + version, + logger +}) => { + const retr = {}; + const serviceName = 'adminer'; + + const { + setImageTag, + setModifiedPorts, + setLoggingState, + setNetworkMode, + setNetworks, + setDevices, + setEnvironmentVariables + } = require('../../../src/utils/commonCompileLogic'); + + const { + checkPortConflicts, + checkNetworkConflicts, + checkDependencyServices + } = require('../../../src/utils/commonBuildChecks'); + + /* + Order: + 1. compile() - merges build options into the final JSON output. + 2. issues() - runs checks on the compile()'ed JSON, and can also test for errors. + 3. assume() - sets required default values if they are not specified in compile(). Once defaults are set, it reruns compile(). This function is optional + 4. build() - sets up scripts and files. + */ + + retr.init = () => { + logger.debug(`ServiceBuilder:init() - '${serviceName}'`); + }; + + retr.compile = ({ + outputTemplateJson, + buildOptions, + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:compile() - '${serviceName}' started`); + + const compileResults = { + modifiedImage: setImageTag({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedPorts: setModifiedPorts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedLogging: setLoggingState({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworkMode: setNetworkMode({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworks: setNetworks({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedEnvironment: setEnvironmentVariables({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedDevices: setDevices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }) + }; + console.info(`ServiceBuilder:compile() - '${serviceName}' Results:`, compileResults); + + console.info(`ServiceBuilder:compile() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + return reject({ + component: `ServiceBuilder::compile() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.issues = ({ + outputTemplateJson, + buildOptions, + tmpPath + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:issues() - '${serviceName}' started`); + let issues = []; + + const portConflicts = checkPortConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...portConflicts]; + + const serviceDependencies = checkDependencyServices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...serviceDependencies]; + + const networkConflicts = checkNetworkConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + if (networkConflicts) { + issues.push(networkConflicts); + } + + console.info(`ServiceBuilder:issues() - '${serviceName}' Issues found: ${issues.length}`); + console.info(`ServiceBuilder:issues() - '${serviceName}' completed`); + return resolve(issues); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::issues() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.build = ({ + outputTemplateJson, + buildOptions, + tmpPath, + zipList, + prebuildScripts, + postbuildScripts + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:build() - '${serviceName}' started`); + // Code here + console.info(`ServiceBuilder:build() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::build() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + return retr; +} + +module.exports = ServiceBuilder; diff --git a/.internal/templates/services/openhab/config.js b/.internal/templates/services/openhab/config.js new file mode 100644 index 000000000..282b3832d --- /dev/null +++ b/.internal/templates/services/openhab/config.js @@ -0,0 +1,66 @@ +const adminer = () => { + const retr = {}; + + const serviceName = 'adminer'; + + retr.getConfigOptions = () => { + return { + serviceName, // Required + labeledPorts: { + "4050:4050": 'http', + "4051:4051": 'ssl' + }, + modifyableEnvironment: [ + { + key: 'OPENHAB_HTTP_PORT', + value: '4050' + }, + { + key: 'OPENHAB_HTTPS_PORT', + value: '4051' + }, + { + key: 'EXTRA_JAVA_OPTS', + value: '-Duser.timezone=Etc/UTC' + } + ], + volumes: true, + networks: false, + logging: true + } + }; + + retr.getHelp = () => { + return { + serviceName, // Required + links: { + "Website": 'https://www.openhab.org/', // Website of service + "Docker": 'https://hub.docker.com/r/openhab/openhab/', + "Source Code": 'https://github.com/openhab', + "{$displayName} Documentation": 'https://www.openhab.org/docs/installation/docker.html', + rawMarkdownRemote: '', // Usually links to github raw help pages. + rawMarkdownLocal: '', // Relative path to docs locally + "IOTstack Documentation for {$displayName}": 'https://sensorsiot.github.io/IOTstack/Containers/OpenHab/' + } + }; + }; + + retr.getCommands = () => { + return { + commands: {} // Key/value pair of helper commands user can run locally + }; + }; + + retr.getMeta = () => { + return { + serviceName, // Required + displayName: 'Open Hab (untested)', + serviceTypeTags: ['wui', 'dashboard', 'home automation'], + iconUri: '/logos/openhab.png' + }; + }; + + return retr; +}; + +module.exports = adminer; diff --git a/.templates/openhab/service.yml b/.internal/templates/services/openhab/template.yml similarity index 79% rename from .templates/openhab/service.yml rename to .internal/templates/services/openhab/template.yml index c6205caa1..b49eae762 100644 --- a/.templates/openhab/service.yml +++ b/.internal/templates/services/openhab/template.yml @@ -1,11 +1,8 @@ openhab: - image: "openhab/openhab:latest" container_name: openhab + image: "openhab/openhab:latest" restart: unless-stopped network_mode: host - #ports: - #- "4050:4050" - #- "4051:4051" volumes: - "/etc/localtime:/etc/localtime:ro" - "/etc/timezone:/etc/timezone:ro" @@ -15,4 +12,8 @@ openhab: environment: - OPENHAB_HTTP_PORT=4050 - OPENHAB_HTTPS_PORT=4051 - - EXTRA_JAVA_OPTS=-Duser.timezone=Etc/UTC" + - EXTRA_JAVA_OPTS=-Duser.timezone=Etc/UTC + logging: + options: + max-size: "5m" + max-file: "3" diff --git a/.internal/templates/services/pgadmin4/template.yml b/.internal/templates/services/pgadmin4/template.yml new file mode 100644 index 000000000..0ac7a5fef --- /dev/null +++ b/.internal/templates/services/pgadmin4/template.yml @@ -0,0 +1,12 @@ +pgadmin4: + container_name: pgadmin4 + image: gpongelli/pgadmin4-arm:latest-armv7 + platform: linux/arm/v7 +# image: gpongelli/pgadmin4-arm:latest-armv8 + restart: unless-stopped + environment: + - TZ=${TZ:-Etc/UTC} + ports: + - "5050:5050" + volumes: + - ./volumes/pgadmin4:/pgadmin4 diff --git a/.internal/templates/services/pihole/build.js b/.internal/templates/services/pihole/build.js new file mode 100644 index 000000000..2bbc4f884 --- /dev/null +++ b/.internal/templates/services/pihole/build.js @@ -0,0 +1,169 @@ +const ServiceBuilder = ({ + settings, + version, + logger +}) => { + const retr = {}; + const serviceName = 'pihole'; + + const { + setModifiedPorts, + setLoggingState, + setNetworkMode, + setNetworks + } = require('../../../src/utils/commonCompileLogic'); + + const { + checkPortConflicts, + checkNetworkConflicts, + checkDependencyServices + } = require('../../../src/utils/commonBuildChecks'); + + /* + Order: + 1. compile() - merges build options into the final JSON output. + 2. issues() - runs checks on the compile()'ed JSON, and can also test for errors. + 3. assume() - sets required default values if they are not specified in compile(). Once defaults are set, it reruns compile(). This function is optional + 4. build() - sets up scripts and files. + */ + + retr.init = () => { + logger.debug(`ServiceBuilder:init() - '${serviceName}'`); + }; + + const createVolumesDirectory = () => { + return ` +mkdir -p ./volumes/pihole/etc-pihole +`; + }; + + const checkVolumesDirectory = () => { + return ` +if [[ ! -d ./volumes/pihole/etc-pihole ]]; then + echo "PiHole config directory is missing!" + sleep 2 +fi +`; + }; + + retr.compile = ({ + outputTemplateJson, + buildOptions, + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:compile() - '${serviceName}' started`); + + const compileResults = { + modifiedPorts: setModifiedPorts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedLogging: setLoggingState({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworkMode: setNetworkMode({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworks: setNetworks({ buildTemplate: outputTemplateJson, buildOptions, serviceName }) + }; + console.info(`ServiceBuilder:compile() - '${serviceName}' Results:`, compileResults); + + console.info(`ServiceBuilder:compile() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + return reject({ + component: `ServiceBuilder::compile() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.issues = ({ + outputTemplateJson, + buildOptions, + tmpPath + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:issues() - '${serviceName}' started`); + let issues = []; + + const portConflicts = checkPortConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...portConflicts]; + + const serviceDependencies = checkDependencyServices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...serviceDependencies]; + + const networkConflicts = checkNetworkConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + if (networkConflicts) { + issues.push(networkConflicts); + } + + console.info(`ServiceBuilder:issues() - '${serviceName}' Issues found: ${issues.length}`); + console.info(`ServiceBuilder:issues() - '${serviceName}' completed`); + return resolve(issues); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::issues() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.build = ({ + outputTemplateJson, + buildOptions, + tmpPath, + zipList, + prebuildScripts, + postbuildScripts + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:build() - '${serviceName}' started`); + + prebuildScripts.push({ + serviceName, + comment: 'Create required service directory exists for first launch', + multilineComment: null, + code: createVolumesDirectory() + }); + + postbuildScripts.push({ + serviceName, + comment: 'Ensure required service directory exists for launch', + multilineComment: null, + code: checkVolumesDirectory() + }); + + console.info(`ServiceBuilder:build() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::build() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + return retr; +} + +module.exports = ServiceBuilder; diff --git a/.internal/templates/services/pihole/config.js b/.internal/templates/services/pihole/config.js new file mode 100644 index 000000000..26f6138d6 --- /dev/null +++ b/.internal/templates/services/pihole/config.js @@ -0,0 +1,56 @@ +const pihole = () => { + const retr = {}; + + const serviceName = 'pihole'; + + retr.getConfigOptions = () => { + return { + serviceName, // Required + labeledPorts: { + "8089:80": 'http' + }, + modifyableEnvironment: [ + { + key: 'INTERFACE', + value: 'eth0' + } + ], + volumes: true, + networks: true, + logging: true + } + }; + + retr.getHelp = () => { + return { + serviceName, // Required + links: { + "Website": 'https://pi-hole.net/', + "Docker": 'https://hub.docker.com/r/pihole/pihole', + "Source Code": 'https://github.com/pi-hole/pi-hole', + rawMarkdownRemote: '', // Usually links to github raw help pages. + rawMarkdownLocal: '', // Relative path to docs locally + "IOTstack Documentation for {$displayName}": 'https://sensorsiot.github.io/IOTstack/Containers/Pi-hole/' // Usually links to the github page for this service. + } + }; + }; + + retr.getCommands = () => { + return { + commands: {} // Key/value pair of helper commands user can run locally + }; + }; + + retr.getMeta = () => { + return { + serviceName, // Required + displayName: 'PiHole', + serviceTypeTags: ['wui', 'dns', 'dashboard'], + iconUri: '/logos/pihole.png' + }; + }; + + return retr; +}; + +module.exports = pihole; diff --git a/.internal/templates/services/pihole/template.yml b/.internal/templates/services/pihole/template.yml new file mode 100644 index 000000000..ea83abb05 --- /dev/null +++ b/.internal/templates/services/pihole/template.yml @@ -0,0 +1,27 @@ +pihole: + container_name: pihole + image: pihole/pihole:latest + ports: + - "8089:80/tcp" + - "53:53/tcp" + - "53:53/udp" + - "67:67/udp" + environment: + - TZ=${TZ:-Etc/UTC} + - WEBPASSWORD= + # see https://sensorsiot.github.io/IOTstack/Containers/Pi-hole/#adminPassword + - INTERFACE=eth0 + # see https://github.com/pi-hole/docker-pi-hole#environment-variables + volumes: + - ./volumes/pihole/etc-pihole:/etc/pihole + - ./volumes/pihole/etc-dnsmasq.d:/etc/dnsmasq.d + dns: + - 127.0.0.1 + - 1.1.1.1 + cap_add: + - NET_ADMIN + restart: unless-stopped + logging: + options: + max-size: "5m" + max-file: "3" diff --git a/.internal/templates/services/plex/build.js b/.internal/templates/services/plex/build.js new file mode 100644 index 000000000..9fa6affe4 --- /dev/null +++ b/.internal/templates/services/plex/build.js @@ -0,0 +1,174 @@ +const ServiceBuilder = ({ + settings, + version, + logger +}) => { + const retr = {}; + const serviceName = 'plex'; + + const { + setModifiedPorts, + setLoggingState, + setNetworkMode, + setNetworks + } = require('../../../src/utils/commonCompileLogic'); + + const { + checkPortConflicts, + checkNetworkConflicts, + checkDependencyServices + } = require('../../../src/utils/commonBuildChecks'); + + /* + Order: + 1. compile() - merges build options into the final JSON output. + 2. issues() - runs checks on the compile()'ed JSON, and can also test for errors. + 3. assume() - sets required default values if they are not specified in compile(). Once defaults are set, it reruns compile(). This function is optional + 4. build() - sets up scripts and files. + */ + + retr.init = () => { + logger.debug(`ServiceBuilder:init() - '${serviceName}'`); + }; + + const createVolumesDirectory = () => { + return ` +mkdir -p ./volumes/plex/config +mkdir -p ./volumes/plex/transcode +`; + }; + + const checkVolumesDirectory = () => { + return ` +if [[ ! -d ./volumes/plex/config ]]; then + echo "Plex config directory is missing!" + sleep 2 +fi +if [[ ! -d ./volumes/plex/transcode ]]; then + echo "Plex transcode directory is missing!" + sleep 2 +fi +`; + }; + + retr.compile = ({ + outputTemplateJson, + buildOptions, + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:compile() - '${serviceName}' started`); + + const compileResults = { + modifiedPorts: setModifiedPorts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedLogging: setLoggingState({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworkMode: setNetworkMode({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworks: setNetworks({ buildTemplate: outputTemplateJson, buildOptions, serviceName }) + }; + console.info(`ServiceBuilder:compile() - '${serviceName}' Results:`, compileResults); + + console.info(`ServiceBuilder:compile() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + return reject({ + component: `ServiceBuilder::compile() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.issues = ({ + outputTemplateJson, + buildOptions, + tmpPath + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:issues() - '${serviceName}' started`); + let issues = []; + + const portConflicts = checkPortConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...portConflicts]; + + const serviceDependencies = checkDependencyServices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...serviceDependencies]; + + const networkConflicts = checkNetworkConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + if (networkConflicts) { + issues.push(networkConflicts); + } + + console.info(`ServiceBuilder:issues() - '${serviceName}' Issues found: ${issues.length}`); + console.info(`ServiceBuilder:issues() - '${serviceName}' completed`); + return resolve(issues); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::issues() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.build = ({ + outputTemplateJson, + buildOptions, + tmpPath, + zipList, + prebuildScripts, + postbuildScripts + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:build() - '${serviceName}' started`); + + prebuildScripts.push({ + serviceName, + comment: 'Create required service directory exists for first launch', + multilineComment: null, + code: createVolumesDirectory() + }); + + postbuildScripts.push({ + serviceName, + comment: 'Ensure required service directory exists for launch', + multilineComment: null, + code: checkVolumesDirectory() + }); + + console.info(`ServiceBuilder:build() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::build() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + return retr; +} + +module.exports = ServiceBuilder; diff --git a/.internal/templates/services/plex/config.js b/.internal/templates/services/plex/config.js new file mode 100644 index 000000000..995ef1ea3 --- /dev/null +++ b/.internal/templates/services/plex/config.js @@ -0,0 +1,64 @@ +const plex = () => { + const retr = {}; + + const serviceName = 'plex'; + + retr.getConfigOptions = () => { + return { + serviceName, // Required + labeledPorts: { + "32400:32400": 'http', + "1900:1900": 'dlna2', + "5353:5353": 'bonjour', + "8324:8324": 'roku', + "32469:32469": 'dlna1', + "32410:32410": 'gdm1', + "32412:32412": 'gdm2', + "32413:32413": 'gdm3', + "32414:32414": 'gdm4' + }, + modifyableEnvironment: [ + { + key: 'VERSION', + value: 'docker' + } + ], + volumes: true, + networks: true, + logging: true + } + }; + + retr.getHelp = () => { + return { + serviceName, // Required + links: { + "Website": 'https://www.plex.tv/', + "Docker": 'https://hub.docker.com/r/linuxserver/plex', + "Github": 'https://github.com/plexinc', + rawMarkdownRemote: '', // Usually links to github raw help pages. + rawMarkdownLocal: '', // Relative path to docs locally + "IOTstack Documentation for {$displayName}": 'https://sensorsiot.github.io/IOTstack/Containers/Plex/' + } + }; + }; + + retr.getCommands = () => { + return { + commands: {} // Key/value pair of helper commands user can run locally + }; + }; + + retr.getMeta = () => { + return { + serviceName, // Required + displayName: 'Plex', + serviceTypeTags: ['wui', 'video', 'media'], + iconUri: '/logos/plex.png' + }; + }; + + return retr; +}; + +module.exports = plex; diff --git a/.internal/templates/services/plex/template.yml b/.internal/templates/services/plex/template.yml new file mode 100644 index 000000000..36b444521 --- /dev/null +++ b/.internal/templates/services/plex/template.yml @@ -0,0 +1,31 @@ +plex: + image: linuxserver/plex + container_name: plex + network_mode: host + ports: + - "32400:32400" + - "1900:1900" + - "5353:5353" + - "8324:8324" + - "32469:32469" + - "32410:32410" + - "32412:32412" + - "32413:32413" + - "32414:32414" + # networks: + # - hosts_nw + environment: + - PUID=1000 + - PGID=1000 + - VERSION=docker + #- UMASK_SET=022 #optional + volumes: + - ./volumes/plex/config:/config + #- ~/mnt/HDD/tvseries:/tv + #- ~/mnt/HDD/movies:/movies + - ./volumes/plex/transcode:/transcode + restart: unless-stopped + logging: + options: + max-size: "5m" + max-file: "3" diff --git a/.internal/templates/services/portainer_agent/build.js b/.internal/templates/services/portainer_agent/build.js new file mode 100644 index 000000000..be1bff361 --- /dev/null +++ b/.internal/templates/services/portainer_agent/build.js @@ -0,0 +1,140 @@ +const ServiceBuilder = ({ + settings, + version, + logger +}) => { + const retr = {}; + const serviceName = 'portainer_agent'; + + const { + setModifiedPorts, + setLoggingState, + setNetworkMode, + setNetworks + } = require('../../../src/utils/commonCompileLogic'); + + const { + checkPortConflicts, + checkNetworkConflicts, + checkDependencyServices + } = require('../../../src/utils/commonBuildChecks'); + + /* + Order: + 1. compile() - merges build options into the final JSON output. + 2. issues() - runs checks on the compile()'ed JSON, and can also test for errors. + 3. assume() - sets required default values if they are not specified in compile(). Once defaults are set, it reruns compile(). This function is optional + 4. build() - sets up scripts and files. + */ + + retr.init = () => { + logger.debug(`ServiceBuilder:init() - '${serviceName}'`); + }; + + retr.compile = ({ + outputTemplateJson, + buildOptions, + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:compile() - '${serviceName}' started`); + + const compileResults = { + modifiedPorts: setModifiedPorts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedLogging: setLoggingState({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworkMode: setNetworkMode({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworks: setNetworks({ buildTemplate: outputTemplateJson, buildOptions, serviceName }) + }; + console.info(`ServiceBuilder:compile() - '${serviceName}' Results:`, compileResults); + + console.info(`ServiceBuilder:compile() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + return reject({ + component: `ServiceBuilder::compile() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.issues = ({ + outputTemplateJson, + buildOptions, + tmpPath + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:issues() - '${serviceName}' started`); + let issues = []; + + const portConflicts = checkPortConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...portConflicts]; + + const serviceDependencies = checkDependencyServices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...serviceDependencies]; + + const networkConflicts = checkNetworkConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + if (networkConflicts) { + issues.push(networkConflicts); + } + + console.info(`ServiceBuilder:issues() - '${serviceName}' Issues found: ${issues.length}`); + console.info(`ServiceBuilder:issues() - '${serviceName}' completed`); + return resolve(issues); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::issues() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.build = ({ + outputTemplateJson, + buildOptions, + tmpPath, + zipList, + prebuildScripts, + postbuildScripts + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:build() - '${serviceName}' started`); + // Code here + console.info(`ServiceBuilder:build() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::build() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + return retr; +} + +module.exports = ServiceBuilder; diff --git a/.internal/templates/services/portainer_agent/config.js b/.internal/templates/services/portainer_agent/config.js new file mode 100644 index 000000000..c21c6a330 --- /dev/null +++ b/.internal/templates/services/portainer_agent/config.js @@ -0,0 +1,51 @@ +const portainer_agent = () => { + const retr = {}; + + const serviceName = 'portainer_agent'; + + retr.getConfigOptions = () => { + return { + serviceName, // Required + labeledPorts: { + "9001:9001": 'api' + }, + volumes: false, + networks: true, + logging: true + } + }; + + retr.getHelp = () => { + return { + serviceName, // Required + links: { + "Website": 'https://portainer.io/', + "Docker": 'https://hub.docker.com/r/portainer/agent', + "{$displayName} Documentation": 'https://documentation.portainer.io/', + "Community": 'https://membership.portainer.io/', + rawMarkdownRemote: '', // Usually links to github raw help pages. + rawMarkdownLocal: '', // Relative path to docs locally + "IOTstack Documentation for {$displayName}": 'https://sensorsiot.github.io/IOTstack/Containers/Portainer-agent/' + } + }; + }; + + retr.getCommands = () => { + return { + commands: {} // Key/value pair of helper commands user can run locally + }; + }; + + retr.getMeta = () => { + return { + serviceName, // Required + displayName: 'Portainer Agent', + serviceTypeTags: ['container manager', 'docker'], + iconUri: '/logos/portainer.png' + }; + }; + + return retr; +}; + +module.exports = portainer_agent; diff --git a/.internal/templates/services/portainer_agent/template.yml b/.internal/templates/services/portainer_agent/template.yml new file mode 100644 index 000000000..baaccc55a --- /dev/null +++ b/.internal/templates/services/portainer_agent/template.yml @@ -0,0 +1,9 @@ +portainer_agent: + image: portainer/agent + container_name: portainer-agent + ports: + - 9001:9001 + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - /var/lib/docker/volumes:/var/lib/docker/volumes + restart: unless-stopped diff --git a/.internal/templates/services/portainer_ce/build.js b/.internal/templates/services/portainer_ce/build.js new file mode 100644 index 000000000..248942884 --- /dev/null +++ b/.internal/templates/services/portainer_ce/build.js @@ -0,0 +1,169 @@ +const ServiceBuilder = ({ + settings, + version, + logger +}) => { + const retr = {}; + const serviceName = 'portainer_ce'; + + const { + setModifiedPorts, + setLoggingState, + setNetworkMode, + setNetworks + } = require('../../../src/utils/commonCompileLogic'); + + const { + checkPortConflicts, + checkNetworkConflicts, + checkDependencyServices + } = require('../../../src/utils/commonBuildChecks'); + + /* + Order: + 1. compile() - merges build options into the final JSON output. + 2. issues() - runs checks on the compile()'ed JSON, and can also test for errors. + 3. assume() - sets required default values if they are not specified in compile(). Once defaults are set, it reruns compile(). This function is optional + 4. build() - sets up scripts and files. + */ + + retr.init = () => { + logger.debug(`ServiceBuilder:init() - '${serviceName}'`); + }; + + const createVolumesDirectory = () => { + return ` +mkdir -p ./volumes/portainer-ce/data +`; + }; + + const checkVolumesDirectory = () => { + return ` +if [[ ! -d ./volumes/portainer-ce/data ]]; then + echo "Portainer-CE data directory is missing!" + sleep 2 +fi +`; + }; + + retr.compile = ({ + outputTemplateJson, + buildOptions, + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:compile() - '${serviceName}' started`); + + const compileResults = { + modifiedPorts: setModifiedPorts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedLogging: setLoggingState({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworkMode: setNetworkMode({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworks: setNetworks({ buildTemplate: outputTemplateJson, buildOptions, serviceName }) + }; + console.info(`ServiceBuilder:compile() - '${serviceName}' Results:`, compileResults); + + console.info(`ServiceBuilder:compile() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + return reject({ + component: `ServiceBuilder::compile() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.issues = ({ + outputTemplateJson, + buildOptions, + tmpPath + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:issues() - '${serviceName}' started`); + let issues = []; + + const portConflicts = checkPortConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...portConflicts]; + + const serviceDependencies = checkDependencyServices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...serviceDependencies]; + + const networkConflicts = checkNetworkConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + if (networkConflicts) { + issues.push(networkConflicts); + } + + console.info(`ServiceBuilder:issues() - '${serviceName}' Issues found: ${issues.length}`); + console.info(`ServiceBuilder:issues() - '${serviceName}' completed`); + return resolve(issues); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::issues() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.build = ({ + outputTemplateJson, + buildOptions, + tmpPath, + zipList, + prebuildScripts, + postbuildScripts + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:build() - '${serviceName}' started`); + + prebuildScripts.push({ + serviceName, + comment: 'Create required service directory exists for first launch', + multilineComment: null, + code: createVolumesDirectory() + }); + + postbuildScripts.push({ + serviceName, + comment: 'Ensure required service directory exists for launch', + multilineComment: null, + code: checkVolumesDirectory() + }); + + console.info(`ServiceBuilder:build() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::build() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + return retr; +} + +module.exports = ServiceBuilder; diff --git a/.internal/templates/services/portainer_ce/config.js b/.internal/templates/services/portainer_ce/config.js new file mode 100644 index 000000000..3dcbb162d --- /dev/null +++ b/.internal/templates/services/portainer_ce/config.js @@ -0,0 +1,51 @@ +const portainer_ce = () => { + const retr = {}; + + const serviceName = 'portainer_ce'; + + retr.getConfigOptions = () => { + return { + serviceName, // Required + labeledPorts: { + "8000:8000": 'http' + }, + volumes: true, + networks: true, + logging: true + } + }; + + retr.getHelp = () => { + return { + serviceName, // Required + links: { + "Website": 'https://portainer.io/', + "Docker": 'https://hub.docker.com/r/portainer/portainer-ce', + "{$displayName} Documentation": 'https://documentation.portainer.io/', + "Community": 'https://membership.portainer.io/', + rawMarkdownRemote: '', // Usually links to github raw help pages. + rawMarkdownLocal: '', // Relative path to docs locally + "IOTstack Documentation for {$displayName}": 'https://sensorsiot.github.io/IOTstack/Containers/Portainer-ce/' + } + }; + }; + + retr.getCommands = () => { + return { + commands: {} // Key/value pair of helper commands user can run locally + }; + }; + + retr.getMeta = () => { + return { + serviceName, // Required + displayName: 'Portainer-CE', + serviceTypeTags: ['wui', 'container manager', 'docker'], + iconUri: '/logos/portainer.png' + }; + }; + + return retr; +}; + +module.exports = portainer_ce; diff --git a/.templates/portainer-ce/service.yml b/.internal/templates/services/portainer_ce/template.yml similarity index 89% rename from .templates/portainer-ce/service.yml rename to .internal/templates/services/portainer_ce/template.yml index f680bacbf..a0585f85b 100644 --- a/.templates/portainer-ce/service.yml +++ b/.internal/templates/services/portainer_ce/template.yml @@ -5,6 +5,8 @@ portainer-ce: ports: - "8000:8000" - "9000:9000" + # HTTPS + - "9443:9443" volumes: - /var/run/docker.sock:/var/run/docker.sock - ./volumes/portainer-ce/data:/data diff --git a/.internal/templates/services/postgres/build.js b/.internal/templates/services/postgres/build.js new file mode 100644 index 000000000..2fe1a99f6 --- /dev/null +++ b/.internal/templates/services/postgres/build.js @@ -0,0 +1,211 @@ +const ServiceBuilder = ({ + settings, + version, + logger +}) => { + const retr = {}; + const serviceName = 'postgres'; + + const { + setModifiedPorts, + setLoggingState, + setNetworkMode, + setNetworks, + setDevices, + setEnvironmentVariables + } = require('../../../src/utils/commonCompileLogic'); + + const { + checkPortConflicts, + checkNetworkConflicts, + checkDependencyServices + } = require('../../../src/utils/commonBuildChecks'); + + /* + Order: + 1. compile() - merges build options into the final JSON output. + 2. issues() - runs checks on the compile()'ed JSON, and can also test for errors. + 3. assume() - sets required default values if they are not specified in compile(). Once defaults are set, it reruns compile(). This function is optional + 4. build() - sets up scripts and files. + */ + + retr.init = () => { + logger.debug(`ServiceBuilder:init() - '${serviceName}'`); + }; + + const createVolumesDirectory = () => { + return ` +mkdir -p ./volumes/postgres/data +`; + }; + + const checkVolumesDirectory = () => { + return ` +if [[ ! -d ./volumes/postgres/data ]]; then + echo "Postgres data directory is missing!" + sleep 2 +fi +`; + }; + + retr.compile = ({ + outputTemplateJson, + buildOptions, + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:compile() - '${serviceName}' started`); + + const compileResults = { + modifiedPorts: setModifiedPorts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedLogging: setLoggingState({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworkMode: setNetworkMode({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworks: setNetworks({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedEnvironment: setEnvironmentVariables({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedDevices: setDevices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }) + }; + console.info(`ServiceBuilder:compile() - '${serviceName}' Results:`, compileResults); + + console.info(`ServiceBuilder:compile() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + return reject({ + component: `ServiceBuilder::compile() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.issues = ({ + outputTemplateJson, + buildOptions, + tmpPath + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:issues() - '${serviceName}' started`); + let issues = []; + + const portConflicts = checkPortConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...portConflicts]; + + const serviceDependencies = checkDependencyServices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...serviceDependencies]; + + const networkConflicts = checkNetworkConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + if (networkConflicts) { + issues.push(networkConflicts); + } + + const environmentList = outputTemplateJson?.services?.[serviceName]?.environment ?? []; + if (Array.isArray(environmentList) && environmentList.length > 0) { + let pwKeysFound = false; + environmentList.forEach((envKVP) => { + const envKey = envKVP.split('=')[0]; + const envValue = envKVP.split('=')[1]; + if (envKey && envValue) { + if (envKey === 'POSTGRES_PASSWORD') { + pwKeysFound = true; + if (envValue === 'Unset' || envValue === 'Unset') { + issues.push({ + type: 'service', + name: serviceName, + issueType: 'environment', + message: `Ensure database passwords are set in environment variables.` + }); + } + } + } + }); + + if (!pwKeysFound) { + issues.push({ + type: 'service', + name: serviceName, + issueType: 'environment', + message: `No environment variables found. Database may not start unless they are set.` + }); + } + } else { + issues.push({ + type: 'service', + name: serviceName, + issueType: 'environment', + message: `No environment variables found. Database may not start unless they are set.` + }); + } + + console.info(`ServiceBuilder:issues() - '${serviceName}' Issues found: ${issues.length}`); + console.info(`ServiceBuilder:issues() - '${serviceName}' completed`); + return resolve(issues); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::issues() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.build = ({ + outputTemplateJson, + buildOptions, + tmpPath, + zipList, + prebuildScripts, + postbuildScripts + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:build() - '${serviceName}' started`); + + prebuildScripts.push({ + serviceName, + comment: 'Create required service directory exists for first launch', + multilineComment: null, + code: createVolumesDirectory() + }); + + postbuildScripts.push({ + serviceName, + comment: 'Ensure required service directory exists for launch', + multilineComment: null, + code: checkVolumesDirectory() + }); + + console.info(`ServiceBuilder:build() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::build() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + return retr; +} + +module.exports = ServiceBuilder; diff --git a/.internal/templates/services/postgres/config.js b/.internal/templates/services/postgres/config.js new file mode 100644 index 000000000..2b65e269b --- /dev/null +++ b/.internal/templates/services/postgres/config.js @@ -0,0 +1,61 @@ +const postgres = () => { + const retr = {}; + + const serviceName = 'postgres'; + + retr.getConfigOptions = () => { + return { + serviceName, // Required + modifyableEnvironment: [ + { + key: 'POSTGRES_USER', + value: 'postuser' + }, + { + key: 'POSTGRES_PASSWORD', + value: '{$randomPassword}' + }, + { + key: 'POSTGRES_DB', + value: 'postdb' + } + ], + volumes: true, + networks: true, + logging: true + } + }; + + retr.getHelp = () => { + return { + serviceName, // Required + links: { + "Website": 'https://www.postgresql.org/', + "Docker": 'https://hub.docker.com/_/postgres', + "Source Code": 'https://github.com/docker-library/postgres/', + rawMarkdownRemote: '', // Usually links to github raw help pages. + rawMarkdownLocal: '', // Relative path to docs locally + "IOTstack Documentation for {$displayName}": 'https://sensorsiot.github.io/IOTstack/Containers/PostgreSQL/' + } + }; + }; + + retr.getCommands = () => { + return { + commands: {} // Key/value pair of helper commands user can run locally + }; + }; + + retr.getMeta = () => { + return { + serviceName, // Required + displayName: 'Postgres', + serviceTypeTags: ['database'], + iconUri: '/logos/postgres.png' + }; + }; + + return retr; +}; + +module.exports = postgres; diff --git a/.internal/templates/services/postgres/template.yml b/.internal/templates/services/postgres/template.yml new file mode 100644 index 000000000..96465cc85 --- /dev/null +++ b/.internal/templates/services/postgres/template.yml @@ -0,0 +1,18 @@ +postgres: + container_name: postgres + image: postgres + restart: unless-stopped + environment: + - TZ=${TZ:-Etc/UTC} + - POSTGRES_USER=${POSTGRES_USER:-postuser} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-IOtSt4ckpostgresDbPw} + - POSTGRES_DB=${POSTGRES_DB:-postdb} + ports: + - "5432:5432" + volumes: + - ./volumes/postgres/data:/var/lib/postgresql/data + - ./volumes/postgres/db_backup:/backup + logging: + options: + max-size: "5m" + max-file: "3" diff --git a/.internal/templates/services/prometheus/build.js b/.internal/templates/services/prometheus/build.js new file mode 100644 index 000000000..df6956074 --- /dev/null +++ b/.internal/templates/services/prometheus/build.js @@ -0,0 +1,140 @@ +const ServiceBuilder = ({ + settings, + version, + logger +}) => { + const retr = {}; + const serviceName = 'prometheus'; + + const { + setModifiedPorts, + setLoggingState, + setNetworkMode, + setNetworks + } = require('../../../src/utils/commonCompileLogic'); + + const { + checkPortConflicts, + checkNetworkConflicts, + checkDependencyServices + } = require('../../../src/utils/commonBuildChecks'); + + /* + Order: + 1. compile() - merges build options into the final JSON output. + 2. issues() - runs checks on the compile()'ed JSON, and can also test for errors. + 3. assume() - sets required default values if they are not specified in compile(). Once defaults are set, it reruns compile(). This function is optional + 4. build() - sets up scripts and files. + */ + + retr.init = () => { + logger.debug(`ServiceBuilder:init() - '${serviceName}'`); + }; + + retr.compile = ({ + outputTemplateJson, + buildOptions, + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:compile() - '${serviceName}' started`); + + const compileResults = { + modifiedPorts: setModifiedPorts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedLogging: setLoggingState({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworkMode: setNetworkMode({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworks: setNetworks({ buildTemplate: outputTemplateJson, buildOptions, serviceName }) + }; + console.info(`ServiceBuilder:compile() - '${serviceName}' Results:`, compileResults); + + console.info(`ServiceBuilder:compile() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + return reject({ + component: `ServiceBuilder::compile() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.issues = ({ + outputTemplateJson, + buildOptions, + tmpPath + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:issues() - '${serviceName}' started`); + let issues = []; + + const portConflicts = checkPortConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...portConflicts]; + + const serviceDependencies = checkDependencyServices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...serviceDependencies]; + + const networkConflicts = checkNetworkConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + if (networkConflicts) { + issues.push(networkConflicts); + } + + console.info(`ServiceBuilder:issues() - '${serviceName}' Issues found: ${issues.length}`); + console.info(`ServiceBuilder:issues() - '${serviceName}' completed`); + return resolve(issues); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::issues() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.build = ({ + outputTemplateJson, + buildOptions, + tmpPath, + zipList, + prebuildScripts, + postbuildScripts + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:build() - '${serviceName}' started`); + // Code here + console.info(`ServiceBuilder:build() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::build() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + return retr; +} + +module.exports = ServiceBuilder; diff --git a/.internal/templates/services/prometheus/buildFiles/Dockerfile b/.internal/templates/services/prometheus/buildFiles/Dockerfile new file mode 100644 index 000000000..e39404b43 --- /dev/null +++ b/.internal/templates/services/prometheus/buildFiles/Dockerfile @@ -0,0 +1,25 @@ +# Download base image +FROM prom/prometheus:latest + +USER root + +# where IOTstack template files are stored +ENV IOTSTACK_DEFAULTS_DIR="iotstack_defaults" +ENV IOTSTACK_CONFIG_DIR="/prometheus/config/" + +# copy template files to image +COPY --chown=nobody:nobody ${IOTSTACK_DEFAULTS_DIR} /${IOTSTACK_DEFAULTS_DIR} + +# add default config from image to template +RUN cp /etc/prometheus/prometheus.yml /${IOTSTACK_DEFAULTS_DIR} + +# replace the docker entry-point script +ENV IOTSTACK_ENTRY_POINT="docker-entrypoint.sh" +COPY ${IOTSTACK_ENTRY_POINT} /${IOTSTACK_ENTRY_POINT} +RUN chmod 755 /${IOTSTACK_ENTRY_POINT} +ENTRYPOINT ["/docker-entrypoint.sh"] +ENV IOTSTACK_ENTRY_POINT= + +USER nobody + +# EOF diff --git a/.internal/templates/services/prometheus/buildFiles/docker-entrypoint.sh b/.internal/templates/services/prometheus/buildFiles/docker-entrypoint.sh new file mode 100644 index 000000000..3c9cc7c91 --- /dev/null +++ b/.internal/templates/services/prometheus/buildFiles/docker-entrypoint.sh @@ -0,0 +1,33 @@ +#!/bin/ash +set -e + +# set defaults for config structure ownership +UID="${IOTSTACK_UID:-nobody}" +GID="${IOTSTACK_GID:-nobody}" + +# were we launched as root? +if [ "$(id -u)" = "0" -a -d /"$IOTSTACK_DEFAULTS_DIR" ]; then + + # yes! ensure that the IOTSTACK_CONFIG_DIR exists + mkdir -p "$IOTSTACK_CONFIG_DIR" + + # populate runtime directory from the defaults + for P in /"$IOTSTACK_DEFAULTS_DIR"/* ; do + + C=$(basename "$P") + + if [ ! -e "$IOTSTACK_CONFIG_DIR/$C" ] ; then + + cp -a "$P" "$IOTSTACK_CONFIG_DIR/$C" + + fi + + done + + # enforce correct ownership + chown -R "$UID":"$GID" "$IOTSTACK_CONFIG_DIR" + +fi + +# launch prometheus with supplied arguments +exec /bin/prometheus "$@" diff --git a/.internal/templates/services/prometheus/buildFiles/iotstack_defaults/config.yml b/.internal/templates/services/prometheus/buildFiles/iotstack_defaults/config.yml new file mode 100644 index 000000000..989e5d6a4 --- /dev/null +++ b/.internal/templates/services/prometheus/buildFiles/iotstack_defaults/config.yml @@ -0,0 +1,12 @@ +global: + scrape_interval: 10s + evaluation_interval: 10s + +scrape_configs: + - job_name: "iotstack" + static_configs: + - targets: + - localhost:9090 + - prometheus-cadvisor:8080 + - prometheus-nodeexporter:9100 + diff --git a/.internal/templates/services/prometheus/template.yml b/.internal/templates/services/prometheus/template.yml new file mode 100644 index 000000000..209189984 --- /dev/null +++ b/.internal/templates/services/prometheus/template.yml @@ -0,0 +1,47 @@ +prometheus: + container_name: prometheus + build: ./.templates/prometheus/. + restart: unless-stopped + user: "0" + ports: + - "9090:9090" + environment: + - IOTSTACK_UID=1000 + - IOTSTACK_GID=1000 + volumes: + - ./volumes/prometheus/data:/prometheus + command: + - '--config.file=/prometheus/config/config.yml' + # defaults are: + # - --config.file=/etc/prometheus/prometheus.yml + # - --storage.tsdb.path=/prometheus + # - --web.console.libraries=/usr/share/prometheus/console_libraries + # - --web.console.templates=/usr/share/prometheus/consoles + depends_on: + - cadvisor + - nodeexporter + networks: + - iotstack_nw + +cadvisor: + container_name: cadvisor + image: zcube/cadvisor:latest + restart: unless-stopped + ports: + - "8082:8080" + volumes: + - /:/rootfs:ro + - /var/run:/var/run:rw + - /sys:/sys:ro + - /var/lib/docker/:/var/lib/docker:ro + networks: + - iotstack_nw + +nodeexporter: + container_name: nodeexporter + image: prom/node-exporter:latest + restart: unless-stopped + expose: + - "9100" + networks: + - iotstack_nw diff --git a/.internal/templates/services/qbittorrent/build.js b/.internal/templates/services/qbittorrent/build.js new file mode 100644 index 000000000..724a6be2e --- /dev/null +++ b/.internal/templates/services/qbittorrent/build.js @@ -0,0 +1,174 @@ +const ServiceBuilder = ({ + settings, + version, + logger +}) => { + const retr = {}; + const serviceName = 'qbittorrent'; + + const { + setModifiedPorts, + setLoggingState, + setNetworkMode, + setNetworks + } = require('../../../src/utils/commonCompileLogic'); + + const { + checkPortConflicts, + checkNetworkConflicts, + checkDependencyServices + } = require('../../../src/utils/commonBuildChecks'); + + /* + Order: + 1. compile() - merges build options into the final JSON output. + 2. issues() - runs checks on the compile()'ed JSON, and can also test for errors. + 3. assume() - sets required default values if they are not specified in compile(). Once defaults are set, it reruns compile(). This function is optional + 4. build() - sets up scripts and files. + */ + + retr.init = () => { + logger.debug(`ServiceBuilder:init() - '${serviceName}'`); + }; + + const createVolumesDirectory = () => { + return ` +mkdir -p ./volumes/qbittorrent/config +mkdir -p ./volumes/qbittorrent/downloads +`; + }; + + const checkVolumesDirectory = () => { + return ` +if [[ ! -d ./volumes/qbittorrent/config ]]; then + echo "qBittorrent config directory is missing!" + sleep 2 +fi +if [[ ! -d ./volumes/qbittorrent/downloads ]]; then + echo "qBittorrent downloads directory is missing!" + sleep 2 +fi +`; + }; + + retr.compile = ({ + outputTemplateJson, + buildOptions, + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:compile() - '${serviceName}' started`); + + const compileResults = { + modifiedPorts: setModifiedPorts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedLogging: setLoggingState({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworkMode: setNetworkMode({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworks: setNetworks({ buildTemplate: outputTemplateJson, buildOptions, serviceName }) + }; + console.info(`ServiceBuilder:compile() - '${serviceName}' Results:`, compileResults); + + console.info(`ServiceBuilder:compile() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + return reject({ + component: `ServiceBuilder::compile() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.issues = ({ + outputTemplateJson, + buildOptions, + tmpPath + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:issues() - '${serviceName}' started`); + let issues = []; + + const portConflicts = checkPortConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...portConflicts]; + + const serviceDependencies = checkDependencyServices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...serviceDependencies]; + + const networkConflicts = checkNetworkConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + if (networkConflicts) { + issues.push(networkConflicts); + } + + console.info(`ServiceBuilder:issues() - '${serviceName}' Issues found: ${issues.length}`); + console.info(`ServiceBuilder:issues() - '${serviceName}' completed`); + return resolve(issues); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::issues() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.build = ({ + outputTemplateJson, + buildOptions, + tmpPath, + zipList, + prebuildScripts, + postbuildScripts + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:build() - '${serviceName}' started`); + + prebuildScripts.push({ + serviceName, + comment: 'Create required service directory exists for first launch', + multilineComment: null, + code: createVolumesDirectory() + }); + + postbuildScripts.push({ + serviceName, + comment: 'Ensure required service directory exists for launch', + multilineComment: null, + code: checkVolumesDirectory() + }); + + console.info(`ServiceBuilder:build() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::build() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + return retr; +} + +module.exports = ServiceBuilder; diff --git a/.internal/templates/services/qbittorrent/config.js b/.internal/templates/services/qbittorrent/config.js new file mode 100644 index 000000000..6ccbd6818 --- /dev/null +++ b/.internal/templates/services/qbittorrent/config.js @@ -0,0 +1,55 @@ +const qbittorrent = () => { + const retr = {}; + + const serviceName = 'qbittorrent'; + + retr.getConfigOptions = () => { + return { + serviceName, // Required + labeledPorts: { + "15080:15080": 'http' + }, + modifyableEnvironment: [ + { + key: 'WEBUI_PORT', + value: '15080' + } + ], + volumes: true, + networks: true, + logging: true + } + }; + + retr.getHelp = () => { + return { + serviceName, // Required + links: { + "Docker": 'https://hub.docker.com/r/linuxserver/qbittorrent', + "Source Code": 'https://github.com/qbittorrent/qBittorrent', + rawMarkdownRemote: '', // Usually links to github raw help pages. + rawMarkdownLocal: '', // Relative path to docs locally + "IOTstack Documentation for {$displayName}": 'https://sensorsiot.github.io/IOTstack/Containers/qBittorrent/' + } + }; + }; + + retr.getCommands = () => { + return { + commands: {} // Key/value pair of helper commands user can run locally + }; + }; + + retr.getMeta = () => { + return { + serviceName, // Required + displayName: 'Q Bittorrent', + serviceTypeTags: ['bittorrent', 'wui'], + iconUri: '/logos/qbittorrent.svg' + }; + }; + + return retr; +}; + +module.exports = qbittorrent; diff --git a/.internal/templates/services/qbittorrent/template.yml b/.internal/templates/services/qbittorrent/template.yml new file mode 100644 index 000000000..c04a0bb05 --- /dev/null +++ b/.internal/templates/services/qbittorrent/template.yml @@ -0,0 +1,16 @@ +qbittorrent: + image: linuxserver/qbittorrent + container_name: qbittorrent + environment: + - PUID=1000 + - PGID=1000 + - UMASK_SET=022 + - WEBUI_PORT=15080 + volumes: + - ./volumes/qbittorrent/config:/config + - ./volumes/qbittorrent/downloads:/downloads + ports: + - "15080:15080" + - "6881:6881" + - "6881:6881/udp" + - "1080:1080" diff --git a/.internal/templates/services/redis/build.js b/.internal/templates/services/redis/build.js new file mode 100644 index 000000000..e1fe82940 --- /dev/null +++ b/.internal/templates/services/redis/build.js @@ -0,0 +1,140 @@ +const ServiceBuilder = ({ + settings, + version, + logger +}) => { + const retr = {}; + const serviceName = 'redis'; + + const { + setModifiedPorts, + setLoggingState, + setNetworkMode, + setNetworks + } = require('../../../src/utils/commonCompileLogic'); + + const { + checkPortConflicts, + checkNetworkConflicts, + checkDependencyServices + } = require('../../../src/utils/commonBuildChecks'); + + /* + Order: + 1. compile() - merges build options into the final JSON output. + 2. issues() - runs checks on the compile()'ed JSON, and can also test for errors. + 3. assume() - sets required default values if they are not specified in compile(). Once defaults are set, it reruns compile(). This function is optional + 4. build() - sets up scripts and files. + */ + + retr.init = () => { + logger.debug(`ServiceBuilder:init() - '${serviceName}'`); + }; + + retr.compile = ({ + outputTemplateJson, + buildOptions, + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:compile() - '${serviceName}' started`); + + const compileResults = { + modifiedPorts: setModifiedPorts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedLogging: setLoggingState({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworkMode: setNetworkMode({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworks: setNetworks({ buildTemplate: outputTemplateJson, buildOptions, serviceName }) + }; + console.info(`ServiceBuilder:compile() - '${serviceName}' Results:`, compileResults); + + console.info(`ServiceBuilder:compile() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + return reject({ + component: `ServiceBuilder::compile() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.issues = ({ + outputTemplateJson, + buildOptions, + tmpPath + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:issues() - '${serviceName}' started`); + let issues = []; + + const portConflicts = checkPortConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...portConflicts]; + + const serviceDependencies = checkDependencyServices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...serviceDependencies]; + + const networkConflicts = checkNetworkConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + if (networkConflicts) { + issues.push(networkConflicts); + } + + console.info(`ServiceBuilder:issues() - '${serviceName}' Issues found: ${issues.length}`); + console.info(`ServiceBuilder:issues() - '${serviceName}' completed`); + return resolve(issues); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::issues() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.build = ({ + outputTemplateJson, + buildOptions, + tmpPath, + zipList, + prebuildScripts, + postbuildScripts + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:build() - '${serviceName}' started`); + + console.info(`ServiceBuilder:build() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::build() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + return retr; +} + +module.exports = ServiceBuilder; diff --git a/.internal/templates/services/redis/config.js b/.internal/templates/services/redis/config.js new file mode 100644 index 000000000..f27e48dd3 --- /dev/null +++ b/.internal/templates/services/redis/config.js @@ -0,0 +1,59 @@ +const pihole = () => { + const retr = {}; + + const serviceName = 'redis'; + + retr.getConfigOptions = () => { + return { + serviceName, // Required + labeledPorts: { + "6379:6379": 'tcp' + }, + modifyableEnvironment: [ + { + key: 'TZ', + value: 'Etc/UTC' + } + ], + volumes: true, + networks: true, + logging: true + } + }; + + retr.getHelp = () => { + return { + serviceName, // Required + links: { + "Website": 'https://redis.io/', + "Docker": 'https://hub.docker.com/_/redis', + "Source Code": 'https://github.com/redis/redis', + "{$displayName} Documentation": 'https://redis.io/documentation', + "Community": 'https://redis.io/community', + "Community Chat (Discord)": 'https://discord.gg/redis', + rawMarkdownRemote: '', // Usually links to github raw help pages. + rawMarkdownLocal: '', // Relative path to docs locally + "IOTstack Documentation for {$displayName}": 'https://sensorsiot.github.io/IOTstack/Containers/Redis/' + } + }; + }; + + retr.getCommands = () => { + return { + commands: {} // Key/value pair of helper commands user can run locally + }; + }; + + retr.getMeta = () => { + return { + serviceName, // Required + displayName: 'Redis', + serviceTypeTags: ['database', 'cache'], + iconUri: '/logos/redis.png' + }; + }; + + return retr; +}; + +module.exports = pihole; diff --git a/.internal/templates/services/redis/template.yml b/.internal/templates/services/redis/template.yml new file mode 100644 index 000000000..03533a419 --- /dev/null +++ b/.internal/templates/services/redis/template.yml @@ -0,0 +1,14 @@ +redis: + container_name: redis + image: redis:latest + ports: + - "6379:6379" + environment: + - TZ=Etc/UTC + restart: unless-stopped + networks: + - iotstack_nw + logging: + options: + max-size: "5m" + max-file: "3" diff --git a/.internal/templates/services/ring-mqtt/template.yml b/.internal/templates/services/ring-mqtt/template.yml new file mode 100644 index 000000000..a495afcde --- /dev/null +++ b/.internal/templates/services/ring-mqtt/template.yml @@ -0,0 +1,17 @@ +ring-mqtt: + container_name: ring-mqtt + image: tsightler/ring-mqtt + restart: unless-stopped + environment: + - TZ=Etc/UTC + - DEBUG=ring-* + ports: + - 8554:8554 + - 55123:55123 + volumes: + - ./volumes/ring-mqtt/data:/data + logging: + options: + max-size: 10m + max-file: "3" + diff --git a/.internal/templates/services/rtl_433/build.js b/.internal/templates/services/rtl_433/build.js new file mode 100644 index 000000000..37b0f630b --- /dev/null +++ b/.internal/templates/services/rtl_433/build.js @@ -0,0 +1,188 @@ +const fs = require('fs'); +const path = require('path'); + +const ServiceBuilder = ({ + settings, + version, + logger +}) => { + const retr = {}; + const serviceName = 'rtl_433'; + + const { + setModifiedPorts, + setLoggingState, + setNetworkMode, + setNetworks + } = require('../../../src/utils/commonCompileLogic'); + + const { + checkPortConflicts, + checkNetworkConflicts, + checkDependencyServices + } = require('../../../src/utils/commonBuildChecks'); + + /* + Order: + 1. compile() - merges build options into the final JSON output. + 2. issues() - runs checks on the compile()'ed JSON, and can also test for errors. + 3. assume() - sets required default values if they are not specified in compile(). Once defaults are set, it reruns compile(). This function is optional + 4. build() - sets up scripts and files. + */ + + retr.init = () => { + logger.debug(`ServiceBuilder:init() - '${serviceName}'`); + }; + + const createVolumesDirectory = () => { + return ` +mkdir -p ./volumes/rtl_433 +`; + }; + + const checkVolumesDirectory = () => { + return ` +if [[ ! -d ./volumes/rtl_433 ]]; then + echo "rtl_433 directory is missing!" + sleep 2 +fi +`; + }; + + retr.compile = ({ + outputTemplateJson, + buildOptions, + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:compile() - '${serviceName}' started`); + + const compileResults = { + modifiedPorts: setModifiedPorts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedLogging: setLoggingState({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworkMode: setNetworkMode({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworks: setNetworks({ buildTemplate: outputTemplateJson, buildOptions, serviceName }) + }; + console.info(`ServiceBuilder:compile() - '${serviceName}' Results:`, compileResults); + + console.info(`ServiceBuilder:compile() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + return reject({ + component: `ServiceBuilder::compile() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.issues = ({ + outputTemplateJson, + buildOptions, + tmpPath + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:issues() - '${serviceName}' started`); + let issues = []; + + const portConflicts = checkPortConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...portConflicts]; + + const serviceDependencies = checkDependencyServices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...serviceDependencies]; + + const networkConflicts = checkNetworkConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + if (networkConflicts) { + issues.push(networkConflicts); + } + + console.info(`ServiceBuilder:issues() - '${serviceName}' Issues found: ${issues.length}`); + console.info(`ServiceBuilder:issues() - '${serviceName}' completed`); + return resolve(issues); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::issues() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.build = ({ + outputTemplateJson, + fileTimePrefix, + buildOptions, + tmpPath, + zipList, + prebuildScripts, + postbuildScripts + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:build() - '${serviceName}' started`); + const noderedDockerfileTemplate = path.join(__dirname, settings.paths.buildFiles, 'Dockerfile.template'); + const templateData = fs.readFileSync(noderedDockerfileTemplate, { encoding: 'utf8', flag: 'r' }); + const tempDockerfileName = `${fileTimePrefix}_Dockerfile.template`; + + const outputDockerFile = templateData; // Replace this with any changes to the template file. + + const tempBuildFile = path.join(tmpPath, tempDockerfileName); + + fs.writeFileSync(tempBuildFile, outputDockerFile); + + zipList.push({ + fullPath: tempBuildFile, + zipName: `/services/${serviceName}/Dockerfile` + }); + + prebuildScripts.push({ + serviceName, + comment: 'Create required service directory exists for first launch', + multilineComment: null, + code: createVolumesDirectory() + }); + + postbuildScripts.push({ + serviceName, + comment: 'Ensure required service directory exists for launch', + multilineComment: null, + code: checkVolumesDirectory() + }); + + // Code here + console.info(`ServiceBuilder:build() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::build() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + return retr; +} + +module.exports = ServiceBuilder; diff --git a/.templates/rtl_433/Dockerfile b/.internal/templates/services/rtl_433/buildFiles/Dockerfile.template similarity index 70% rename from .templates/rtl_433/Dockerfile rename to .internal/templates/services/rtl_433/buildFiles/Dockerfile.template index bab136671..6eac9e040 100644 --- a/.templates/rtl_433/Dockerfile +++ b/.internal/templates/services/rtl_433/buildFiles/Dockerfile.template @@ -8,12 +8,12 @@ ENV MQTT_TOPIC RTL_433 ENV RTL_PARAMS "-C si" RUN apt-get update && apt-get install -y git libtool libusb-1.0.0-dev librtlsdr-dev rtl-sdr cmake automake && \ - git clone https://github.com/merbanan/rtl_433.git /tmp/rtl_433 && \ - cd /tmp/rtl_433/ && \ - mkdir build && \ - cd build && \ - cmake ../ && \ - make && \ - make install + git clone https://github.com/merbanan/rtl_433.git /tmp/rtl_433 && \ + cd /tmp/rtl_433/ && \ + mkdir build && \ + cd build && \ + cmake ../ && \ + make && \ + make install CMD ["sh", "-c", "rtl_433 ${RTL_PARAMS} -F mqtt://${MQTT_ADDRESS}:${MQTT_PORT},events=${MQTT_TOPIC},user=${MQTT_USER},pass=${MQTT_PASSWORD}"] diff --git a/.internal/templates/services/rtl_433/config.js b/.internal/templates/services/rtl_433/config.js new file mode 100644 index 000000000..195601944 --- /dev/null +++ b/.internal/templates/services/rtl_433/config.js @@ -0,0 +1,44 @@ +const rtl_433 = () => { + const retr = {}; + + const serviceName = 'rtl_433'; + + retr.getConfigOptions = () => { + return { + serviceName, // Required + volumes: false, + networks: false, + logging: true + } + }; + + retr.getHelp = () => { + return { + serviceName, // Required + links: { + "Source Code": 'https://github.com/merbanan/rtl_433', + rawMarkdownRemote: '', // Usually links to github raw help pages. + rawMarkdownLocal: '', // Relative path to docs locally + "IOTstack Documentation for {$displayName}": 'https://sensorsiot.github.io/IOTstack/Containers/RTL_433-docker/' + } + }; + }; + + retr.getCommands = () => { + return { + commands: {} // Key/value pair of helper commands user can run locally + }; + }; + + retr.getMeta = () => { + return { + serviceName, // Required + displayName: 'RTL 433 (untested)', + serviceTypeTags: ['wui', 'database manager'] + }; + }; + + return retr; +}; + +module.exports = rtl_433; diff --git a/.templates/rtl_433/service.yml b/.internal/templates/services/rtl_433/template.yml similarity index 100% rename from .templates/rtl_433/service.yml rename to .internal/templates/services/rtl_433/template.yml diff --git a/.internal/templates/services/tasmoadmin/build.js b/.internal/templates/services/tasmoadmin/build.js new file mode 100644 index 000000000..29d194cce --- /dev/null +++ b/.internal/templates/services/tasmoadmin/build.js @@ -0,0 +1,169 @@ +const ServiceBuilder = ({ + settings, + version, + logger +}) => { + const retr = {}; + const serviceName = 'tasmoadmin'; + + const { + setModifiedPorts, + setLoggingState, + setNetworkMode, + setNetworks + } = require('../../../src/utils/commonCompileLogic'); + + const { + checkPortConflicts, + checkNetworkConflicts, + checkDependencyServices + } = require('../../../src/utils/commonBuildChecks'); + + /* + Order: + 1. compile() - merges build options into the final JSON output. + 2. issues() - runs checks on the compile()'ed JSON, and can also test for errors. + 3. assume() - sets required default values if they are not specified in compile(). Once defaults are set, it reruns compile(). This function is optional + 4. build() - sets up scripts and files. + */ + + retr.init = () => { + logger.debug(`ServiceBuilder:init() - '${serviceName}'`); + }; + + const createVolumesDirectory = () => { + return ` +mkdir -p ./volumes/tasmoadmin/data +`; + }; + + const checkVolumesDirectory = () => { + return ` +if [[ ! -d ./volumes/tasmoadmin/data ]]; then + echo "TasmoAdmin data directory is missing!" + sleep 2 +fi +`; + }; + + retr.compile = ({ + outputTemplateJson, + buildOptions, + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:compile() - '${serviceName}' started`); + + const compileResults = { + modifiedPorts: setModifiedPorts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedLogging: setLoggingState({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworkMode: setNetworkMode({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworks: setNetworks({ buildTemplate: outputTemplateJson, buildOptions, serviceName }) + }; + console.info(`ServiceBuilder:compile() - '${serviceName}' Results:`, compileResults); + + console.info(`ServiceBuilder:compile() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + return reject({ + component: `ServiceBuilder::compile() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.issues = ({ + outputTemplateJson, + buildOptions, + tmpPath + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:issues() - '${serviceName}' started`); + let issues = []; + + const portConflicts = checkPortConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...portConflicts]; + + const serviceDependencies = checkDependencyServices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...serviceDependencies]; + + const networkConflicts = checkNetworkConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + if (networkConflicts) { + issues.push(networkConflicts); + } + + console.info(`ServiceBuilder:issues() - '${serviceName}' Issues found: ${issues.length}`); + console.info(`ServiceBuilder:issues() - '${serviceName}' completed`); + return resolve(issues); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::issues() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.build = ({ + outputTemplateJson, + buildOptions, + tmpPath, + zipList, + prebuildScripts, + postbuildScripts + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:build() - '${serviceName}' started`); + + prebuildScripts.push({ + serviceName, + comment: 'Create required service directory exists for first launch', + multilineComment: null, + code: createVolumesDirectory() + }); + + postbuildScripts.push({ + serviceName, + comment: 'Ensure required service directory exists for launch', + multilineComment: null, + code: checkVolumesDirectory() + }); + + console.info(`ServiceBuilder:build() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::build() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + return retr; +} + +module.exports = ServiceBuilder; diff --git a/.internal/templates/services/tasmoadmin/config.js b/.internal/templates/services/tasmoadmin/config.js new file mode 100644 index 000000000..0d63f5582 --- /dev/null +++ b/.internal/templates/services/tasmoadmin/config.js @@ -0,0 +1,49 @@ +const tasmoadmin = () => { + const retr = {}; + + const serviceName = 'tasmoadmin'; + + retr.getConfigOptions = () => { + return { + serviceName, // Required + labeledPorts: { + "8088:80": 'http' + }, + volumes: true, + networks: false, + logging: true + } + }; + + retr.getHelp = () => { + return { + serviceName, // Required + links: { + "Docker": 'https://hub.docker.com/r/raymondmm/tasmoadmin', + "Source Code": 'https://github.com/reloxx13/TasmoAdmin', + rawMarkdownRemote: '', // Usually links to github raw help pages. + rawMarkdownLocal: '', // Relative path to docs locally + "IOTstack Documentation for {$displayName}": 'https://sensorsiot.github.io/IOTstack/Containers/TasmoAdmin/' + } + }; + }; + + retr.getCommands = () => { + return { + commands: {} // Key/value pair of helper commands user can run locally + }; + }; + + retr.getMeta = () => { + return { + serviceName, // Required + displayName: 'TasmoAdmin', + serviceTypeTags: ['wui', 'iot'], + iconUri: '/logos/tasmoadmin.png' + }; + }; + + return retr; +}; + +module.exports = tasmoadmin; diff --git a/.internal/templates/services/tasmoadmin/template.yml b/.internal/templates/services/tasmoadmin/template.yml new file mode 100644 index 000000000..04212491a --- /dev/null +++ b/.internal/templates/services/tasmoadmin/template.yml @@ -0,0 +1,14 @@ +tasmoadmin: + container_name: tasmoadmin + image: ghcr.io/tasmoadmin/tasmoadmin:latest + restart: unless-stopped + environment: + - TZ=Etc/UTC + ports: + - "8088:80" + volumes: + - ./volumes/tasmoadmin/data:/data + logging: + options: + max-size: "5m" + max-file: "3" diff --git a/.internal/templates/services/telegraf/build.js b/.internal/templates/services/telegraf/build.js new file mode 100644 index 000000000..c0df6ecf6 --- /dev/null +++ b/.internal/templates/services/telegraf/build.js @@ -0,0 +1,140 @@ +const ServiceBuilder = ({ + settings, + version, + logger +}) => { + const retr = {}; + const serviceName = 'telegraf'; + + const { + setModifiedPorts, + setLoggingState, + setNetworkMode, + setNetworks + } = require('../../../src/utils/commonCompileLogic'); + + const { + checkPortConflicts, + checkNetworkConflicts, + checkDependencyServices + } = require('../../../src/utils/commonBuildChecks'); + + /* + Order: + 1. compile() - merges build options into the final JSON output. + 2. issues() - runs checks on the compile()'ed JSON, and can also test for errors. + 3. assume() - sets required default values if they are not specified in compile(). Once defaults are set, it reruns compile(). This function is optional + 4. build() - sets up scripts and files. + */ + + retr.init = () => { + logger.debug(`ServiceBuilder:init() - '${serviceName}'`); + }; + + retr.compile = ({ + outputTemplateJson, + buildOptions, + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:compile() - '${serviceName}' started`); + + const compileResults = { + modifiedPorts: setModifiedPorts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedLogging: setLoggingState({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworkMode: setNetworkMode({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworks: setNetworks({ buildTemplate: outputTemplateJson, buildOptions, serviceName }) + }; + console.info(`ServiceBuilder:compile() - '${serviceName}' Results:`, compileResults); + + console.info(`ServiceBuilder:compile() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + return reject({ + component: `ServiceBuilder::compile() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.issues = ({ + outputTemplateJson, + buildOptions, + tmpPath + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:issues() - '${serviceName}' started`); + let issues = []; + + const portConflicts = checkPortConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...portConflicts]; + + const serviceDependencies = checkDependencyServices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...serviceDependencies]; + + const networkConflicts = checkNetworkConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + if (networkConflicts) { + issues.push(networkConflicts); + } + + console.info(`ServiceBuilder:issues() - '${serviceName}' Issues found: ${issues.length}`); + console.info(`ServiceBuilder:issues() - '${serviceName}' completed`); + return resolve(issues); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::issues() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.build = ({ + outputTemplateJson, + buildOptions, + tmpPath, + zipList, + prebuildScripts, + postbuildScripts + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:build() - '${serviceName}' started`); + // Code here + console.info(`ServiceBuilder:build() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::build() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + return retr; +} + +module.exports = ServiceBuilder; diff --git a/.internal/templates/services/telegraf/buildFiles/Dockerfile b/.internal/templates/services/telegraf/buildFiles/Dockerfile new file mode 100644 index 000000000..2a08f771b --- /dev/null +++ b/.internal/templates/services/telegraf/buildFiles/Dockerfile @@ -0,0 +1,37 @@ +# Download base image +FROM telegraf:latest + +# Add support tool +RUN apt update && apt install -y rsync + +# where IOTstack template files are stored +ENV IOTSTACK_DEFAULTS_DIR="iotstack_defaults" + +# copy template files to image +COPY ${IOTSTACK_DEFAULTS_DIR} /${IOTSTACK_DEFAULTS_DIR} + +# 1. copy the default configuration file that ships with the image as +# a baseline reference for the user, and make it read-only. +# 2. strip comment lines and blank lines from the baseline reference to +# use as the starting point for the IOTstack default configuration. +# 3. edit the IOTstack default configuration to insert an appropriate +# URL for influxdb running in another container in the same stack. +ENV BASELINE_CONFIG=/${IOTSTACK_DEFAULTS_DIR}/telegraf-reference.conf +ENV IOTSTACK_CONFIG=/${IOTSTACK_DEFAULTS_DIR}/telegraf.conf +RUN cp /etc/telegraf/telegraf.conf ${BASELINE_CONFIG} && \ + chmod 444 ${BASELINE_CONFIG} && \ + grep -v -e "^[ ]*#" -e "^[ ]*$" ${BASELINE_CONFIG} >${IOTSTACK_CONFIG} && \ + sed -i '/^\[\[outputs.influxdb\]\]/a\ \ urls = ["http://influxdb:8086"]' ${IOTSTACK_CONFIG} +ENV BASELINE_CONFIG= +ENV IOTSTACK_CONFIG= + +# replace the docker entry-point script with a self-repairing version +ENV IOTSTACK_ENTRY_POINT="entrypoint.sh" +COPY ${IOTSTACK_ENTRY_POINT} /${IOTSTACK_ENTRY_POINT} +RUN chmod 755 /${IOTSTACK_ENTRY_POINT} +ENV IOTSTACK_ENTRY_POINT= + +# IOTstack declares this path for persistent storage +VOLUME ["/etc/telegraf"] + +# EOF diff --git a/.internal/templates/services/telegraf/buildFiles/entrypoint.sh b/.internal/templates/services/telegraf/buildFiles/entrypoint.sh new file mode 100755 index 000000000..877b73a60 --- /dev/null +++ b/.internal/templates/services/telegraf/buildFiles/entrypoint.sh @@ -0,0 +1,18 @@ +#!/bin/bash +set -e + +if [ "${1:0:1}" = '-' ]; then + set -- telegraf "$@" +fi + +# perform IOTstack self-repair +U="$(id -u)" +T="/etc/telegraf" +if [ "$U" = '0' -a -d "$T" ]; then + rsync -arp --ignore-existing /${IOTSTACK_DEFAULTS_DIR}/ "$T" + chown -R "$U:$U" "$T" +fi + +exec "$@" + + diff --git a/.internal/templates/services/telegraf/buildFiles/iotstack_defaults/additions/inputs.docker.conf b/.internal/templates/services/telegraf/buildFiles/iotstack_defaults/additions/inputs.docker.conf new file mode 100644 index 000000000..79acf85e2 --- /dev/null +++ b/.internal/templates/services/telegraf/buildFiles/iotstack_defaults/additions/inputs.docker.conf @@ -0,0 +1,15 @@ +# Read metrics about docker containers +# Credit: @tablatronix +[[inputs.docker]] + endpoint = "unix:///var/run/docker.sock" + gather_services = false + container_names = [] + source_tag = false + container_name_include = [] + container_name_exclude = [] + timeout = "5s" + perdevice = false + total = true + docker_label_include = [] + docker_label_exclude = [] + tag_env = ["HEAP_SIZE"] diff --git a/.internal/templates/services/telegraf/buildFiles/iotstack_defaults/additions/inputs.mqtt_consumer.conf b/.internal/templates/services/telegraf/buildFiles/iotstack_defaults/additions/inputs.mqtt_consumer.conf new file mode 100644 index 000000000..306fe3144 --- /dev/null +++ b/.internal/templates/services/telegraf/buildFiles/iotstack_defaults/additions/inputs.mqtt_consumer.conf @@ -0,0 +1,10 @@ +# Read metrics from MQTT topic(s) +# Credit: https://github.com/gcgarner/IOTstack/blob/master/.templates/telegraf/telegraf.conf +[[inputs.mqtt_consumer]] + servers = ["tcp://mosquitto:1883"] + topics = [ + "telegraf/host01/cpu", + "telegraf/+/mem", + "sensors/#", + ] + data_format = "json" diff --git a/.internal/templates/services/telegraf/config.js b/.internal/templates/services/telegraf/config.js new file mode 100644 index 000000000..6bb568b6c --- /dev/null +++ b/.internal/templates/services/telegraf/config.js @@ -0,0 +1,46 @@ +const telegraf = () => { + const retr = {}; + + const serviceName = 'telegraf'; + + retr.getConfigOptions = () => { + return { + serviceName, // Required + volumes: true, + networks: false, + logging: true + } + }; + + retr.getHelp = () => { + return { + serviceName, // Required + links: { + "Docker": 'https://hub.docker.com/_/telegraf', + "Source Code": 'https://github.com/influxdata/telegraf', + rawMarkdownRemote: '', // Usually links to github raw help pages. + rawMarkdownLocal: '', // Relative path to docs locally + "IOTstack Documentation for {$displayName}": 'https://sensorsiot.github.io/IOTstack/Containers/Telegraf/' + } + }; + }; + + retr.getCommands = () => { + return { + commands: {} // Key/value pair of helper commands user can run locally + }; + }; + + retr.getMeta = () => { + return { + serviceName, // Required + displayName: 'Telegraf (untested)', + serviceTypeTags: ['iot'], + iconUri: '/logos/telegraf.png' + }; + }; + + return retr; +}; + +module.exports = telegraf; diff --git a/.internal/templates/services/telegraf/template.yml b/.internal/templates/services/telegraf/template.yml new file mode 100644 index 000000000..83d90a4be --- /dev/null +++ b/.internal/templates/services/telegraf/template.yml @@ -0,0 +1,19 @@ +telegraf: + container_name: telegraf + build: ./.templates/telegraf/. + restart: unless-stopped + environment: + - TZ=Etc/UTC + ports: + - "8092:8092/udp" + - "8094:8094/tcp" + - "8125:8125/udp" + volumes: + - ./volumes/telegraf:/etc/telegraf + - /var/run/docker.sock:/var/run/docker.sock:ro + depends_on: + - influxdb + - mosquitto + networks: + - iotstack_nw + diff --git a/.internal/templates/services/timescaledb/build.js b/.internal/templates/services/timescaledb/build.js new file mode 100644 index 000000000..1e7e575dd --- /dev/null +++ b/.internal/templates/services/timescaledb/build.js @@ -0,0 +1,211 @@ +const ServiceBuilder = ({ + settings, + version, + logger +}) => { + const retr = {}; + const serviceName = 'timescaledb'; + + const { + setModifiedPorts, + setLoggingState, + setNetworkMode, + setNetworks, + setEnvironmentVariables, + setDevices + } = require('../../../src/utils/commonCompileLogic'); + + const { + checkPortConflicts, + checkNetworkConflicts, + checkDependencyServices + } = require('../../../src/utils/commonBuildChecks'); + + /* + Order: + 1. compile() - merges build options into the final JSON output. + 2. issues() - runs checks on the compile()'ed JSON, and can also test for errors. + 3. assume() - sets required default values if they are not specified in compile(). Once defaults are set, it reruns compile(). This function is optional + 4. build() - sets up scripts and files. + */ + + retr.init = () => { + logger.debug(`ServiceBuilder:init() - '${serviceName}'`); + }; + + const createVolumesDirectory = () => { + return ` +mkdir -p ./volumes/timescaledb/data +`; + }; + + const checkVolumesDirectory = () => { + return ` +if [[ ! -d ./volumes/timescaledb/data ]]; then + echo "TimescaleDB data directory is missing!" + sleep 2 +fi +`; + }; + + retr.compile = ({ + outputTemplateJson, + buildOptions, + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:compile() - '${serviceName}' started`); + + const compileResults = { + modifiedPorts: setModifiedPorts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedLogging: setLoggingState({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworkMode: setNetworkMode({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworks: setNetworks({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedEnvironment: setEnvironmentVariables({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedDevices: setDevices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }) + }; + console.info(`ServiceBuilder:compile() - '${serviceName}' Results:`, compileResults); + + console.info(`ServiceBuilder:compile() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + return reject({ + component: `ServiceBuilder::compile() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.issues = ({ + outputTemplateJson, + buildOptions, + tmpPath + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:issues() - '${serviceName}' started`); + let issues = []; + + const portConflicts = checkPortConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...portConflicts]; + + const serviceDependencies = checkDependencyServices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...serviceDependencies]; + + const networkConflicts = checkNetworkConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + if (networkConflicts) { + issues.push(networkConflicts); + } + + const environmentList = outputTemplateJson?.services?.[serviceName]?.environment ?? []; + if (Array.isArray(environmentList) && environmentList.length > 0) { + let pwKeysFound = false; + environmentList.forEach((envKVP) => { + const envKey = envKVP.split('=')[0]; + const envValue = envKVP.split('=')[1]; + if (envKey && envValue) { + if (envKey === 'POSTGRES_PASSWORD') { + pwKeysFound = true; + if (envValue === 'Unset' || envValue === 'Unset') { + issues.push({ + type: 'service', + name: serviceName, + issueType: 'environment', + message: `Ensure database passwords are set in environment variables.` + }); + } + } + } + }); + + if (!pwKeysFound) { + issues.push({ + type: 'service', + name: serviceName, + issueType: 'environment', + message: `No environment variables found. Database may not start unless they are set.` + }); + } + } else { + issues.push({ + type: 'service', + name: serviceName, + issueType: 'environment', + message: `No environment variables found. Database may not start unless they are set.` + }); + } + + console.info(`ServiceBuilder:issues() - '${serviceName}' Issues found: ${issues.length}`); + console.info(`ServiceBuilder:issues() - '${serviceName}' completed`); + return resolve(issues); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::issues() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.build = ({ + outputTemplateJson, + buildOptions, + tmpPath, + zipList, + prebuildScripts, + postbuildScripts + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:build() - '${serviceName}' started`); + + prebuildScripts.push({ + serviceName, + comment: 'Create required service directory exists for first launch', + multilineComment: null, + code: createVolumesDirectory() + }); + + postbuildScripts.push({ + serviceName, + comment: 'Ensure required service directory exists for launch', + multilineComment: null, + code: checkVolumesDirectory() + }); + + console.info(`ServiceBuilder:build() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::build() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + return retr; +} + +module.exports = ServiceBuilder; diff --git a/.internal/templates/services/timescaledb/config.js b/.internal/templates/services/timescaledb/config.js new file mode 100644 index 000000000..5b631eea9 --- /dev/null +++ b/.internal/templates/services/timescaledb/config.js @@ -0,0 +1,61 @@ +const timescaledb = () => { + const retr = {}; + + const serviceName = 'timescaledb'; + + retr.getConfigOptions = () => { + return { + serviceName, // Required + modifyableEnvironment: [ + { + key: 'POSTGRES_USER', + value: 'timescaleuser' + }, + { + key: 'POSTGRES_PASSWORD', + value: '{$randomPassword}' + }, + { + key: 'POSTGRES_DB', + value: 'postdb' + } + ], + volumes: true, + networks: true, + logging: true + } + }; + + retr.getHelp = () => { + return { + serviceName, // Required + links: { + "Website": 'https://www.timescale.com/', + "Docker": 'https://hub.docker.com/r/timescale/timescaledb', + "Source Code": 'https://github.com/timescale/timescaledb', + rawMarkdownRemote: '', // Usually links to github raw help pages. + rawMarkdownLocal: '', // Relative path to docs locally + "IOTstack Documentation for {$displayName}": 'https://sensorsiot.github.io/IOTstack/Containers/TimescaleDB/' + } + }; + }; + + retr.getCommands = () => { + return { + commands: {} // Key/value pair of helper commands user can run locally + }; + }; + + retr.getMeta = () => { + return { + serviceName, // Required + displayName: 'Timescale DB', + serviceTypeTags: ['timeseries', 'database'], + iconUri: '/logos/timescaledb.png' + }; + }; + + return retr; +}; + +module.exports = timescaledb; diff --git a/.templates/timescaledb/service.yml b/.internal/templates/services/timescaledb/template.yml similarity index 62% rename from .templates/timescaledb/service.yml rename to .internal/templates/services/timescaledb/template.yml index ad7f5c3f4..337d5d6bb 100644 --- a/.templates/timescaledb/service.yml +++ b/.internal/templates/services/timescaledb/template.yml @@ -3,12 +3,16 @@ timescaledb: image: timescale/timescaledb:latest-pg12 restart: unless-stopped environment: - POSTGRES_USER=timescaleuser - POSTGRES_PASSWORD=%randomPassword% - POSTGRES_DB=postdb + - POSTGRES_USER=timescaleuser + - POSTGRES_PASSWORD=Unset + - POSTGRES_DB=postdb ports: - "5432:5432" volumes: - ./volumes/timescaledb/data:/var/lib/postgresql/data networks: - iotstack_nw + logging: + options: + max-size: "5m" + max-file: "3" diff --git a/.internal/templates/services/transmission/build.js b/.internal/templates/services/transmission/build.js new file mode 100644 index 000000000..80873bcd8 --- /dev/null +++ b/.internal/templates/services/transmission/build.js @@ -0,0 +1,179 @@ +const ServiceBuilder = ({ + settings, + version, + logger +}) => { + const retr = {}; + const serviceName = 'transmission'; + + const { + setModifiedPorts, + setLoggingState, + setNetworkMode, + setNetworks + } = require('../../../src/utils/commonCompileLogic'); + + const { + checkPortConflicts, + checkNetworkConflicts, + checkDependencyServices + } = require('../../../src/utils/commonBuildChecks'); + + /* + Order: + 1. compile() - merges build options into the final JSON output. + 2. issues() - runs checks on the compile()'ed JSON, and can also test for errors. + 3. assume() - sets required default values if they are not specified in compile(). Once defaults are set, it reruns compile(). This function is optional + 4. build() - sets up scripts and files. + */ + + retr.init = () => { + logger.debug(`ServiceBuilder:init() - '${serviceName}'`); + }; + + const createVolumesDirectory = () => { + return ` +mkdir -p ./volumes/transmission/config +mkdir -p ./volumes/transmission/downloads +mkdir -p ./volumes/transmission/watch +`; + }; + + const checkVolumesDirectory = () => { + return ` +if [[ ! -d ./volumes/transmission/config ]]; then + echo "Transmission config directory is missing!" + sleep 2 +fi +if [[ ! -d ./volumes/transmission/downloads ]]; then + echo "Transmission downloads directory is missing!" + sleep 2 +fi +if [[ ! -d ./volumes/transmission/watch ]]; then + echo "Transmission watch directory is missing!" + sleep 2 +fi +`; + }; + + retr.compile = ({ + outputTemplateJson, + buildOptions, + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:compile() - '${serviceName}' started`); + + const compileResults = { + modifiedPorts: setModifiedPorts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedLogging: setLoggingState({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworkMode: setNetworkMode({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworks: setNetworks({ buildTemplate: outputTemplateJson, buildOptions, serviceName }) + }; + console.info(`ServiceBuilder:compile() - '${serviceName}' Results:`, compileResults); + + console.info(`ServiceBuilder:compile() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + return reject({ + component: `ServiceBuilder::compile() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.issues = ({ + outputTemplateJson, + buildOptions, + tmpPath + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:issues() - '${serviceName}' started`); + let issues = []; + + const portConflicts = checkPortConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...portConflicts]; + + const serviceDependencies = checkDependencyServices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...serviceDependencies]; + + const networkConflicts = checkNetworkConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + if (networkConflicts) { + issues.push(networkConflicts); + } + + console.info(`ServiceBuilder:issues() - '${serviceName}' Issues found: ${issues.length}`); + console.info(`ServiceBuilder:issues() - '${serviceName}' completed`); + return resolve(issues); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::issues() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.build = ({ + outputTemplateJson, + buildOptions, + tmpPath, + zipList, + prebuildScripts, + postbuildScripts + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:build() - '${serviceName}' started`); + + prebuildScripts.push({ + serviceName, + comment: 'Create required service directory exists for first launch', + multilineComment: null, + code: createVolumesDirectory() + }); + + postbuildScripts.push({ + serviceName, + comment: 'Ensure required service directory exists for launch', + multilineComment: null, + code: checkVolumesDirectory() + }); + + console.info(`ServiceBuilder:build() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::build() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + return retr; +} + +module.exports = ServiceBuilder; diff --git a/.internal/templates/services/transmission/config.js b/.internal/templates/services/transmission/config.js new file mode 100644 index 000000000..b4d762b81 --- /dev/null +++ b/.internal/templates/services/transmission/config.js @@ -0,0 +1,57 @@ +const transmission = () => { + const retr = {}; + + const serviceName = 'transmission'; + + retr.getConfigOptions = () => { + return { + serviceName, // Required + labeledPorts: { + "9091:9091": 'http' + }, + modifyableEnvironment: [ + { + key: 'TZ', + value: 'Etc/UTC' + } + ], + volumes: true, + networks: true, + logging: true + } + }; + + retr.getHelp = () => { + return { + serviceName, // Required + links: { + "Docker": 'https://hub.docker.com/r/linuxserver/transmission', + "Github": 'https://github.com/linuxserver', + "Community": 'https://fleet.linuxserver.io/', + "Community Chat (Discord)": 'https://discord.gg/YWrKVTn', + rawMarkdownRemote: '', // Usually links to github raw help pages. + rawMarkdownLocal: '', // Relative path to docs locally + "IOTstack Documentation for {$displayName}": 'https://sensorsiot.github.io/IOTstack/Containers/Transmission/' + } + }; + }; + + retr.getCommands = () => { + return { + commands: {} // Key/value pair of helper commands user can run locally + }; + }; + + retr.getMeta = () => { + return { + serviceName, // Required + displayName: 'Transmission', + serviceTypeTags: ['bittorrent', 'wui'], + iconUri: '/logos/transmission.png' + }; + }; + + return retr; +}; + +module.exports = transmission; diff --git a/.templates/transmission/service.yml b/.internal/templates/services/transmission/template.yml similarity index 86% rename from .templates/transmission/service.yml rename to .internal/templates/services/transmission/template.yml index be5230461..7d03e9159 100644 --- a/.templates/transmission/service.yml +++ b/.internal/templates/services/transmission/template.yml @@ -16,3 +16,7 @@ transmission: restart: unless-stopped networks: - iotstack_nw + logging: + options: + max-size: "5m" + max-file: "3" diff --git a/.internal/templates/services/webthingsio_gateway/build.js b/.internal/templates/services/webthingsio_gateway/build.js new file mode 100644 index 000000000..be10190b0 --- /dev/null +++ b/.internal/templates/services/webthingsio_gateway/build.js @@ -0,0 +1,189 @@ +const fs = require('fs'); +const path = require('path'); + +const ServiceBuilder = ({ + settings, + version, + logger +}) => { + const retr = {}; + const serviceName = 'webthingsio_gateway'; + + const { + setModifiedPorts, + setLoggingState, + setNetworkMode, + setNetworks + } = require('../../../src/utils/commonCompileLogic'); + + const { + checkPortConflicts, + checkNetworkConflicts, + checkDependencyServices + } = require('../../../src/utils/commonBuildChecks'); + + /* + Order: + 1. compile() - merges build options into the final JSON output. + 2. issues() - runs checks on the compile()'ed JSON, and can also test for errors. + 3. assume() - sets required default values if they are not specified in compile(). Once defaults are set, it reruns compile(). This function is optional + 4. build() - sets up scripts and files. + */ + + retr.init = () => { + logger.debug(`ServiceBuilder:init() - '${serviceName}'`); + }; + + const createVolumesDirectory = () => { + return ` +mkdir -p ./volumes/webthingsio_gateway/share/config +`; + }; + + const checkVolumesDirectory = () => { + return ` +if [[ ! -f ./volumes/webthingsio_gateway/share/config/local.json ]]; then + echo "WebThings Gateway webthingsio_gateway/share/config/local.json file is missing!" + sleep 2 +fi +`; + }; + + retr.compile = ({ + outputTemplateJson, + buildOptions, + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:compile() - '${serviceName}' started`); + + const compileResults = { + modifiedPorts: setModifiedPorts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedLogging: setLoggingState({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworkMode: setNetworkMode({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworks: setNetworks({ buildTemplate: outputTemplateJson, buildOptions, serviceName }) + }; + console.info(`ServiceBuilder:compile() - '${serviceName}' Results:`, compileResults); + + console.info(`ServiceBuilder:compile() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + return reject({ + component: `ServiceBuilder::compile() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.issues = ({ + outputTemplateJson, + buildOptions, + tmpPath + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:issues() - '${serviceName}' started`); + let issues = []; + + const portConflicts = checkPortConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...portConflicts]; + + const serviceDependencies = checkDependencyServices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...serviceDependencies]; + + const networkConflicts = checkNetworkConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + if (networkConflicts) { + issues.push(networkConflicts); + } + + console.info(`ServiceBuilder:issues() - '${serviceName}' Issues found: ${issues.length}`); + console.info(`ServiceBuilder:issues() - '${serviceName}' completed`); + return resolve(issues); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::issues() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.build = ({ + outputTemplateJson, + fileTimePrefix, + buildOptions, + tmpPath, + zipList, + prebuildScripts, + postbuildScripts + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:build() - '${serviceName}' started`); + + // Start config read, update and write: + const tempConfigfileName = `${fileTimePrefix}_local.json`; + const tempBuildFile = path.join(tmpPath, tempConfigfileName); + const webThingsConfFilePath = path.join(__dirname, settings.paths.serviceFiles, 'local.json'); + + const webthingsConfigFile = require(webThingsConfFilePath); + webthingsConfigFile.test = 'true'; + + fs.writeFileSync(tempBuildFile, JSON.stringify(webthingsConfigFile)); + + // Add config file to zip + zipList.push({ + fullPath: tempBuildFile, + zipName: '/volumes/webthingsio_gateway/share/config/local.json' + }); + + prebuildScripts.push({ + serviceName, + comment: 'Create required service directory exists for first launch', + multilineComment: null, + code: createVolumesDirectory() + }); + + postbuildScripts.push({ + serviceName, + comment: 'Ensure required service directory exists for launch', + multilineComment: null, + code: checkVolumesDirectory() + }); + + console.info(`ServiceBuilder:build() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::build() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + return retr; +} + +module.exports = ServiceBuilder; diff --git a/.internal/templates/services/webthingsio_gateway/config.js b/.internal/templates/services/webthingsio_gateway/config.js new file mode 100644 index 000000000..bf0d1c4fa --- /dev/null +++ b/.internal/templates/services/webthingsio_gateway/config.js @@ -0,0 +1,51 @@ +const webthingsio_gateway = () => { + const retr = {}; + + const serviceName = 'webthingsio_gateway'; + + retr.getConfigOptions = () => { + return { + serviceName, // Required + labeledPorts: { + "4060:8080": 'http', + "4443:4443": 'other' + }, + volumes: true, + networks: false, + logging: true + } + }; + + retr.getHelp = () => { + return { + serviceName, // Required + links: { + "Website": 'https://webthings.io/gateway', + "Docker": 'https://hub.docker.com/r/webthingsio/gateway', + "Source Code": 'https://github.com/WebThingsIO/gateway', + rawMarkdownRemote: '', // Usually links to github raw help pages. + rawMarkdownLocal: '', // Relative path to docs locally + "IOTstack Documentation for {$displayName}": 'https://sensorsiot.github.io/IOTstack/Containers/WebThings/' + } + }; + }; + + retr.getCommands = () => { + return { + commands: {} // Key/value pair of helper commands user can run locally + }; + }; + + retr.getMeta = () => { + return { + serviceName, // Required + displayName: 'Web Things', + serviceTypeTags: ['wui', 'iot'], + iconUri: '/logos/webthings.png' + }; + }; + + return retr; +}; + +module.exports = webthingsio_gateway; diff --git a/.templates/webthings_gateway/local.json b/.internal/templates/services/webthingsio_gateway/serviceFiles/local.json old mode 100755 new mode 100644 similarity index 100% rename from .templates/webthings_gateway/local.json rename to .internal/templates/services/webthingsio_gateway/serviceFiles/local.json diff --git a/.internal/templates/services/webthingsio_gateway/template.yml b/.internal/templates/services/webthingsio_gateway/template.yml new file mode 100644 index 000000000..50d445205 --- /dev/null +++ b/.internal/templates/services/webthingsio_gateway/template.yml @@ -0,0 +1,12 @@ +webthingsio_gateway: + image: webthingsio/gateway:latest + container_name: webthingsio_gateway + network_mode: host + environment: + - TZ=Etc/UTC + volumes: + - ./volumes/webthingsio_gateway/share:/home/node/.webthings/ + logging: + options: + max-size: "5m" + max-file: "3" diff --git a/.internal/templates/services/wireguard/build.js b/.internal/templates/services/wireguard/build.js new file mode 100644 index 000000000..e8090e0ad --- /dev/null +++ b/.internal/templates/services/wireguard/build.js @@ -0,0 +1,221 @@ +const path = require('path'); + +const ServiceBuilder = ({ + settings, + version, + logger +}) => { + const retr = {}; + const serviceName = 'wireguard'; + + const { byName } = require('../../../src/utils/interpolate'); + + const { + setModifiedPorts, + setLoggingState, + setNetworkMode, + setNetworks, + setEnvironmentVariables, + setDevices + } = require('../../../src/utils/commonCompileLogic'); + + const { + checkPortConflicts, + getSetPortsByConfigName, + checkNetworkConflicts, + checkDependencyServices + } = require('../../../src/utils/commonBuildChecks'); + + /* + Order: + 1. compile() - merges build options into the final JSON output. + 2. issues() - runs checks on the compile()'ed JSON, and can also test for errors. + 3. assume() - sets required default values if they are not specified in compile(). Once defaults are set, it reruns compile(). This function is optional + 4. build() - sets up scripts and files. + */ + + retr.init = () => { + logger.debug(`ServiceBuilder:init() - '${serviceName}'`); + }; + + const checkServiceFilesCopied = () => { + return ` +if [[ ! -f ./services/wireguard/wg0.conf ]]; then + echo "Wireguard config file is missing!" + sleep 2 +fi +`; + }; + + const createVolumesDirectory = () => { + return ` +mkdir -p ./services/wireguard/config +`; + }; + + const checkVolumesDirectory = () => { + return ` +if [[ ! -d ./services/wireguard/config ]]; then + echo "Wireguard directory is missing!" + sleep 2 +fi +`; + }; + + retr.compile = ({ + outputTemplateJson, + buildOptions, + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:compile() - '${serviceName}' started`); + + const compileResults = { + modifiedPorts: setModifiedPorts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedLogging: setLoggingState({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworkMode: setNetworkMode({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworks: setNetworks({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedEnvironment: setEnvironmentVariables({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedDevices: setDevices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }) + }; + + // Set env var port + const serviceEnvironmentList = outputTemplateJson?.services?.[serviceName]?.environment ?? []; + if (Array.isArray(serviceEnvironmentList) && serviceEnvironmentList.length > 0) { + const { getConfigOptions } = require('./config')({}); + const envPorts = { + wireguardInternalPort: getSetPortsByConfigName({ buildTemplate: outputTemplateJson, buildOptions, serviceName, configOptions: getConfigOptions(), portName: 'vpn' })?.internalPort, + wireguardExternalPort: getSetPortsByConfigName({ buildTemplate: outputTemplateJson, buildOptions, serviceName, configOptions: getConfigOptions(), portName: 'vpn' })?.externalPort + }; + + serviceEnvironmentList.forEach((envKVP, index) => { + outputTemplateJson.services[serviceName].environment[index] = byName( + outputTemplateJson.services[serviceName].environment[index], + { + ...envPorts + } + ); + }); + } + + console.info(`ServiceBuilder:compile() - '${serviceName}' Results:`, compileResults); + + console.info(`ServiceBuilder:compile() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + return reject({ + component: `ServiceBuilder::compile() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.issues = ({ + outputTemplateJson, + buildOptions, + tmpPath + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:issues() - '${serviceName}' started`); + let issues = []; + + const portConflicts = checkPortConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...portConflicts]; + + const serviceDependencies = checkDependencyServices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...serviceDependencies]; + + const networkConflicts = checkNetworkConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + if (networkConflicts) { + issues.push(networkConflicts); + } + + console.info(`ServiceBuilder:issues() - '${serviceName}' Issues found: ${issues.length}`); + console.info(`ServiceBuilder:issues() - '${serviceName}' completed`); + return resolve(issues); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::issues() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.build = ({ + outputTemplateJson, + buildOptions, + tmpPath, + zipList, + prebuildScripts, + postbuildScripts + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:build() - '${serviceName}' started`); + + const wireguardConfFilePath = path.join(__dirname, settings.paths.serviceFiles, 'wg0.conf'); + zipList.push({ + fullPath: wireguardConfFilePath, + zipName: '/services/wireguard/wg0.conf' + }); + console.debug(`ServiceBuilder:build() - '${serviceName}' Added '${wireguardConfFilePath}' to zip`); + + postbuildScripts.push({ + serviceName, + comment: 'Ensure required service files exist for launch', + multilineComment: null, + code: checkServiceFilesCopied() + }); + + prebuildScripts.push({ + serviceName, + comment: 'Create required service directory exists for first launch', + multilineComment: null, + code: createVolumesDirectory() + }); + + postbuildScripts.push({ + serviceName, + comment: 'Ensure required service directory exists for launch', + multilineComment: null, + code: checkVolumesDirectory() + }); + + console.info(`ServiceBuilder:build() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::build() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + return retr; +} + +module.exports = ServiceBuilder; diff --git a/.internal/templates/services/wireguard/config.js b/.internal/templates/services/wireguard/config.js new file mode 100644 index 000000000..acb357675 --- /dev/null +++ b/.internal/templates/services/wireguard/config.js @@ -0,0 +1,77 @@ +const wireguard = () => { + const retr = {}; + + const serviceName = 'wireguard'; + + retr.getConfigOptions = () => { + return { + serviceName, // Required + labeledPorts: { + "51820:51820": 'vpn' + }, + modifyableEnvironment: [ + { + key: 'TZ', + value: 'Etc/UTC' + }, + { + key: 'SERVERURL', + value: '{$wireguardDuckDns}' + }, + { + key: 'SERVERPORT', + value: '{$wireguardInternalPort}' + }, + { + key: 'PEERS', + value: '1' + }, + { + key: 'PEERDNS', + value: 'auto' + }, + { + key: 'INTERNAL_SUBNET', + value: '100.64.0.0/24' + } + ], + volumes: true, + networks: true, + logging: true + } + }; + + retr.getHelp = () => { + return { + serviceName, // Required + links: { + "Docker": 'https://hub.docker.com/r/linuxserver/wireguard', + "Github": 'https://github.com/linuxserver', + "Community": 'https://fleet.linuxserver.io/', + "Community Chat (Discord)": 'https://discord.gg/YWrKVTn', + rawMarkdownRemote: '', // Usually links to github raw help pages. + rawMarkdownLocal: '', // Relative path to docs locally + "IOTstack Documentation for {$displayName}": 'https://sensorsiot.github.io/IOTstack/Containers/Wireguard/' + } + }; + }; + + retr.getCommands = () => { + return { + commands: {} // Key/value pair of helper commands user can run locally + }; + }; + + retr.getMeta = () => { + return { + serviceName, // Required + displayName: 'Wire Guard (untested)', + serviceTypeTags: ['vpn'], + iconUri: '/logos/wireguard.png' + }; + }; + + return retr; +}; + +module.exports = wireguard; diff --git a/.internal/templates/services/wireguard/template.yml b/.internal/templates/services/wireguard/template.yml new file mode 100644 index 000000000..8797ecd67 --- /dev/null +++ b/.internal/templates/services/wireguard/template.yml @@ -0,0 +1,26 @@ +wireguard: + container_name: wireguard + image: ghcr.io/linuxserver/wireguard + restart: unless-stopped + environment: + - PUID=1000 + - PGID=1000 + - TZ=Etc/UTC + - SERVERURL=Unset + - SERVERPORT=Unset + - PEERS=laptop,phone,tablet + - PEERDNS=auto +# - INTERNAL_SUBNET=100.64.0.0/24 + - ALLOWEDIPS=0.0.0.0/0 + ports: + - "51820:51820/udp" + volumes: + - ./volumes/wireguard/config:/config + - ./volumes/wireguard/custom-cont-init.d:/custom-cont-init.d + - ./volumes/wireguard/custom-services.d:/custom-services.d + - /lib/modules:/lib/modules:ro + cap_add: + - NET_ADMIN + - SYS_MODULE + sysctls: + - net.ipv4.conf.all.src_valid_mark=1 diff --git a/.templates/zigbee2mqtt/Dockerfile b/.internal/templates/services/zigbee2mqtt/Dockerfile similarity index 55% rename from .templates/zigbee2mqtt/Dockerfile rename to .internal/templates/services/zigbee2mqtt/Dockerfile index a6b3f276f..740602985 100644 --- a/.templates/zigbee2mqtt/Dockerfile +++ b/.internal/templates/services/zigbee2mqtt/Dockerfile @@ -1,3 +1,7 @@ +# This file is deprecated. It is being retained for backwards +# compatibility with existing docker-compose.yml files but will +# be removed, eventually. + # Download base image FROM koenkk/zigbee2mqtt @@ -9,4 +13,7 @@ RUN sed -i.bak \ -e '$s/$/\n\nfrontend:\n port: 8080\n# auth_token: PASSWORD\n/' \ /app/configuration.yaml +RUN echo "*** DEPRECATION NOTICE: Please read IOTstack Zigbee2MQTT documentation:" +RUN echo "*** https://sensorsiot.github.io/IOTstack/Containers/Zigbee2MQTT/" + # EOF diff --git a/.internal/templates/services/zigbee2mqtt/build.js b/.internal/templates/services/zigbee2mqtt/build.js new file mode 100644 index 000000000..d86b1f373 --- /dev/null +++ b/.internal/templates/services/zigbee2mqtt/build.js @@ -0,0 +1,169 @@ +const ServiceBuilder = ({ + settings, + version, + logger +}) => { + const retr = {}; + const serviceName = 'zigbee2mqtt'; + + const { + setModifiedPorts, + setLoggingState, + setNetworkMode, + setNetworks + } = require('../../../src/utils/commonCompileLogic'); + + const { + checkPortConflicts, + checkNetworkConflicts, + checkDependencyServices + } = require('../../../src/utils/commonBuildChecks'); + + /* + Order: + 1. compile() - merges build options into the final JSON output. + 2. issues() - runs checks on the compile()'ed JSON, and can also test for errors. + 3. assume() - sets required default values if they are not specified in compile(). Once defaults are set, it reruns compile(). This function is optional + 4. build() - sets up scripts and files. + */ + + retr.init = () => { + logger.debug(`ServiceBuilder:init() - '${serviceName}'`); + }; + + const createVolumesDirectory = () => { + return ` +mkdir -p ./volumes/zigbee2mqtt/data +`; + }; + + const checkVolumesDirectory = () => { + return ` +if [[ ! -d ./volumes/zigbee2mqtt/data ]]; then + echo "zigbee2mqtt data directory is missing!" + sleep 2 +fi +`; + }; + + retr.compile = ({ + outputTemplateJson, + buildOptions, + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:compile() - '${serviceName}' started`); + + const compileResults = { + modifiedPorts: setModifiedPorts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedLogging: setLoggingState({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworkMode: setNetworkMode({ buildTemplate: outputTemplateJson, buildOptions, serviceName }), + modifiedNetworks: setNetworks({ buildTemplate: outputTemplateJson, buildOptions, serviceName }) + }; + console.info(`ServiceBuilder:compile() - '${serviceName}' Results:`, compileResults); + + console.info(`ServiceBuilder:compile() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + return reject({ + component: `ServiceBuilder::compile() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.issues = ({ + outputTemplateJson, + buildOptions, + tmpPath + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:issues() - '${serviceName}' started`); + let issues = []; + + const portConflicts = checkPortConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...portConflicts]; + + const serviceDependencies = checkDependencyServices({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + issues = [...issues, ...serviceDependencies]; + + const networkConflicts = checkNetworkConflicts({ buildTemplate: outputTemplateJson, buildOptions, serviceName }); + if (networkConflicts) { + issues.push(networkConflicts); + } + + console.info(`ServiceBuilder:issues() - '${serviceName}' Issues found: ${issues.length}`); + console.info(`ServiceBuilder:issues() - '${serviceName}' completed`); + return resolve(issues); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::issues() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + retr.build = ({ + outputTemplateJson, + buildOptions, + tmpPath, + zipList, + prebuildScripts, + postbuildScripts + }) => { + return new Promise((resolve, reject) => { + try { + console.info(`ServiceBuilder:build() - '${serviceName}' started`); + + prebuildScripts.push({ + serviceName, + comment: 'Create required service directory exists for first launch', + multilineComment: null, + code: createVolumesDirectory() + }); + + postbuildScripts.push({ + serviceName, + comment: 'Ensure required service directory exists for launch', + multilineComment: null, + code: checkVolumesDirectory() + }); + + console.info(`ServiceBuilder:build() - '${serviceName}' completed`); + return resolve({ type: 'service' }); + } catch (err) { + console.error(err); + console.trace(); + console.debug("\nParams:"); + console.debug({ outputTemplateJson }); + console.debug({ buildOptions }); + console.debug({ tmpPath }); + return reject({ + component: `ServiceBuilder::build() - '${serviceName}'`, + message: 'Unhandled error occured', + error: JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))) + }); + } + }); + }; + + return retr; +} + +module.exports = ServiceBuilder; diff --git a/.internal/templates/services/zigbee2mqtt/config.js b/.internal/templates/services/zigbee2mqtt/config.js new file mode 100644 index 000000000..58466e107 --- /dev/null +++ b/.internal/templates/services/zigbee2mqtt/config.js @@ -0,0 +1,51 @@ +const zigbee2mqtt = () => { + const retr = {}; + + const serviceName = 'zigbee2mqtt'; + + retr.getConfigOptions = () => { + return { + serviceName, // Required + labeledPorts: { + "9080:8080": 'http' + }, + volumes: true, + devices: true, + networks: false, + logging: true + } + }; + + retr.getHelp = () => { + return { + serviceName, // Required + links: { + "Website": 'https://www.zigbee2mqtt.io/', + "Docker": 'https://hub.docker.com/r/koenkk/zigbee2mqtt/', + "Source Code": 'https://github.com/koenkk/zigbee2mqtt', + rawMarkdownRemote: '', // Usually links to github raw help pages. + rawMarkdownLocal: '', // Relative path to docs locally + "IOTstack Documentation for {$displayName}": 'https://sensorsiot.github.io/IOTstack/Containers/Zigbee2MQTT/' + } + }; + }; + + retr.getCommands = () => { + return { + commands: {} // Key/value pair of helper commands user can run locally + }; + }; + + retr.getMeta = () => { + return { + serviceName, // Required + displayName: 'zigbee2mqtt (untested)', + serviceTypeTags: ['zigbee', 'mqtt'], + iconUri: '/logos/zigbee2mqtt.png' + }; + }; + + return retr; +}; + +module.exports = zigbee2mqtt; diff --git a/.templates/zigbee2mqtt/build.py b/.internal/templates/services/zigbee2mqtt/old.build.py old mode 100755 new mode 100644 similarity index 100% rename from .templates/zigbee2mqtt/build.py rename to .internal/templates/services/zigbee2mqtt/old.build.py diff --git a/.internal/templates/services/zigbee2mqtt/template.yml b/.internal/templates/services/zigbee2mqtt/template.yml new file mode 100644 index 000000000..9affea2f3 --- /dev/null +++ b/.internal/templates/services/zigbee2mqtt/template.yml @@ -0,0 +1,22 @@ +zigbee2mqtt: + container_name: zigbee2mqtt + image: koenkk/zigbee2mqtt:latest + environment: + - TZ=Etc/UTC + - ZIGBEE2MQTT_CONFIG_MQTT_SERVER=mqtt://mosquitto:1883 + - ZIGBEE2MQTT_CONFIG_FRONTEND=true + - ZIGBEE2MQTT_CONFIG_ADVANCED_LOG_SYMLINK_CURRENT=true + ports: + - "8080:8080" + volumes: + - ./volumes/zigbee2mqtt/data:/app/data + devices: + - /dev/ttyAMA0:/dev/ttyACM0 + restart: unless-stopped + depends_on: + - mosquitto + logging: + options: + max-size: "5m" + max-file: "3" + diff --git a/.internal/wui.Dockerfile b/.internal/wui.Dockerfile new file mode 100644 index 000000000..9a2e85fdd --- /dev/null +++ b/.internal/wui.Dockerfile @@ -0,0 +1,11 @@ +FROM node:14 + +WORKDIR /usr/iotstack_wui + +# node_modules is ignored with this copy, as specified in .dockerignore +COPY ./wui ./ +RUN npm install +RUN npm run build + +EXPOSE 32777 +CMD [ "npm", "run", "serve" ] diff --git a/.internal/wui/.gitignore b/.internal/wui/.gitignore new file mode 100644 index 000000000..80c051b06 --- /dev/null +++ b/.internal/wui/.gitignore @@ -0,0 +1,25 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +*.eslintcache + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/.internal/wui/README.md b/.internal/wui/README.md new file mode 100644 index 000000000..88116513e --- /dev/null +++ b/.internal/wui/README.md @@ -0,0 +1,68 @@ +This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app), using the [Redux](https://redux.js.org/) and [Redux Toolkit](https://redux-toolkit.js.org/) template. + +## Available Scripts + +In the project directory, you can run: + +### `npm start` + +Runs the app in the development mode.
+Open [http://localhost:3000](http://localhost:3000) to view it in the browser. + +The page will reload if you make edits.
+You will also see any lint errors in the console. + +### `npm test` + +Launches the test runner in the interactive watch mode.
+See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. + +### `npm run build` + +Builds the app for production to the `build` folder.
+It correctly bundles React in production mode and optimizes the build for the best performance. + +The build is minified and the filenames include the hashes.
+Your app is ready to be deployed! + +See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. + +### `npm run eject` + +**Note: this is a one-way operation. Once you `eject`, you can’t go back!** + +If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. + +Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. + +You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. + +## Learn More + +You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). + +To learn React, check out the [React documentation](https://reactjs.org/). + +### Code Splitting + +This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting + +### Analyzing the Bundle Size + +This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size + +### Making a Progressive Web App + +This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app + +### Advanced Configuration + +This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration + +### Deployment + +This section has moved here: https://facebook.github.io/create-react-app/docs/deployment + +### `npm run build` fails to minify + +This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify diff --git a/.internal/wui/package-lock.json b/.internal/wui/package-lock.json new file mode 100644 index 000000000..b3fceb7d8 --- /dev/null +++ b/.internal/wui/package-lock.json @@ -0,0 +1,17454 @@ +{ + "name": "iotstack_wui", + "version": "0.0.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "@babel/compat-data": { + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.12.7.tgz", + "integrity": "sha512-YaxPMGs/XIWtYqrdEOZOCPsVWfEoriXopnsz3/i7apYPXQ3698UFhS6dVT1KN5qOsWmVgw/FOrmQgpRaZayGsw==" + }, + "@babel/core": { + "version": "7.12.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.3.tgz", + "integrity": "sha512-0qXcZYKZp3/6N2jKYVxZv0aNCsxTSVCiK72DTiTYZAu7sjg73W0/aynWjMbiGd87EQL4WyA8reiJVh92AVla9g==", + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.12.1", + "@babel/helper-module-transforms": "^7.12.1", + "@babel/helpers": "^7.12.1", + "@babel/parser": "^7.12.3", + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.12.1", + "@babel/types": "^7.12.1", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.1", + "json5": "^2.1.2", + "lodash": "^4.17.19", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } + } + }, + "@babel/generator": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.5.tgz", + "integrity": "sha512-m16TQQJ8hPt7E+OS/XVQg/7U184MLXtvuGbCdA7na61vha+ImkyyNM/9DDA0unYCVZn3ZOhng+qz48/KBOT96A==", + "requires": { + "@babel/types": "^7.12.5", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz", + "integrity": "sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==", + "requires": { + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.10.4.tgz", + "integrity": "sha512-L0zGlFrGWZK4PbT8AszSfLTM5sDU1+Az/En9VrdT8/LmEiJt4zXt+Jve9DCAnQcbqDhCI+29y/L93mrDzddCcg==", + "requires": { + "@babel/helper-explode-assignable-expression": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-builder-react-jsx": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.10.4.tgz", + "integrity": "sha512-5nPcIZ7+KKDxT1427oBivl9V9YTal7qk0diccnh7RrcgrT/pGFOjgGw1dgryyx1GvHEpXVfoDF6Ak3rTiWh8Rg==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-builder-react-jsx-experimental": { + "version": "7.12.4", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx-experimental/-/helper-builder-react-jsx-experimental-7.12.4.tgz", + "integrity": "sha512-AjEa0jrQqNk7eDQOo0pTfUOwQBMF+xVqrausQwT9/rTKy0g04ggFNaJpaE09IQMn9yExluigWMJcj0WC7bq+Og==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/helper-module-imports": "^7.12.1", + "@babel/types": "^7.12.1" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.12.5.tgz", + "integrity": "sha512-+qH6NrscMolUlzOYngSBMIOQpKUGPPsc61Bu5W10mg84LxZ7cmvnBHzARKbDoFxVvqqAbj6Tg6N7bSrWSPXMyw==", + "requires": { + "@babel/compat-data": "^7.12.5", + "@babel/helper-validator-option": "^7.12.1", + "browserslist": "^4.14.5", + "semver": "^5.5.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } + } + }, + "@babel/helper-create-class-features-plugin": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.12.1.tgz", + "integrity": "sha512-hkL++rWeta/OVOBTRJc9a5Azh5mt5WgZUGAKMD8JM141YsE08K//bp1unBBieO6rUKkIPyUE0USQ30jAy3Sk1w==", + "requires": { + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-member-expression-to-functions": "^7.12.1", + "@babel/helper-optimise-call-expression": "^7.10.4", + "@babel/helper-replace-supers": "^7.12.1", + "@babel/helper-split-export-declaration": "^7.10.4" + } + }, + "@babel/helper-create-regexp-features-plugin": { + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.12.7.tgz", + "integrity": "sha512-idnutvQPdpbduutvi3JVfEgcVIHooQnhvhx0Nk9isOINOIGYkZea1Pk2JlJRiUnMefrlvr0vkByATBY/mB4vjQ==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.10.4", + "regexpu-core": "^4.7.1" + } + }, + "@babel/helper-define-map": { + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.10.5.tgz", + "integrity": "sha512-fMw4kgFB720aQFXSVaXr79pjjcW5puTCM16+rECJ/plGS+zByelE8l9nCpV1GibxTnFVmUuYG9U8wYfQHdzOEQ==", + "requires": { + "@babel/helper-function-name": "^7.10.4", + "@babel/types": "^7.10.5", + "lodash": "^4.17.19" + } + }, + "@babel/helper-explode-assignable-expression": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.12.1.tgz", + "integrity": "sha512-dmUwH8XmlrUpVqgtZ737tK88v07l840z9j3OEhCLwKTkjlvKpfqXVIZ0wpK3aeOxspwGrf/5AP5qLx4rO3w5rA==", + "requires": { + "@babel/types": "^7.12.1" + } + }, + "@babel/helper-function-name": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", + "requires": { + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", + "requires": { + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.4.tgz", + "integrity": "sha512-wljroF5PgCk2juF69kanHVs6vrLwIPNp6DLD+Lrl3hoQ3PpPPikaDRNFA+0t81NOoMt2DL6WW/mdU8k4k6ZzuA==", + "requires": { + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.7.tgz", + "integrity": "sha512-DCsuPyeWxeHgh1Dus7APn7iza42i/qXqiFPWyBDdOFtvS581JQePsc1F/nD+fHrcswhLlRc2UpYS1NwERxZhHw==", + "requires": { + "@babel/types": "^7.12.7" + } + }, + "@babel/helper-module-imports": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.12.5.tgz", + "integrity": "sha512-SR713Ogqg6++uexFRORf/+nPXMmWIn80TALu0uaFb+iQIUoR7bOC7zBWyzBs5b3tBBJXuyD0cRu1F15GyzjOWA==", + "requires": { + "@babel/types": "^7.12.5" + } + }, + "@babel/helper-module-transforms": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.12.1.tgz", + "integrity": "sha512-QQzehgFAZ2bbISiCpmVGfiGux8YVFXQ0abBic2Envhej22DVXV9nCFaS5hIQbkyo1AdGb+gNME2TSh3hYJVV/w==", + "requires": { + "@babel/helper-module-imports": "^7.12.1", + "@babel/helper-replace-supers": "^7.12.1", + "@babel/helper-simple-access": "^7.12.1", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/helper-validator-identifier": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.12.1", + "@babel/types": "^7.12.1", + "lodash": "^4.17.19" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.7.tgz", + "integrity": "sha512-I5xc9oSJ2h59OwyUqjv95HRyzxj53DAubUERgQMrpcCEYQyToeHA+NEcUEsVWB4j53RDeskeBJ0SgRAYHDBckw==", + "requires": { + "@babel/types": "^7.12.7" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==" + }, + "@babel/helper-remap-async-to-generator": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.12.1.tgz", + "integrity": "sha512-9d0KQCRM8clMPcDwo8SevNs+/9a8yWVVmaE80FGJcEP8N1qToREmWEGnBn8BUlJhYRFz6fqxeRL1sl5Ogsed7A==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/helper-wrap-function": "^7.10.4", + "@babel/types": "^7.12.1" + } + }, + "@babel/helper-replace-supers": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.5.tgz", + "integrity": "sha512-5YILoed0ZyIpF4gKcpZitEnXEJ9UoDRki1Ey6xz46rxOzfNMAhVIJMoune1hmPVxh40LRv1+oafz7UsWX+vyWA==", + "requires": { + "@babel/helper-member-expression-to-functions": "^7.12.1", + "@babel/helper-optimise-call-expression": "^7.10.4", + "@babel/traverse": "^7.12.5", + "@babel/types": "^7.12.5" + } + }, + "@babel/helper-simple-access": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.12.1.tgz", + "integrity": "sha512-OxBp7pMrjVewSSC8fXDFrHrBcJATOOFssZwv16F3/6Xtc138GHybBfPbm9kfiqQHKhYQrlamWILwlDCeyMFEaA==", + "requires": { + "@babel/types": "^7.12.1" + } + }, + "@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.12.1.tgz", + "integrity": "sha512-Mf5AUuhG1/OCChOJ/HcADmvcHM42WJockombn8ATJG3OnyiSxBK/Mm5x78BQWvmtXZKHgbjdGL2kin/HOLlZGA==", + "requires": { + "@babel/types": "^7.12.1" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", + "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", + "requires": { + "@babel/types": "^7.11.0" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==" + }, + "@babel/helper-validator-option": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.12.1.tgz", + "integrity": "sha512-YpJabsXlJVWP0USHjnC/AQDTLlZERbON577YUVO/wLpqyj6HAtVYnWaQaN0iUN+1/tWn3c+uKKXjRut5115Y2A==" + }, + "@babel/helper-wrap-function": { + "version": "7.12.3", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.12.3.tgz", + "integrity": "sha512-Cvb8IuJDln3rs6tzjW3Y8UeelAOdnpB8xtQ4sme2MSZ9wOxrbThporC0y/EtE16VAtoyEfLM404Xr1e0OOp+ow==", + "requires": { + "@babel/helper-function-name": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/helpers": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.12.5.tgz", + "integrity": "sha512-lgKGMQlKqA8meJqKsW6rUnc4MdUk35Ln0ATDqdM1a/UpARODdI4j5Y5lVfUScnSNkJcdCRAaWkspykNoFg9sJA==", + "requires": { + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.12.5", + "@babel/types": "^7.12.5" + } + }, + "@babel/highlight": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.7.tgz", + "integrity": "sha512-oWR02Ubp4xTLCAqPRiNIuMVgNO5Aif/xpXtabhzW2HWUD47XJsAB4Zd/Rg30+XeQA3juXigV7hlquOTmwqLiwg==" + }, + "@babel/plugin-proposal-async-generator-functions": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.12.1.tgz", + "integrity": "sha512-d+/o30tJxFxrA1lhzJqiUcEJdI6jKlNregCv5bASeGf2Q4MXmnwH7viDo7nhx1/ohf09oaH8j1GVYG/e3Yqk6A==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-remap-async-to-generator": "^7.12.1", + "@babel/plugin-syntax-async-generators": "^7.8.0" + } + }, + "@babel/plugin-proposal-class-properties": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.12.1.tgz", + "integrity": "sha512-cKp3dlQsFsEs5CWKnN7BnSHOd0EOW8EKpEjkoz1pO2E5KzIDNV9Ros1b0CnmbVgAGXJubOYVBOGCT1OmJwOI7w==", + "requires": { + "@babel/helper-create-class-features-plugin": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-proposal-decorators": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.12.1.tgz", + "integrity": "sha512-knNIuusychgYN8fGJHONL0RbFxLGawhXOJNLBk75TniTsZZeA+wdkDuv6wp4lGwzQEKjZi6/WYtnb3udNPmQmQ==", + "requires": { + "@babel/helper-create-class-features-plugin": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-decorators": "^7.12.1" + } + }, + "@babel/plugin-proposal-dynamic-import": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.12.1.tgz", + "integrity": "sha512-a4rhUSZFuq5W8/OO8H7BL5zspjnc1FLd9hlOxIK/f7qG4a0qsqk8uvF/ywgBA8/OmjsapjpvaEOYItfGG1qIvQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-dynamic-import": "^7.8.0" + } + }, + "@babel/plugin-proposal-export-namespace-from": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.12.1.tgz", + "integrity": "sha512-6CThGf0irEkzujYS5LQcjBx8j/4aQGiVv7J9+2f7pGfxqyKh3WnmVJYW3hdrQjyksErMGBPQrCnHfOtna+WLbw==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + } + }, + "@babel/plugin-proposal-json-strings": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.12.1.tgz", + "integrity": "sha512-GoLDUi6U9ZLzlSda2Df++VSqDJg3CG+dR0+iWsv6XRw1rEq+zwt4DirM9yrxW6XWaTpmai1cWJLMfM8qQJf+yw==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.0" + } + }, + "@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.12.1.tgz", + "integrity": "sha512-k8ZmVv0JU+4gcUGeCDZOGd0lCIamU/sMtIiX3UWnUc5yzgq6YUGyEolNYD+MLYKfSzgECPcqetVcJP9Afe/aCA==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + } + }, + "@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.12.1.tgz", + "integrity": "sha512-nZY0ESiaQDI1y96+jk6VxMOaL4LPo/QDHBqL+SF3/vl6dHkTwHlOI8L4ZwuRBHgakRBw5zsVylel7QPbbGuYgg==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0" + } + }, + "@babel/plugin-proposal-numeric-separator": { + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.12.7.tgz", + "integrity": "sha512-8c+uy0qmnRTeukiGsjLGy6uVs/TFjJchGXUeBqlG4VWYOdJWkhhVPdQ3uHwbmalfJwv2JsV0qffXP4asRfL2SQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + } + }, + "@babel/plugin-proposal-object-rest-spread": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.12.1.tgz", + "integrity": "sha512-s6SowJIjzlhx8o7lsFx5zmY4At6CTtDvgNQDdPzkBQucle58A6b/TTeEBYtyDgmcXjUTM+vE8YOGHZzzbc/ioA==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.0", + "@babel/plugin-transform-parameters": "^7.12.1" + } + }, + "@babel/plugin-proposal-optional-catch-binding": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.12.1.tgz", + "integrity": "sha512-hFvIjgprh9mMw5v42sJWLI1lzU5L2sznP805zeT6rySVRA0Y18StRhDqhSxlap0oVgItRsB6WSROp4YnJTJz0g==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.0" + } + }, + "@babel/plugin-proposal-optional-chaining": { + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.12.7.tgz", + "integrity": "sha512-4ovylXZ0PWmwoOvhU2vhnzVNnm88/Sm9nx7V8BPgMvAzn5zDou3/Awy0EjglyubVHasJj+XCEkr/r1X3P5elCA==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-skip-transparent-expression-wrappers": "^7.12.1", + "@babel/plugin-syntax-optional-chaining": "^7.8.0" + } + }, + "@babel/plugin-proposal-private-methods": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.12.1.tgz", + "integrity": "sha512-mwZ1phvH7/NHK6Kf8LP7MYDogGV+DKB1mryFOEwx5EBNQrosvIczzZFTUmWaeujd5xT6G1ELYWUz3CutMhjE1w==", + "requires": { + "@babel/helper-create-class-features-plugin": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-proposal-unicode-property-regex": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.12.1.tgz", + "integrity": "sha512-MYq+l+PvHuw/rKUz1at/vb6nCnQ2gmJBNaM62z0OgH7B2W1D9pvkpYtlti9bGtizNIU1K3zm4bZF9F91efVY0w==", + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.1.tgz", + "integrity": "sha512-U40A76x5gTwmESz+qiqssqmeEsKvcSyvtgktrm0uzcARAmM9I1jR221f6Oq+GmHrcD+LvZDag1UTOTe2fL3TeA==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-decorators": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.12.1.tgz", + "integrity": "sha512-ir9YW5daRrTYiy9UJ2TzdNIJEZu8KclVzDcfSt4iEmOtwQ4llPtWInNKJyKnVXp1vE4bbVd5S31M/im3mYMO1w==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-syntax-flow": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.12.1.tgz", + "integrity": "sha512-1lBLLmtxrwpm4VKmtVFselI/P3pX+G63fAtUUt6b2Nzgao77KNDwyuRt90Mj2/9pKobtt68FdvjfqohZjg/FCA==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-jsx": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.12.1.tgz", + "integrity": "sha512-1yRi7yAtB0ETgxdY9ti/p2TivUxJkTdhu/ZbF9MshVGqOx1TdB3b7xCXs49Fupgg50N45KcAsRP/ZqWjs9SRjg==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.12.1.tgz", + "integrity": "sha512-i7ooMZFS+a/Om0crxZodrTzNEPJHZrlMVGMTEpFAj6rYY/bKCddB0Dk/YxfPuYXOopuhKk/e1jV6h+WUU9XN3A==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-typescript": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.12.1.tgz", + "integrity": "sha512-UZNEcCY+4Dp9yYRCAHrHDU+9ZXLYaY9MgBXSRLkB9WjYFRR6quJBumfVrEkUxrePPBwFcpWfNKXqVRQQtm7mMA==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-arrow-functions": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.12.1.tgz", + "integrity": "sha512-5QB50qyN44fzzz4/qxDPQMBCTHgxg3n0xRBLJUmBlLoU/sFvxVWGZF/ZUfMVDQuJUKXaBhbupxIzIfZ6Fwk/0A==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-async-to-generator": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.12.1.tgz", + "integrity": "sha512-SDtqoEcarK1DFlRJ1hHRY5HvJUj5kX4qmtpMAm2QnhOlyuMC4TMdCRgW6WXpv93rZeYNeLP22y8Aq2dbcDRM1A==", + "requires": { + "@babel/helper-module-imports": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-remap-async-to-generator": "^7.12.1" + } + }, + "@babel/plugin-transform-block-scoped-functions": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.12.1.tgz", + "integrity": "sha512-5OpxfuYnSgPalRpo8EWGPzIYf0lHBWORCkj5M0oLBwHdlux9Ri36QqGW3/LR13RSVOAoUUMzoPI/jpE4ABcHoA==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-block-scoping": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.12.1.tgz", + "integrity": "sha512-zJyAC9sZdE60r1nVQHblcfCj29Dh2Y0DOvlMkcqSo0ckqjiCwNiUezUKw+RjOCwGfpLRwnAeQ2XlLpsnGkvv9w==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-classes": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.12.1.tgz", + "integrity": "sha512-/74xkA7bVdzQTBeSUhLLJgYIcxw/dpEpCdRDiHgPJ3Mv6uC11UhjpOhl72CgqbBCmt1qtssCyB2xnJm1+PFjog==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/helper-define-map": "^7.10.4", + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-optimise-call-expression": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-replace-supers": "^7.12.1", + "@babel/helper-split-export-declaration": "^7.10.4", + "globals": "^11.1.0" + } + }, + "@babel/plugin-transform-computed-properties": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.12.1.tgz", + "integrity": "sha512-vVUOYpPWB7BkgUWPo4C44mUQHpTZXakEqFjbv8rQMg7TC6S6ZhGZ3otQcRH6u7+adSlE5i0sp63eMC/XGffrzg==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-destructuring": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.12.1.tgz", + "integrity": "sha512-fRMYFKuzi/rSiYb2uRLiUENJOKq4Gnl+6qOv5f8z0TZXg3llUwUhsNNwrwaT/6dUhJTzNpBr+CUvEWBtfNY1cw==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-dotall-regex": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.12.1.tgz", + "integrity": "sha512-B2pXeRKoLszfEW7J4Hg9LoFaWEbr/kzo3teWHmtFCszjRNa/b40f9mfeqZsIDLLt/FjwQ6pz/Gdlwy85xNckBA==", + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-duplicate-keys": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.12.1.tgz", + "integrity": "sha512-iRght0T0HztAb/CazveUpUQrZY+aGKKaWXMJ4uf9YJtqxSUe09j3wteztCUDRHs+SRAL7yMuFqUsLoAKKzgXjw==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-exponentiation-operator": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.12.1.tgz", + "integrity": "sha512-7tqwy2bv48q+c1EHbXK0Zx3KXd2RVQp6OC7PbwFNt/dPTAV3Lu5sWtWuAj8owr5wqtWnqHfl2/mJlUmqkChKug==", + "requires": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-flow-strip-types": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.12.1.tgz", + "integrity": "sha512-8hAtkmsQb36yMmEtk2JZ9JnVyDSnDOdlB+0nEGzIDLuK4yR3JcEjfuFPYkdEPSh8Id+rAMeBEn+X0iVEyho6Hg==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-flow": "^7.12.1" + } + }, + "@babel/plugin-transform-for-of": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.12.1.tgz", + "integrity": "sha512-Zaeq10naAsuHo7heQvyV0ptj4dlZJwZgNAtBYBnu5nNKJoW62m0zKcIEyVECrUKErkUkg6ajMy4ZfnVZciSBhg==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-function-name": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.12.1.tgz", + "integrity": "sha512-JF3UgJUILoFrFMEnOJLJkRHSk6LUSXLmEFsA23aR2O5CSLUxbeUX1IZ1YQ7Sn0aXb601Ncwjx73a+FVqgcljVw==", + "requires": { + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-literals": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.12.1.tgz", + "integrity": "sha512-+PxVGA+2Ag6uGgL0A5f+9rklOnnMccwEBzwYFL3EUaKuiyVnUipyXncFcfjSkbimLrODoqki1U9XxZzTvfN7IQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-member-expression-literals": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.12.1.tgz", + "integrity": "sha512-1sxePl6z9ad0gFMB9KqmYofk34flq62aqMt9NqliS/7hPEpURUCMbyHXrMPlo282iY7nAvUB1aQd5mg79UD9Jg==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-modules-amd": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.12.1.tgz", + "integrity": "sha512-tDW8hMkzad5oDtzsB70HIQQRBiTKrhfgwC/KkJeGsaNFTdWhKNt/BiE8c5yj19XiGyrxpbkOfH87qkNg1YGlOQ==", + "requires": { + "@babel/helper-module-transforms": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4", + "babel-plugin-dynamic-import-node": "^2.3.3" + } + }, + "@babel/plugin-transform-modules-commonjs": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.12.1.tgz", + "integrity": "sha512-dY789wq6l0uLY8py9c1B48V8mVL5gZh/+PQ5ZPrylPYsnAvnEMjqsUXkuoDVPeVK+0VyGar+D08107LzDQ6pag==", + "requires": { + "@babel/helper-module-transforms": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-simple-access": "^7.12.1", + "babel-plugin-dynamic-import-node": "^2.3.3" + } + }, + "@babel/plugin-transform-modules-systemjs": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.12.1.tgz", + "integrity": "sha512-Hn7cVvOavVh8yvW6fLwveFqSnd7rbQN3zJvoPNyNaQSvgfKmDBO9U1YL9+PCXGRlZD9tNdWTy5ACKqMuzyn32Q==", + "requires": { + "@babel/helper-hoist-variables": "^7.10.4", + "@babel/helper-module-transforms": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-validator-identifier": "^7.10.4", + "babel-plugin-dynamic-import-node": "^2.3.3" + } + }, + "@babel/plugin-transform-modules-umd": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.12.1.tgz", + "integrity": "sha512-aEIubCS0KHKM0zUos5fIoQm+AZUMt1ZvMpqz0/H5qAQ7vWylr9+PLYurT+Ic7ID/bKLd4q8hDovaG3Zch2uz5Q==", + "requires": { + "@babel/helper-module-transforms": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.12.1.tgz", + "integrity": "sha512-tB43uQ62RHcoDp9v2Nsf+dSM8sbNodbEicbQNA53zHz8pWUhsgHSJCGpt7daXxRydjb0KnfmB+ChXOv3oADp1Q==", + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.12.1" + } + }, + "@babel/plugin-transform-new-target": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.12.1.tgz", + "integrity": "sha512-+eW/VLcUL5L9IvJH7rT1sT0CzkdUTvPrXC2PXTn/7z7tXLBuKvezYbGdxD5WMRoyvyaujOq2fWoKl869heKjhw==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-object-super": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.12.1.tgz", + "integrity": "sha512-AvypiGJH9hsquNUn+RXVcBdeE3KHPZexWRdimhuV59cSoOt5kFBmqlByorAeUlGG2CJWd0U+4ZtNKga/TB0cAw==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-replace-supers": "^7.12.1" + } + }, + "@babel/plugin-transform-parameters": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.12.1.tgz", + "integrity": "sha512-xq9C5EQhdPK23ZeCdMxl8bbRnAgHFrw5EOC3KJUsSylZqdkCaFEXxGSBuTSObOpiiHHNyb82es8M1QYgfQGfNg==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-property-literals": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.12.1.tgz", + "integrity": "sha512-6MTCR/mZ1MQS+AwZLplX4cEySjCpnIF26ToWo942nqn8hXSm7McaHQNeGx/pt7suI1TWOWMfa/NgBhiqSnX0cQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-react-constant-elements": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.12.1.tgz", + "integrity": "sha512-KOHd0tIRLoER+J+8f9DblZDa1fLGPwaaN1DI1TVHuQFOpjHV22C3CUB3obeC4fexHY9nx+fH0hQNvLFFfA1mxA==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-react-display-name": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.12.1.tgz", + "integrity": "sha512-cAzB+UzBIrekfYxyLlFqf/OagTvHLcVBb5vpouzkYkBclRPraiygVnafvAoipErZLI8ANv8Ecn6E/m5qPXD26w==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-react-jsx": { + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.12.7.tgz", + "integrity": "sha512-YFlTi6MEsclFAPIDNZYiCRbneg1MFGao9pPG9uD5htwE0vDbPaMUMeYd6itWjw7K4kro4UbdQf3ljmFl9y48dQ==", + "requires": { + "@babel/helper-builder-react-jsx": "^7.10.4", + "@babel/helper-builder-react-jsx-experimental": "^7.12.4", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-jsx": "^7.12.1" + } + }, + "@babel/plugin-transform-react-jsx-development": { + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.12.7.tgz", + "integrity": "sha512-Rs3ETtMtR3VLXFeYRChle5SsP/P9Jp/6dsewBQfokDSzKJThlsuFcnzLTDRALiUmTC48ej19YD9uN1mupEeEDg==", + "requires": { + "@babel/helper-builder-react-jsx-experimental": "^7.12.4", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-jsx": "^7.12.1" + } + }, + "@babel/plugin-transform-react-jsx-self": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.12.1.tgz", + "integrity": "sha512-FbpL0ieNWiiBB5tCldX17EtXgmzeEZjFrix72rQYeq9X6nUK38HCaxexzVQrZWXanxKJPKVVIU37gFjEQYkPkA==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-react-jsx-source": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.12.1.tgz", + "integrity": "sha512-keQ5kBfjJNRc6zZN1/nVHCd6LLIHq4aUKcVnvE/2l+ZZROSbqoiGFRtT5t3Is89XJxBQaP7NLZX2jgGHdZvvFQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-react-pure-annotations": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.12.1.tgz", + "integrity": "sha512-RqeaHiwZtphSIUZ5I85PEH19LOSzxfuEazoY7/pWASCAIBuATQzpSVD+eT6MebeeZT2F4eSL0u4vw6n4Nm0Mjg==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-regenerator": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.12.1.tgz", + "integrity": "sha512-gYrHqs5itw6i4PflFX3OdBPMQdPbF4bj2REIUxlMRUFk0/ZOAIpDFuViuxPjUL7YC8UPnf+XG7/utJvqXdPKng==", + "requires": { + "regenerator-transform": "^0.14.2" + } + }, + "@babel/plugin-transform-reserved-words": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.12.1.tgz", + "integrity": "sha512-pOnUfhyPKvZpVyBHhSBoX8vfA09b7r00Pmm1sH+29ae2hMTKVmSp4Ztsr8KBKjLjx17H0eJqaRC3bR2iThM54A==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-runtime": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.12.1.tgz", + "integrity": "sha512-Ac/H6G9FEIkS2tXsZjL4RAdS3L3WHxci0usAnz7laPWUmFiGtj7tIASChqKZMHTSQTQY6xDbOq+V1/vIq3QrWg==", + "requires": { + "@babel/helper-module-imports": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4", + "resolve": "^1.8.1", + "semver": "^5.5.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } + } + }, + "@babel/plugin-transform-shorthand-properties": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.12.1.tgz", + "integrity": "sha512-GFZS3c/MhX1OusqB1MZ1ct2xRzX5ppQh2JU1h2Pnfk88HtFTM+TWQqJNfwkmxtPQtb/s1tk87oENfXJlx7rSDw==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-spread": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.12.1.tgz", + "integrity": "sha512-vuLp8CP0BE18zVYjsEBZ5xoCecMK6LBMMxYzJnh01rxQRvhNhH1csMMmBfNo5tGpGO+NhdSNW2mzIvBu3K1fng==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-skip-transparent-expression-wrappers": "^7.12.1" + } + }, + "@babel/plugin-transform-sticky-regex": { + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.12.7.tgz", + "integrity": "sha512-VEiqZL5N/QvDbdjfYQBhruN0HYjSPjC4XkeqW4ny/jNtH9gcbgaqBIXYEZCNnESMAGs0/K/R7oFGMhOyu/eIxg==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-template-literals": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.12.1.tgz", + "integrity": "sha512-b4Zx3KHi+taXB1dVRBhVJtEPi9h1THCeKmae2qP0YdUHIFhVjtpqqNfxeVAa1xeHVhAy4SbHxEwx5cltAu5apw==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-typeof-symbol": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.12.1.tgz", + "integrity": "sha512-EPGgpGy+O5Kg5pJFNDKuxt9RdmTgj5sgrus2XVeMp/ZIbOESadgILUbm50SNpghOh3/6yrbsH+NB5+WJTmsA7Q==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-typescript": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.12.1.tgz", + "integrity": "sha512-VrsBByqAIntM+EYMqSm59SiMEf7qkmI9dqMt6RbD/wlwueWmYcI0FFK5Fj47pP6DRZm+3teXjosKlwcZJ5lIMw==", + "requires": { + "@babel/helper-create-class-features-plugin": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-typescript": "^7.12.1" + } + }, + "@babel/plugin-transform-unicode-escapes": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.12.1.tgz", + "integrity": "sha512-I8gNHJLIc7GdApm7wkVnStWssPNbSRMPtgHdmH3sRM1zopz09UWPS4x5V4n1yz/MIWTVnJ9sp6IkuXdWM4w+2Q==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-unicode-regex": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.12.1.tgz", + "integrity": "sha512-SqH4ClNngh/zGwHZOOQMTD+e8FGWexILV+ePMyiDJttAWRh5dhDL8rcl5lSgU3Huiq6Zn6pWTMvdPAb21Dwdyg==", + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/preset-env": { + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.12.7.tgz", + "integrity": "sha512-OnNdfAr1FUQg7ksb7bmbKoby4qFOHw6DKWWUNB9KqnnCldxhxJlP+21dpyaWFmf2h0rTbOkXJtAGevY3XW1eew==", + "requires": { + "@babel/compat-data": "^7.12.7", + "@babel/helper-compilation-targets": "^7.12.5", + "@babel/helper-module-imports": "^7.12.5", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-validator-option": "^7.12.1", + "@babel/plugin-proposal-async-generator-functions": "^7.12.1", + "@babel/plugin-proposal-class-properties": "^7.12.1", + "@babel/plugin-proposal-dynamic-import": "^7.12.1", + "@babel/plugin-proposal-export-namespace-from": "^7.12.1", + "@babel/plugin-proposal-json-strings": "^7.12.1", + "@babel/plugin-proposal-logical-assignment-operators": "^7.12.1", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.1", + "@babel/plugin-proposal-numeric-separator": "^7.12.7", + "@babel/plugin-proposal-object-rest-spread": "^7.12.1", + "@babel/plugin-proposal-optional-catch-binding": "^7.12.1", + "@babel/plugin-proposal-optional-chaining": "^7.12.7", + "@babel/plugin-proposal-private-methods": "^7.12.1", + "@babel/plugin-proposal-unicode-property-regex": "^7.12.1", + "@babel/plugin-syntax-async-generators": "^7.8.0", + "@babel/plugin-syntax-class-properties": "^7.12.1", + "@babel/plugin-syntax-dynamic-import": "^7.8.0", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.0", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.0", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.0", + "@babel/plugin-syntax-top-level-await": "^7.12.1", + "@babel/plugin-transform-arrow-functions": "^7.12.1", + "@babel/plugin-transform-async-to-generator": "^7.12.1", + "@babel/plugin-transform-block-scoped-functions": "^7.12.1", + "@babel/plugin-transform-block-scoping": "^7.12.1", + "@babel/plugin-transform-classes": "^7.12.1", + "@babel/plugin-transform-computed-properties": "^7.12.1", + "@babel/plugin-transform-destructuring": "^7.12.1", + "@babel/plugin-transform-dotall-regex": "^7.12.1", + "@babel/plugin-transform-duplicate-keys": "^7.12.1", + "@babel/plugin-transform-exponentiation-operator": "^7.12.1", + "@babel/plugin-transform-for-of": "^7.12.1", + "@babel/plugin-transform-function-name": "^7.12.1", + "@babel/plugin-transform-literals": "^7.12.1", + "@babel/plugin-transform-member-expression-literals": "^7.12.1", + "@babel/plugin-transform-modules-amd": "^7.12.1", + "@babel/plugin-transform-modules-commonjs": "^7.12.1", + "@babel/plugin-transform-modules-systemjs": "^7.12.1", + "@babel/plugin-transform-modules-umd": "^7.12.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.12.1", + "@babel/plugin-transform-new-target": "^7.12.1", + "@babel/plugin-transform-object-super": "^7.12.1", + "@babel/plugin-transform-parameters": "^7.12.1", + "@babel/plugin-transform-property-literals": "^7.12.1", + "@babel/plugin-transform-regenerator": "^7.12.1", + "@babel/plugin-transform-reserved-words": "^7.12.1", + "@babel/plugin-transform-shorthand-properties": "^7.12.1", + "@babel/plugin-transform-spread": "^7.12.1", + "@babel/plugin-transform-sticky-regex": "^7.12.7", + "@babel/plugin-transform-template-literals": "^7.12.1", + "@babel/plugin-transform-typeof-symbol": "^7.12.1", + "@babel/plugin-transform-unicode-escapes": "^7.12.1", + "@babel/plugin-transform-unicode-regex": "^7.12.1", + "@babel/preset-modules": "^0.1.3", + "@babel/types": "^7.12.7", + "core-js-compat": "^3.7.0", + "semver": "^5.5.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } + } + }, + "@babel/preset-modules": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.4.tgz", + "integrity": "sha512-J36NhwnfdzpmH41M1DrnkkgAqhZaqr/NBdPfQ677mLzlaXo+oDiv1deyCDtgAhz8p328otdob0Du7+xgHGZbKg==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + } + }, + "@babel/preset-react": { + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.12.7.tgz", + "integrity": "sha512-wKeTdnGUP5AEYCYQIMeXMMwU7j+2opxrG0WzuZfxuuW9nhKvvALBjl67653CWamZJVefuJGI219G591RSldrqQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-transform-react-display-name": "^7.12.1", + "@babel/plugin-transform-react-jsx": "^7.12.7", + "@babel/plugin-transform-react-jsx-development": "^7.12.7", + "@babel/plugin-transform-react-jsx-self": "^7.12.1", + "@babel/plugin-transform-react-jsx-source": "^7.12.1", + "@babel/plugin-transform-react-pure-annotations": "^7.12.1" + } + }, + "@babel/preset-typescript": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.12.1.tgz", + "integrity": "sha512-hNK/DhmoJPsksdHuI/RVrcEws7GN5eamhi28JkO52MqIxU8Z0QpmiSOQxZHWOHV7I3P4UjHV97ay4TcamMA6Kw==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-transform-typescript": "^7.12.1" + } + }, + "@babel/runtime": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz", + "integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "@babel/runtime-corejs3": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.12.5.tgz", + "integrity": "sha512-roGr54CsTmNPPzZoCP1AmDXuBoNao7tnSA83TXTwt+UK5QVyh1DIJnrgYRPWKCF2flqZQXwa7Yr8v7VmLzF0YQ==", + "requires": { + "core-js-pure": "^3.0.0", + "regenerator-runtime": "^0.13.4" + } + }, + "@babel/template": { + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.7.tgz", + "integrity": "sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow==", + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.12.7", + "@babel/types": "^7.12.7" + } + }, + "@babel/traverse": { + "version": "7.12.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.9.tgz", + "integrity": "sha512-iX9ajqnLdoU1s1nHt36JDI9KG4k+vmI8WgjK5d+aDTwQbL2fUnzedNedssA645Ede3PM2ma1n8Q4h2ohwXgMXw==", + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.12.5", + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/parser": "^7.12.7", + "@babel/types": "^7.12.7", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.19" + } + }, + "@babel/types": { + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz", + "integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==", + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==" + }, + "@cnakazawa/watch": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz", + "integrity": "sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ==", + "requires": { + "exec-sh": "^0.3.2", + "minimist": "^1.2.0" + } + }, + "@csstools/convert-colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@csstools/convert-colors/-/convert-colors-1.4.0.tgz", + "integrity": "sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==" + }, + "@csstools/normalize.css": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-10.1.0.tgz", + "integrity": "sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg==" + }, + "@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==" + }, + "@eslint/eslintrc": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.2.1.tgz", + "integrity": "sha512-XRUeBZ5zBWLYgSANMpThFddrZZkEbGHgUdt5UJjZfnlN9BGCiUBrf+nvbRupSjMvqzwnQN0qwCmOxITt1cfywA==", + "requires": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "lodash": "^4.17.19", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "requires": { + "type-fest": "^0.8.1" + } + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==" + } + } + }, + "@hapi/address": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz", + "integrity": "sha512-QD1PhQk+s31P1ixsX0H0Suoupp3VMXzIVMSwobR3F3MSUO2YCV0B7xqLcUw/Bh8yuvd3LhpyqLQWTNcRmp6IdQ==" + }, + "@hapi/bourne": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-1.3.2.tgz", + "integrity": "sha512-1dVNHT76Uu5N3eJNTYcvxee+jzX4Z9lfciqRRHCU27ihbUcYi+iSc2iml5Ke1LXe1SyJCLA0+14Jh4tXJgOppA==" + }, + "@hapi/hoek": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-8.5.1.tgz", + "integrity": "sha512-yN7kbciD87WzLGc5539Tn0sApjyiGHAJgKvG9W8C7O+6c7qmoQMfVs0W4bX17eqz6C78QJqqFrtgdK5EWf6Qow==" + }, + "@hapi/joi": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/@hapi/joi/-/joi-15.1.1.tgz", + "integrity": "sha512-entf8ZMOK8sc+8YfeOlM8pCfg3b5+WZIKBfUaaJT8UsjAAPjartzxIYm3TIbjvA4u+u++KbcXD38k682nVHDAQ==", + "requires": { + "@hapi/address": "2.x.x", + "@hapi/bourne": "1.x.x", + "@hapi/hoek": "8.x.x", + "@hapi/topo": "3.x.x" + } + }, + "@hapi/topo": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-3.1.6.tgz", + "integrity": "sha512-tAag0jEcjwH+P2quUfipd7liWCNX2F8NvYjQp2wtInsZxnMlypdw0FtAOLxtvvkO+GSRRbmNi8m/5y42PQJYCQ==", + "requires": { + "@hapi/hoek": "^8.3.0" + } + }, + "@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==" + } + } + }, + "@istanbuljs/schema": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", + "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==" + }, + "@jest/console": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-26.6.2.tgz", + "integrity": "sha512-IY1R2i2aLsLr7Id3S6p2BA82GNWryt4oSvEXLAKc+L2zdi89dSkE8xC1C+0kpATG4JhBJREnQOH7/zmccM2B0g==", + "requires": { + "@jest/types": "^26.6.2", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^26.6.2", + "jest-util": "^26.6.2", + "slash": "^3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@jest/core": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-26.6.3.tgz", + "integrity": "sha512-xvV1kKbhfUqFVuZ8Cyo+JPpipAHHAV3kcDBftiduK8EICXmTFddryy3P7NfZt8Pv37rA9nEJBKCCkglCPt/Xjw==", + "requires": { + "@jest/console": "^26.6.2", + "@jest/reporters": "^26.6.2", + "@jest/test-result": "^26.6.2", + "@jest/transform": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.4", + "jest-changed-files": "^26.6.2", + "jest-config": "^26.6.3", + "jest-haste-map": "^26.6.2", + "jest-message-util": "^26.6.2", + "jest-regex-util": "^26.0.0", + "jest-resolve": "^26.6.2", + "jest-resolve-dependencies": "^26.6.3", + "jest-runner": "^26.6.3", + "jest-runtime": "^26.6.3", + "jest-snapshot": "^26.6.2", + "jest-util": "^26.6.2", + "jest-validate": "^26.6.2", + "jest-watcher": "^26.6.2", + "micromatch": "^4.0.2", + "p-each-series": "^2.1.0", + "rimraf": "^3.0.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "jest-resolve": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-26.6.2.tgz", + "integrity": "sha512-sOxsZOq25mT1wRsfHcbtkInS+Ek7Q8jCHUB0ZUTP0tc/c41QHriU/NunqMfCUWsL4H3MHpvQD4QR9kSYhS7UvQ==", + "requires": { + "@jest/types": "^26.6.2", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^26.6.2", + "read-pkg-up": "^7.0.1", + "resolve": "^1.18.1", + "slash": "^3.0.0" + } + }, + "read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "requires": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "dependencies": { + "type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==" + } + } + }, + "read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "requires": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + } + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@jest/environment": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-26.6.2.tgz", + "integrity": "sha512-nFy+fHl28zUrRsCeMB61VDThV1pVTtlEokBRgqPrcT1JNq4yRNIyTHfyht6PqtUvY9IsuLGTrbG8kPXjSZIZwA==", + "requires": { + "@jest/fake-timers": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", + "jest-mock": "^26.6.2" + } + }, + "@jest/fake-timers": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-26.6.2.tgz", + "integrity": "sha512-14Uleatt7jdzefLPYM3KLcnUl1ZNikaKq34enpb5XG9i81JpppDb5muZvonvKyrl7ftEHkKS5L5/eB/kxJ+bvA==", + "requires": { + "@jest/types": "^26.6.2", + "@sinonjs/fake-timers": "^6.0.1", + "@types/node": "*", + "jest-message-util": "^26.6.2", + "jest-mock": "^26.6.2", + "jest-util": "^26.6.2" + } + }, + "@jest/globals": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-26.6.2.tgz", + "integrity": "sha512-85Ltnm7HlB/KesBUuALwQ68YTU72w9H2xW9FjZ1eL1U3lhtefjjl5c2MiUbpXt/i6LaPRvoOFJ22yCBSfQ0JIA==", + "requires": { + "@jest/environment": "^26.6.2", + "@jest/types": "^26.6.2", + "expect": "^26.6.2" + } + }, + "@jest/reporters": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-26.6.2.tgz", + "integrity": "sha512-h2bW53APG4HvkOnVMo8q3QXa6pcaNt1HkwVsOPMBV6LD/q9oSpxNSYZQYkAnjdMjrJ86UuYeLo+aEZClV6opnw==", + "requires": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^26.6.2", + "@jest/test-result": "^26.6.2", + "@jest/transform": "^26.6.2", + "@jest/types": "^26.6.2", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.2", + "graceful-fs": "^4.2.4", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^4.0.3", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "jest-haste-map": "^26.6.2", + "jest-resolve": "^26.6.2", + "jest-util": "^26.6.2", + "jest-worker": "^26.6.2", + "node-notifier": "^8.0.0", + "slash": "^3.0.0", + "source-map": "^0.6.0", + "string-length": "^4.0.1", + "terminal-link": "^2.0.0", + "v8-to-istanbul": "^7.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "jest-resolve": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-26.6.2.tgz", + "integrity": "sha512-sOxsZOq25mT1wRsfHcbtkInS+Ek7Q8jCHUB0ZUTP0tc/c41QHriU/NunqMfCUWsL4H3MHpvQD4QR9kSYhS7UvQ==", + "requires": { + "@jest/types": "^26.6.2", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^26.6.2", + "read-pkg-up": "^7.0.1", + "resolve": "^1.18.1", + "slash": "^3.0.0" + } + }, + "read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "requires": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "dependencies": { + "type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==" + } + } + }, + "read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "requires": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@jest/source-map": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-26.6.2.tgz", + "integrity": "sha512-YwYcCwAnNmOVsZ8mr3GfnzdXDAl4LaenZP5z+G0c8bzC9/dugL8zRmxZzdoTl4IaS3CryS1uWnROLPFmb6lVvA==", + "requires": { + "callsites": "^3.0.0", + "graceful-fs": "^4.2.4", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "@jest/test-result": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-26.6.2.tgz", + "integrity": "sha512-5O7H5c/7YlojphYNrK02LlDIV2GNPYisKwHm2QTKjNZeEzezCbwYs9swJySv2UfPMyZ0VdsmMv7jIlD/IKYQpQ==", + "requires": { + "@jest/console": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + } + }, + "@jest/test-sequencer": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-26.6.3.tgz", + "integrity": "sha512-YHlVIjP5nfEyjlrSr8t/YdNfU/1XEt7c5b4OxcXCjyRhjzLYu/rO69/WHPuYcbCWkz8kAeZVZp2N2+IOLLEPGw==", + "requires": { + "@jest/test-result": "^26.6.2", + "graceful-fs": "^4.2.4", + "jest-haste-map": "^26.6.2", + "jest-runner": "^26.6.3", + "jest-runtime": "^26.6.3" + } + }, + "@jest/transform": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-26.6.2.tgz", + "integrity": "sha512-E9JjhUgNzvuQ+vVAL21vlyfy12gP0GhazGgJC4h6qUt1jSdUXGWJ1wfu/X7Sd8etSgxV4ovT1pb9v5D6QW4XgA==", + "requires": { + "@babel/core": "^7.1.0", + "@jest/types": "^26.6.2", + "babel-plugin-istanbul": "^6.0.0", + "chalk": "^4.0.0", + "convert-source-map": "^1.4.0", + "fast-json-stable-stringify": "^2.0.0", + "graceful-fs": "^4.2.4", + "jest-haste-map": "^26.6.2", + "jest-regex-util": "^26.0.0", + "jest-util": "^26.6.2", + "micromatch": "^4.0.2", + "pirates": "^4.0.1", + "slash": "^3.0.0", + "source-map": "^0.6.1", + "write-file-atomic": "^3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@material-ui/core": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.11.2.tgz", + "integrity": "sha512-/D1+AQQeYX/WhT/FUk78UCRj8ch/RCglsQLYujYTIqPSJlwZHKcvHidNeVhODXeApojeXjkl0tWdk5C9ofwOkQ==", + "requires": { + "@babel/runtime": "^7.4.4", + "@material-ui/styles": "^4.11.2", + "@material-ui/system": "^4.11.2", + "@material-ui/types": "^5.1.0", + "@material-ui/utils": "^4.11.2", + "@types/react-transition-group": "^4.2.0", + "clsx": "^1.0.4", + "hoist-non-react-statics": "^3.3.2", + "popper.js": "1.16.1-lts", + "prop-types": "^15.7.2", + "react-is": "^16.8.0 || ^17.0.0", + "react-transition-group": "^4.4.0" + } + }, + "@material-ui/icons": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@material-ui/icons/-/icons-4.11.2.tgz", + "integrity": "sha512-fQNsKX2TxBmqIGJCSi3tGTO/gZ+eJgWmMJkgDiOfyNaunNaxcklJQFaFogYcFl0qFuaEz1qaXYXboa/bUXVSOQ==", + "requires": { + "@babel/runtime": "^7.4.4" + } + }, + "@material-ui/lab": { + "version": "4.0.0-alpha.57", + "resolved": "https://registry.npmjs.org/@material-ui/lab/-/lab-4.0.0-alpha.57.tgz", + "integrity": "sha512-qo/IuIQOmEKtzmRD2E4Aa6DB4A87kmY6h0uYhjUmrrgmEAgbbw9etXpWPVXuRK6AGIQCjFzV6WO2i21m1R4FCw==", + "requires": { + "@babel/runtime": "^7.4.4", + "@material-ui/utils": "^4.11.2", + "clsx": "^1.0.4", + "prop-types": "^15.7.2", + "react-is": "^16.8.0 || ^17.0.0" + } + }, + "@material-ui/styles": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.2.tgz", + "integrity": "sha512-xbItf8zkfD3FuGoD9f2vlcyPf9jTEtj9YTJoNNV+NMWaSAHXgrW6geqRoo/IwBuMjqpwqsZhct13e2nUyU9Ljw==", + "requires": { + "@babel/runtime": "^7.4.4", + "@emotion/hash": "^0.8.0", + "@material-ui/types": "^5.1.0", + "@material-ui/utils": "^4.11.2", + "clsx": "^1.0.4", + "csstype": "^2.5.2", + "hoist-non-react-statics": "^3.3.2", + "jss": "^10.0.3", + "jss-plugin-camel-case": "^10.0.3", + "jss-plugin-default-unit": "^10.0.3", + "jss-plugin-global": "^10.0.3", + "jss-plugin-nested": "^10.0.3", + "jss-plugin-props-sort": "^10.0.3", + "jss-plugin-rule-value-function": "^10.0.3", + "jss-plugin-vendor-prefixer": "^10.0.3", + "prop-types": "^15.7.2" + }, + "dependencies": { + "csstype": { + "version": "2.6.14", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.14.tgz", + "integrity": "sha512-2mSc+VEpGPblzAxyeR+vZhJKgYg0Og0nnRi7pmRXFYYxSfnOnW8A5wwQb4n4cE2nIOzqKOAzLCaEX6aBmNEv8A==" + } + } + }, + "@material-ui/system": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@material-ui/system/-/system-4.11.2.tgz", + "integrity": "sha512-BELFJEel5E+5DMiZb6XXT3peWRn6UixRvBtKwSxqntmD0+zwbbfCij6jtGwwdJhN1qX/aXrKu10zX31GBaeR7A==", + "requires": { + "@babel/runtime": "^7.4.4", + "@material-ui/utils": "^4.11.2", + "csstype": "^2.5.2", + "prop-types": "^15.7.2" + }, + "dependencies": { + "csstype": { + "version": "2.6.14", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.14.tgz", + "integrity": "sha512-2mSc+VEpGPblzAxyeR+vZhJKgYg0Og0nnRi7pmRXFYYxSfnOnW8A5wwQb4n4cE2nIOzqKOAzLCaEX6aBmNEv8A==" + } + } + }, + "@material-ui/types": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@material-ui/types/-/types-5.1.0.tgz", + "integrity": "sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==" + }, + "@material-ui/utils": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@material-ui/utils/-/utils-4.11.2.tgz", + "integrity": "sha512-Uul8w38u+PICe2Fg2pDKCaIG7kOyhowZ9vjiC1FsVwPABTW8vPPKfF6OvxRq3IiBaI1faOJmgdvMG7rMJARBhA==", + "requires": { + "@babel/runtime": "^7.4.4", + "prop-types": "^15.7.2", + "react-is": "^16.8.0 || ^17.0.0" + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz", + "integrity": "sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw==", + "requires": { + "@nodelib/fs.stat": "2.0.3", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz", + "integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==" + }, + "@nodelib/fs.walk": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz", + "integrity": "sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ==", + "requires": { + "@nodelib/fs.scandir": "2.1.3", + "fastq": "^1.6.0" + } + }, + "@npmcli/move-file": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.0.1.tgz", + "integrity": "sha512-Uv6h1sT+0DrblvIrolFtbvM1FgWm+/sy4B3pvLp67Zys+thcukzS5ekn7HsZFGpWP4Q3fYJCljbWQE/XivMRLw==", + "requires": { + "mkdirp": "^1.0.4" + }, + "dependencies": { + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + } + } + }, + "@pmmmwh/react-refresh-webpack-plugin": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.4.2.tgz", + "integrity": "sha512-Loc4UDGutcZ+Bd56hBInkm6JyjyCwWy4t2wcDXzN8EDPANgVRj0VP8Nxn0Zq2pc+WKauZwEivQgbDGg4xZO20A==", + "requires": { + "ansi-html": "^0.0.7", + "error-stack-parser": "^2.0.6", + "html-entities": "^1.2.1", + "native-url": "^0.2.6", + "schema-utils": "^2.6.5", + "source-map": "^0.7.3" + }, + "dependencies": { + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" + } + } + }, + "@reduxjs/toolkit": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.5.0.tgz", + "integrity": "sha512-E/FUraRx+8guw9Hlg/Ja8jI/hwCrmIKed8Annt9YsZw3BQp+F24t5I5b2OWR6pkEHY4hn1BgP08FrTZFRKsdaQ==", + "requires": { + "immer": "^8.0.0", + "redux": "^4.0.0", + "redux-thunk": "^2.3.0", + "reselect": "^4.0.0" + }, + "dependencies": { + "immer": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-8.0.0.tgz", + "integrity": "sha512-jm87NNBAIG4fHwouilCHIecFXp5rMGkiFrAuhVO685UnMAlOneEAnOyzPt8OnP47TC11q/E7vpzZe0WvwepFTg==" + } + } + }, + "@rollup/plugin-node-resolve": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-7.1.3.tgz", + "integrity": "sha512-RxtSL3XmdTAE2byxekYLnx+98kEUOrPHF/KRVjLH+DEIHy6kjIw7YINQzn+NXiH/NTrQLAwYs0GWB+csWygA9Q==", + "requires": { + "@rollup/pluginutils": "^3.0.8", + "@types/resolve": "0.0.8", + "builtin-modules": "^3.1.0", + "is-module": "^1.0.0", + "resolve": "^1.14.2" + } + }, + "@rollup/plugin-replace": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.3.4.tgz", + "integrity": "sha512-waBhMzyAtjCL1GwZes2jaE9MjuQ/DQF2BatH3fRivUF3z0JBFrU0U6iBNC/4WR+2rLKhaAhPWDNPYp4mI6RqdQ==", + "requires": { + "@rollup/pluginutils": "^3.1.0", + "magic-string": "^0.25.7" + } + }, + "@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "requires": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + }, + "dependencies": { + "@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==" + } + } + }, + "@sheerun/mutationobserver-shim": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.3.tgz", + "integrity": "sha512-DetpxZw1fzPD5xUBrIAoplLChO2VB8DlL5Gg+I1IR9b2wPqYIca2WSUxL5g1vLeR4MsQq1NeWriXAVffV+U1Fw==" + }, + "@sinonjs/commons": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.1.tgz", + "integrity": "sha512-892K+kWUUi3cl+LlqEWIDrhvLgdL79tECi8JZUyq6IviKy/DNhuzCRlbHUjxK89f4ypPMMaFnFuR9Ie6DoIMsw==", + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz", + "integrity": "sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==", + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, + "@surma/rollup-plugin-off-main-thread": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-1.4.2.tgz", + "integrity": "sha512-yBMPqmd1yEJo/280PAMkychuaALyQ9Lkb5q1ck3mjJrFuEobIfhnQ4J3mbvBoISmR3SWMWV+cGB/I0lCQee79A==", + "requires": { + "ejs": "^2.6.1", + "magic-string": "^0.25.0" + } + }, + "@svgr/babel-plugin-add-jsx-attribute": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-5.4.0.tgz", + "integrity": "sha512-ZFf2gs/8/6B8PnSofI0inYXr2SDNTDScPXhN7k5EqD4aZ3gi6u+rbmZHVB8IM3wDyx8ntKACZbtXSm7oZGRqVg==" + }, + "@svgr/babel-plugin-remove-jsx-attribute": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-5.4.0.tgz", + "integrity": "sha512-yaS4o2PgUtwLFGTKbsiAy6D0o3ugcUhWK0Z45umJ66EPWunAz9fuFw2gJuje6wqQvQWOTJvIahUwndOXb7QCPg==" + }, + "@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-5.0.1.tgz", + "integrity": "sha512-LA72+88A11ND/yFIMzyuLRSMJ+tRKeYKeQ+mR3DcAZ5I4h5CPWN9AHyUzJbWSYp/u2u0xhmgOe0+E41+GjEueA==" + }, + "@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-5.0.1.tgz", + "integrity": "sha512-PoiE6ZD2Eiy5mK+fjHqwGOS+IXX0wq/YDtNyIgOrc6ejFnxN4b13pRpiIPbtPwHEc+NT2KCjteAcq33/F1Y9KQ==" + }, + "@svgr/babel-plugin-svg-dynamic-title": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-5.4.0.tgz", + "integrity": "sha512-zSOZH8PdZOpuG1ZVx/cLVePB2ibo3WPpqo7gFIjLV9a0QsuQAzJiwwqmuEdTaW2pegyBE17Uu15mOgOcgabQZg==" + }, + "@svgr/babel-plugin-svg-em-dimensions": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-5.4.0.tgz", + "integrity": "sha512-cPzDbDA5oT/sPXDCUYoVXEmm3VIoAWAPT6mSPTJNbQaBNUuEKVKyGH93oDY4e42PYHRW67N5alJx/eEol20abw==" + }, + "@svgr/babel-plugin-transform-react-native-svg": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-5.4.0.tgz", + "integrity": "sha512-3eYP/SaopZ41GHwXma7Rmxcv9uRslRDTY1estspeB1w1ueZWd/tPlMfEOoccYpEMZU3jD4OU7YitnXcF5hLW2Q==" + }, + "@svgr/babel-plugin-transform-svg-component": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-5.5.0.tgz", + "integrity": "sha512-q4jSH1UUvbrsOtlo/tKcgSeiCHRSBdXoIoqX1pgcKK/aU3JD27wmMKwGtpB8qRYUYoyXvfGxUVKchLuR5pB3rQ==" + }, + "@svgr/babel-preset": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-5.5.0.tgz", + "integrity": "sha512-4FiXBjvQ+z2j7yASeGPEi8VD/5rrGQk4Xrq3EdJmoZgz/tpqChpo5hgXDvmEauwtvOc52q8ghhZK4Oy7qph4ig==", + "requires": { + "@svgr/babel-plugin-add-jsx-attribute": "^5.4.0", + "@svgr/babel-plugin-remove-jsx-attribute": "^5.4.0", + "@svgr/babel-plugin-remove-jsx-empty-expression": "^5.0.1", + "@svgr/babel-plugin-replace-jsx-attribute-value": "^5.0.1", + "@svgr/babel-plugin-svg-dynamic-title": "^5.4.0", + "@svgr/babel-plugin-svg-em-dimensions": "^5.4.0", + "@svgr/babel-plugin-transform-react-native-svg": "^5.4.0", + "@svgr/babel-plugin-transform-svg-component": "^5.5.0" + } + }, + "@svgr/core": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-5.5.0.tgz", + "integrity": "sha512-q52VOcsJPvV3jO1wkPtzTuKlvX7Y3xIcWRpCMtBF3MrteZJtBfQw/+u0B1BHy5ColpQc1/YVTrPEtSYIMNZlrQ==", + "requires": { + "@svgr/plugin-jsx": "^5.5.0", + "camelcase": "^6.2.0", + "cosmiconfig": "^7.0.0" + } + }, + "@svgr/hast-util-to-babel-ast": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-5.5.0.tgz", + "integrity": "sha512-cAaR/CAiZRB8GP32N+1jocovUtvlj0+e65TB50/6Lcime+EA49m/8l+P2ko+XPJ4dw3xaPS3jOL4F2X4KWxoeQ==", + "requires": { + "@babel/types": "^7.12.6" + } + }, + "@svgr/plugin-jsx": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-5.5.0.tgz", + "integrity": "sha512-V/wVh33j12hGh05IDg8GpIUXbjAPnTdPTKuP4VNLggnwaHMPNQNae2pRnyTAILWCQdz5GyMqtO488g7CKM8CBA==", + "requires": { + "@babel/core": "^7.12.3", + "@svgr/babel-preset": "^5.5.0", + "@svgr/hast-util-to-babel-ast": "^5.5.0", + "svg-parser": "^2.0.2" + } + }, + "@svgr/plugin-svgo": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-5.5.0.tgz", + "integrity": "sha512-r5swKk46GuQl4RrVejVwpeeJaydoxkdwkM1mBKOgJLBUJPGaLci6ylg/IjhrRsREKDkr4kbMWdgOtbXEh0fyLQ==", + "requires": { + "cosmiconfig": "^7.0.0", + "deepmerge": "^4.2.2", + "svgo": "^1.2.2" + } + }, + "@svgr/webpack": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-5.4.0.tgz", + "integrity": "sha512-LjepnS/BSAvelnOnnzr6Gg0GcpLmnZ9ThGFK5WJtm1xOqdBE/1IACZU7MMdVzjyUkfFqGz87eRE4hFaSLiUwYg==", + "requires": { + "@babel/core": "^7.9.0", + "@babel/plugin-transform-react-constant-elements": "^7.9.0", + "@babel/preset-env": "^7.9.5", + "@babel/preset-react": "^7.9.4", + "@svgr/core": "^5.4.0", + "@svgr/plugin-jsx": "^5.4.0", + "@svgr/plugin-svgo": "^5.4.0", + "loader-utils": "^2.0.0" + } + }, + "@testing-library/dom": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-6.16.0.tgz", + "integrity": "sha512-lBD88ssxqEfz0wFL6MeUyyWZfV/2cjEZZV3YRpb2IoJRej/4f1jB0TzqIOznTpfR1r34CNesrubxwIlAQ8zgPA==", + "requires": { + "@babel/runtime": "^7.8.4", + "@sheerun/mutationobserver-shim": "^0.3.2", + "@types/testing-library__dom": "^6.12.1", + "aria-query": "^4.0.2", + "dom-accessibility-api": "^0.3.0", + "pretty-format": "^25.1.0", + "wait-for-expect": "^3.0.2" + }, + "dependencies": { + "@jest/types": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", + "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "pretty-format": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.5.0.tgz", + "integrity": "sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ==", + "requires": { + "@jest/types": "^25.5.0", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^16.12.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@testing-library/jest-dom": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-4.2.4.tgz", + "integrity": "sha512-j31Bn0rQo12fhCWOUWy9fl7wtqkp7In/YP2p5ZFyRuiiB9Qs3g+hS4gAmDWONbAHcRmVooNJ5eOHQDCOmUFXHg==", + "requires": { + "@babel/runtime": "^7.5.1", + "chalk": "^2.4.1", + "css": "^2.2.3", + "css.escape": "^1.5.1", + "jest-diff": "^24.0.0", + "jest-matcher-utils": "^24.0.0", + "lodash": "^4.17.11", + "pretty-format": "^24.0.0", + "redent": "^3.0.0" + }, + "dependencies": { + "@jest/types": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + } + }, + "@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "@types/yargs": { + "version": "13.0.11", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.11.tgz", + "integrity": "sha512-NRqD6T4gktUrDi1o1wLH3EKC1o2caCr7/wR87ODcbVITQF106OM3sFN92ysZ++wqelOd1CTzatnOBRDYYG6wGQ==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + }, + "diff-sequences": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-24.9.0.tgz", + "integrity": "sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew==" + }, + "jest-diff": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-24.9.0.tgz", + "integrity": "sha512-qMfrTs8AdJE2iqrTp0hzh7kTd2PQWrsFyj9tORoKmu32xjPjeE4NyjVRDz8ybYwqS2ik8N4hsIpiVTyFeo2lBQ==", + "requires": { + "chalk": "^2.0.1", + "diff-sequences": "^24.9.0", + "jest-get-type": "^24.9.0", + "pretty-format": "^24.9.0" + } + }, + "jest-get-type": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-24.9.0.tgz", + "integrity": "sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q==" + }, + "jest-matcher-utils": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-24.9.0.tgz", + "integrity": "sha512-OZz2IXsu6eaiMAwe67c1T+5tUAtQyQx27/EMEkbFAGiw52tB9em+uGbzpcgYVpA8wl0hlxKPZxrly4CXU/GjHA==", + "requires": { + "chalk": "^2.0.1", + "jest-diff": "^24.9.0", + "jest-get-type": "^24.9.0", + "pretty-format": "^24.9.0" + } + }, + "pretty-format": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-24.9.0.tgz", + "integrity": "sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==", + "requires": { + "@jest/types": "^24.9.0", + "ansi-regex": "^4.0.0", + "ansi-styles": "^3.2.0", + "react-is": "^16.8.4" + } + } + } + }, + "@testing-library/react": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-9.5.0.tgz", + "integrity": "sha512-di1b+D0p+rfeboHO5W7gTVeZDIK5+maEgstrZbWZSSvxDyfDRkkyBE1AJR5Psd6doNldluXlCWqXriUfqu/9Qg==", + "requires": { + "@babel/runtime": "^7.8.4", + "@testing-library/dom": "^6.15.0", + "@types/testing-library__react": "^9.1.2" + } + }, + "@testing-library/user-event": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-7.2.1.tgz", + "integrity": "sha512-oZ0Ib5I4Z2pUEcoo95cT1cr6slco9WY7yiPpG+RGNkj8YcYgJnM7pXmYmorNOReh8MIGcKSqXyeGjxnr8YiZbA==" + }, + "@types/anymatch": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz", + "integrity": "sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA==" + }, + "@types/babel__core": { + "version": "7.1.12", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.12.tgz", + "integrity": "sha512-wMTHiiTiBAAPebqaPiPDLFA4LYPKr6Ph0Xq/6rq1Ur3v66HXyG+clfR9CNETkD7MQS8ZHvpQOtA53DLws5WAEQ==", + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.2.tgz", + "integrity": "sha512-MdSJnBjl+bdwkLskZ3NGFp9YcXGx5ggLpQQPqtgakVhsWK0hTtNYhjpZLlWQTviGTvF8at+Bvli3jV7faPdgeQ==", + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@types/babel__template": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.0.tgz", + "integrity": "sha512-NTPErx4/FiPCGScH7foPyr+/1Dkzkni+rHiYHHoTjvwou7AQzJkNeD60A9CXRy+ZEN2B1bggmkTMCDb+Mv5k+A==", + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.0.16", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.0.16.tgz", + "integrity": "sha512-S63Dt4CZOkuTmpLGGWtT/mQdVORJOpx6SZWGVaP56dda/0Nx5nEe82K7/LAm8zYr6SfMq+1N2OreIOrHAx656w==", + "requires": { + "@babel/types": "^7.3.0" + } + }, + "@types/eslint": { + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.2.6.tgz", + "integrity": "sha512-I+1sYH+NPQ3/tVqCeUSBwTE/0heyvtXqpIopUUArlBm0Kpocb8FbMa3AZ/ASKIFpN3rnEx932TTXDbt9OXsNDw==", + "requires": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "@types/estree": { + "version": "0.0.45", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.45.tgz", + "integrity": "sha512-jnqIUKDUqJbDIUxm0Uj7bnlMnRm1T/eZ9N+AVMqhPgzrba2GhGG5o/jCTwmdPK709nEZsGoMzXEDUjcXHa3W0g==" + }, + "@types/glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==", + "requires": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "@types/graceful-fs": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.4.tgz", + "integrity": "sha512-mWA/4zFQhfvOA8zWkXobwJvBD7vzcxgrOQ0J5CH1votGqdq9m7+FwtGaqyCZqC3NyyBkc9z4m+iry4LlqcMWJg==", + "requires": { + "@types/node": "*" + } + }, + "@types/html-minifier-terser": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz", + "integrity": "sha512-giAlZwstKbmvMk1OO7WXSj4OZ0keXAcl2TQq4LWHiiPH2ByaH7WeUzng+Qej8UPxxv+8lRTuouo0iaNDBuzIBA==" + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", + "integrity": "sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==" + }, + "@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "@types/json-schema": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz", + "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==" + }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=" + }, + "@types/minimatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==" + }, + "@types/node": { + "version": "14.14.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.10.tgz", + "integrity": "sha512-J32dgx2hw8vXrSbu4ZlVhn1Nm3GbeCFNw2FWL8S5QKucHGY0cyNwjdQdO+KMBZ4wpmC7KhLCiNsdk1RFRIYUQQ==" + }, + "@types/normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==" + }, + "@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" + }, + "@types/prettier": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.1.5.tgz", + "integrity": "sha512-UEyp8LwZ4Dg30kVU2Q3amHHyTn1jEdhCIE59ANed76GaT1Vp76DD3ZWSAxgCrw6wJ0TqeoBpqmfUHiUDPs//HQ==" + }, + "@types/prop-types": { + "version": "15.7.3", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", + "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==" + }, + "@types/q": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.4.tgz", + "integrity": "sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==" + }, + "@types/react": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.0.tgz", + "integrity": "sha512-aj/L7RIMsRlWML3YB6KZiXB3fV2t41+5RBGYF8z+tAKU43Px8C3cYUZsDvf1/+Bm4FK21QWBrDutu8ZJ/70qOw==", + "requires": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "@types/react-dom": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.0.tgz", + "integrity": "sha512-lUqY7OlkF/RbNtD5nIq7ot8NquXrdFrjSOR6+w9a9RFQevGi1oZO1dcJbXMeONAPKtZ2UrZOEJ5UOCVsxbLk/g==", + "requires": { + "@types/react": "*" + } + }, + "@types/react-transition-group": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.0.tgz", + "integrity": "sha512-/QfLHGpu+2fQOqQaXh8MG9q03bFENooTb/it4jr5kKaZlDQfWvjqWZg48AwzPVMBHlRuTRAY7hRHCEOXz5kV6w==", + "requires": { + "@types/react": "*" + } + }, + "@types/resolve": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz", + "integrity": "sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==", + "requires": { + "@types/node": "*" + } + }, + "@types/source-list-map": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", + "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==" + }, + "@types/stack-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz", + "integrity": "sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==" + }, + "@types/tapable": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.6.tgz", + "integrity": "sha512-W+bw9ds02rAQaMvaLYxAbJ6cvguW/iJXNT6lTssS1ps6QdrMKttqEAMEG/b5CR8TZl3/L7/lH0ZV5nNR1LXikA==" + }, + "@types/testing-library__dom": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/testing-library__dom/-/testing-library__dom-6.14.0.tgz", + "integrity": "sha512-sMl7OSv0AvMOqn1UJ6j1unPMIHRXen0Ita1ujnMX912rrOcawe4f7wu0Zt9GIQhBhJvH2BaibqFgQ3lP+Pj2hA==", + "requires": { + "pretty-format": "^24.3.0" + }, + "dependencies": { + "@jest/types": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + } + }, + "@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "@types/yargs": { + "version": "13.0.11", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.11.tgz", + "integrity": "sha512-NRqD6T4gktUrDi1o1wLH3EKC1o2caCr7/wR87ODcbVITQF106OM3sFN92ysZ++wqelOd1CTzatnOBRDYYG6wGQ==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + }, + "pretty-format": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-24.9.0.tgz", + "integrity": "sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==", + "requires": { + "@jest/types": "^24.9.0", + "ansi-regex": "^4.0.0", + "ansi-styles": "^3.2.0", + "react-is": "^16.8.4" + } + } + } + }, + "@types/testing-library__react": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/@types/testing-library__react/-/testing-library__react-9.1.3.tgz", + "integrity": "sha512-iCdNPKU3IsYwRK9JieSYAiX0+aYDXOGAmrC/3/M7AqqSDKnWWVv07X+Zk1uFSL7cMTUYzv4lQRfohucEocn5/w==", + "requires": { + "@types/react-dom": "*", + "@types/testing-library__dom": "*", + "pretty-format": "^25.1.0" + }, + "dependencies": { + "@jest/types": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", + "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "pretty-format": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.5.0.tgz", + "integrity": "sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ==", + "requires": { + "@jest/types": "^25.5.0", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^16.12.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@types/uglify-js": { + "version": "3.11.1", + "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.11.1.tgz", + "integrity": "sha512-7npvPKV+jINLu1SpSYVWG8KvyJBhBa8tmzMMdDoVc2pWUYHN8KIXlPJhjJ4LT97c4dXJA2SHL/q6ADbDriZN+Q==", + "requires": { + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "@types/webpack": { + "version": "4.41.25", + "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.25.tgz", + "integrity": "sha512-cr6kZ+4m9lp86ytQc1jPOJXgINQyz3kLLunZ57jznW+WIAL0JqZbGubQk4GlD42MuQL5JGOABrxdpqqWeovlVQ==", + "requires": { + "@types/anymatch": "*", + "@types/node": "*", + "@types/tapable": "*", + "@types/uglify-js": "*", + "@types/webpack-sources": "*", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "@types/webpack-sources": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-2.1.0.tgz", + "integrity": "sha512-LXn/oYIpBeucgP1EIJbKQ2/4ZmpvRl+dlrFdX7+94SKRUV3Evy3FsfMZY318vGhkWUS5MPhtOM3w1/hCOAOXcg==", + "requires": { + "@types/node": "*", + "@types/source-list-map": "*", + "source-map": "^0.7.3" + }, + "dependencies": { + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" + } + } + }, + "@types/yargs": { + "version": "15.0.10", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.10.tgz", + "integrity": "sha512-z8PNtlhrj7eJNLmrAivM7rjBESG6JwC5xP3RVk12i/8HVP7Xnx/sEmERnRImyEuUaJfO942X0qMOYsoupaJbZQ==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-15.0.0.tgz", + "integrity": "sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw==" + }, + "@typescript-eslint/eslint-plugin": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.9.0.tgz", + "integrity": "sha512-WrVzGMzzCrgrpnQMQm4Tnf+dk+wdl/YbgIgd5hKGa2P+lnJ2MON+nQnbwgbxtN9QDLi8HO+JAq0/krMnjQK6Cw==", + "requires": { + "@typescript-eslint/experimental-utils": "4.9.0", + "@typescript-eslint/scope-manager": "4.9.0", + "debug": "^4.1.1", + "functional-red-black-tree": "^1.0.1", + "regexpp": "^3.0.0", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + } + }, + "@typescript-eslint/experimental-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.9.0.tgz", + "integrity": "sha512-0p8GnDWB3R2oGhmRXlEnCvYOtaBCijtA5uBfH5GxQKsukdSQyI4opC4NGTUb88CagsoNQ4rb/hId2JuMbzWKFQ==", + "requires": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/scope-manager": "4.9.0", + "@typescript-eslint/types": "4.9.0", + "@typescript-eslint/typescript-estree": "4.9.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" + } + }, + "@typescript-eslint/parser": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.9.0.tgz", + "integrity": "sha512-QRSDAV8tGZoQye/ogp28ypb8qpsZPV6FOLD+tbN4ohKUWHD2n/u0Q2tIBnCsGwQCiD94RdtLkcqpdK4vKcLCCw==", + "requires": { + "@typescript-eslint/scope-manager": "4.9.0", + "@typescript-eslint/types": "4.9.0", + "@typescript-eslint/typescript-estree": "4.9.0", + "debug": "^4.1.1" + } + }, + "@typescript-eslint/scope-manager": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.9.0.tgz", + "integrity": "sha512-q/81jtmcDtMRE+nfFt5pWqO0R41k46gpVLnuefqVOXl4QV1GdQoBWfk5REcipoJNQH9+F5l+dwa9Li5fbALjzg==", + "requires": { + "@typescript-eslint/types": "4.9.0", + "@typescript-eslint/visitor-keys": "4.9.0" + } + }, + "@typescript-eslint/types": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.9.0.tgz", + "integrity": "sha512-luzLKmowfiM/IoJL/rus1K9iZpSJK6GlOS/1ezKplb7MkORt2dDcfi8g9B0bsF6JoRGhqn0D3Va55b+vredFHA==" + }, + "@typescript-eslint/typescript-estree": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.9.0.tgz", + "integrity": "sha512-rmDR++PGrIyQzAtt3pPcmKWLr7MA+u/Cmq9b/rON3//t5WofNR4m/Ybft2vOLj0WtUzjn018ekHjTsnIyBsQug==", + "requires": { + "@typescript-eslint/types": "4.9.0", + "@typescript-eslint/visitor-keys": "4.9.0", + "debug": "^4.1.1", + "globby": "^11.0.1", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.9.0.tgz", + "integrity": "sha512-sV45zfdRqQo1A97pOSx3fsjR+3blmwtdCt8LDrXgCX36v4Vmz4KHrhpV6Fo2cRdXmyumxx11AHw0pNJqCNpDyg==", + "requires": { + "@typescript-eslint/types": "4.9.0", + "eslint-visitor-keys": "^2.0.0" + } + }, + "@webassemblyjs/ast": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", + "integrity": "sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA==", + "requires": { + "@webassemblyjs/helper-module-context": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/wast-parser": "1.9.0" + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz", + "integrity": "sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA==" + }, + "@webassemblyjs/helper-api-error": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz", + "integrity": "sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw==" + }, + "@webassemblyjs/helper-buffer": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz", + "integrity": "sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA==" + }, + "@webassemblyjs/helper-code-frame": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.0.tgz", + "integrity": "sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA==", + "requires": { + "@webassemblyjs/wast-printer": "1.9.0" + } + }, + "@webassemblyjs/helper-fsm": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.9.0.tgz", + "integrity": "sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw==" + }, + "@webassemblyjs/helper-module-context": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.9.0.tgz", + "integrity": "sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g==", + "requires": { + "@webassemblyjs/ast": "1.9.0" + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz", + "integrity": "sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw==" + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz", + "integrity": "sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw==", + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0" + } + }, + "@webassemblyjs/ieee754": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz", + "integrity": "sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg==", + "requires": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "@webassemblyjs/leb128": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.9.0.tgz", + "integrity": "sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw==", + "requires": { + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/utf8": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.9.0.tgz", + "integrity": "sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w==" + }, + "@webassemblyjs/wasm-edit": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz", + "integrity": "sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw==", + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/helper-wasm-section": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0", + "@webassemblyjs/wasm-opt": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0", + "@webassemblyjs/wast-printer": "1.9.0" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz", + "integrity": "sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA==", + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/ieee754": "1.9.0", + "@webassemblyjs/leb128": "1.9.0", + "@webassemblyjs/utf8": "1.9.0" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz", + "integrity": "sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A==", + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz", + "integrity": "sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA==", + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-api-error": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/ieee754": "1.9.0", + "@webassemblyjs/leb128": "1.9.0", + "@webassemblyjs/utf8": "1.9.0" + } + }, + "@webassemblyjs/wast-parser": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.9.0.tgz", + "integrity": "sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw==", + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/floating-point-hex-parser": "1.9.0", + "@webassemblyjs/helper-api-error": "1.9.0", + "@webassemblyjs/helper-code-frame": "1.9.0", + "@webassemblyjs/helper-fsm": "1.9.0", + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz", + "integrity": "sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA==", + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/wast-parser": "1.9.0", + "@xtuc/long": "4.2.2" + } + }, + "@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==" + }, + "@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" + }, + "@zeit/schemas": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.6.0.tgz", + "integrity": "sha512-uUrgZ8AxS+Lio0fZKAipJjAh415JyrOZowliZAzmnJSsf7piVL5w+G0+gFJ0KSu3QRhvui/7zuvpLz03YjXAhg==" + }, + "abab": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", + "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==" + }, + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==" + }, + "acorn-globals": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", + "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", + "requires": { + "acorn": "^7.1.1", + "acorn-walk": "^7.1.1" + } + }, + "acorn-jsx": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz", + "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==" + }, + "acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==" + }, + "address": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/address/-/address-1.1.2.tgz", + "integrity": "sha512-aT6camzM4xEA54YVJYSqxz1kv4IHnQZRtThJJHhUMRExaU5spC7jX5ugSwTaTgJliIgs4VhZOk7htClvQ/LmRA==" + }, + "adjust-sourcemap-loader": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-3.0.0.tgz", + "integrity": "sha512-YBrGyT2/uVQ/c6Rr+t6ZJXniY03YtHGMJQYal368burRGYKqhx9qGTWqcBU5s1CwYY9E/ri63RYyG1IacMZtqw==", + "requires": { + "loader-utils": "^2.0.0", + "regex-parser": "^2.2.11" + } + }, + "aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + } + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-errors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", + "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==" + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==" + }, + "alphanum-sort": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", + "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=" + }, + "ansi-align": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz", + "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", + "requires": { + "string-width": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==" + }, + "ansi-escapes": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", + "requires": { + "type-fest": "^0.11.0" + }, + "dependencies": { + "type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==" + } + } + }, + "ansi-html": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz", + "integrity": "sha1-gTWEAhliqenm/QOflA0S9WynhZ4=" + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + }, + "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" + } + }, + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + }, + "arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==" + }, + "arg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arg/-/arg-2.0.0.tgz", + "integrity": "sha512-XxNTUzKnz1ctK3ZIcI2XUPlD96wbHP2nGqkPKpvk/HNRlPveYrXIVSTk9m3LcqOgDPg3B1nMvdV/K8wZd7PG4w==" + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "aria-query": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz", + "integrity": "sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==", + "requires": { + "@babel/runtime": "^7.10.2", + "@babel/runtime-corejs3": "^7.10.2" + } + }, + "arity-n": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arity-n/-/arity-n-1.0.4.tgz", + "integrity": "sha1-2edrEXM+CFacCEeuezmyhgswt0U=" + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" + }, + "array-flatten": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", + "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==" + }, + "array-includes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.2.tgz", + "integrity": "sha512-w2GspexNQpx+PutG3QpT437/BenZBj0M/MZGn5mzv/MofYqo0xmRHzn4lFsoDlWJ+THYsGJmFlW68WlDFx7VRw==", + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1", + "get-intrinsic": "^1.0.1", + "is-string": "^1.0.5" + } + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==" + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=" + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" + }, + "array.prototype.flat": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.4.tgz", + "integrity": "sha512-4470Xi3GAPAjZqFcljX2xzckv1qeKPizoNkiS0+O4IoPR2ZNpcjE0pkhdihlDouK+x6QOast26B4Q/O9DJnwSg==", + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1" + } + }, + "array.prototype.flatmap": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.2.4.tgz", + "integrity": "sha512-r9Z0zYoxqHz60vvQbWEdXIEtCwHF0yxaWfno9qzXeNHvfyl3BZqygmGzb84dsubyaXLH4husF+NFgMSdpZhk2Q==", + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1", + "function-bind": "^1.1.1" + } + }, + "arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==" + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" + }, + "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" + } + }, + "asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" + } + } + }, + "assert": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", + "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", + "requires": { + "object-assign": "^4.1.1", + "util": "0.10.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=" + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "requires": { + "inherits": "2.0.1" + } + } + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" + }, + "ast-types-flow": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", + "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=" + }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==" + }, + "async": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "requires": { + "lodash": "^4.17.14" + } + }, + "async-each": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", + "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==" + }, + "async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==" + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" + }, + "autoprefixer": { + "version": "9.8.6", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.6.tgz", + "integrity": "sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg==", + "requires": { + "browserslist": "^4.12.0", + "caniuse-lite": "^1.0.30001109", + "colorette": "^1.2.1", + "normalize-range": "^0.1.2", + "num2fraction": "^1.2.2", + "postcss": "^7.0.32", + "postcss-value-parser": "^4.1.0" + } + }, + "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.11.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" + }, + "axe-core": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.1.1.tgz", + "integrity": "sha512-5Kgy8Cz6LPC9DJcNb3yjAXTu3XihQgEdnIg50c//zOC/MyLP0Clg+Y8Sh9ZjjnvBrDZU4DgXS9C3T9r4/scGZQ==" + }, + "axobject-query": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", + "integrity": "sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==" + }, + "babel-eslint": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", + "integrity": "sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==", + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.7.0", + "@babel/traverse": "^7.7.0", + "@babel/types": "^7.7.0", + "eslint-visitor-keys": "^1.0.0", + "resolve": "^1.12.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==" + } + } + }, + "babel-extract-comments": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/babel-extract-comments/-/babel-extract-comments-1.0.0.tgz", + "integrity": "sha512-qWWzi4TlddohA91bFwgt6zO/J0X+io7Qp184Fw0m2JYRSTZnJbFR8+07KmzudHCZgOiKRCrjhylwv9Xd8gfhVQ==", + "requires": { + "babylon": "^6.18.0" + } + }, + "babel-jest": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-26.6.3.tgz", + "integrity": "sha512-pl4Q+GAVOHwvjrck6jKjvmGhnO3jHX/xuB9d27f+EJZ/6k+6nMuPjorrYp7s++bKKdANwzElBWnLWaObvTnaZA==", + "requires": { + "@jest/transform": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/babel__core": "^7.1.7", + "babel-plugin-istanbul": "^6.0.0", + "babel-preset-jest": "^26.6.2", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "slash": "^3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "babel-loader": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.1.0.tgz", + "integrity": "sha512-7q7nC1tYOrqvUrN3LQK4GwSk/TQorZSOlO9C+RZDZpODgyN4ZlCqE5q9cDsyWOliN+aU9B4JX01xK9eJXowJLw==", + "requires": { + "find-cache-dir": "^2.1.0", + "loader-utils": "^1.4.0", + "mkdirp": "^0.5.3", + "pify": "^4.0.1", + "schema-utils": "^2.6.5" + }, + "dependencies": { + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", + "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + } + } + } + }, + "babel-plugin-dynamic-import-node": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", + "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", + "requires": { + "object.assign": "^4.1.0" + } + }, + "babel-plugin-istanbul": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz", + "integrity": "sha512-AF55rZXpe7trmEylbaE1Gv54wn6rwU03aptvRoVIGP8YykoSxqdVLV1TfwflBCE/QtHmqtP8SWlTENqbK8GCSQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^4.0.0", + "test-exclude": "^6.0.0" + } + }, + "babel-plugin-jest-hoist": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.6.2.tgz", + "integrity": "sha512-PO9t0697lNTmcEHH69mdtYiOIkkOlj9fySqfO3K1eCcdISevLAE0xY59VLLUj0SoiPiTX/JU2CYFpILydUa5Lw==", + "requires": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.0.0", + "@types/babel__traverse": "^7.0.6" + } + }, + "babel-plugin-macros": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz", + "integrity": "sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==", + "requires": { + "@babel/runtime": "^7.7.2", + "cosmiconfig": "^6.0.0", + "resolve": "^1.12.0" + }, + "dependencies": { + "cosmiconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", + "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.7.2" + } + } + } + }, + "babel-plugin-named-asset-import": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.7.tgz", + "integrity": "sha512-squySRkf+6JGnvjoUtDEjSREJEBirnXi9NqP6rjSYsylxQxqBTz+pkmf395i9E2zsvmYUaI40BHo6SqZUdydlw==" + }, + "babel-plugin-syntax-object-rest-spread": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", + "integrity": "sha1-/WU28rzhODb/o6VFjEkDpZe7O/U=" + }, + "babel-plugin-transform-object-rest-spread": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz", + "integrity": "sha1-DzZpLVD+9rfi1LOsFHgTepY7ewY=", + "requires": { + "babel-plugin-syntax-object-rest-spread": "^6.8.0", + "babel-runtime": "^6.26.0" + } + }, + "babel-plugin-transform-react-remove-prop-types": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz", + "integrity": "sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA==" + }, + "babel-preset-current-node-syntax": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.0.tgz", + "integrity": "sha512-mGkvkpocWJes1CmMKtgGUwCeeq0pOhALyymozzDWYomHTbDLwueDYG6p4TK1YOeYHCzBzYPsWkgTto10JubI1Q==", + "requires": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + } + }, + "babel-preset-jest": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-26.6.2.tgz", + "integrity": "sha512-YvdtlVm9t3k777c5NPQIv6cxFFFapys25HiUmuSgHwIZhfifweR5c5Sf5nwE3MAbfu327CYSvps8Yx6ANLyleQ==", + "requires": { + "babel-plugin-jest-hoist": "^26.6.2", + "babel-preset-current-node-syntax": "^1.0.0" + } + }, + "babel-preset-react-app": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/babel-preset-react-app/-/babel-preset-react-app-10.0.0.tgz", + "integrity": "sha512-itL2z8v16khpuKutx5IH8UdCdSTuzrOhRFTEdIhveZ2i1iBKDrVE0ATa4sFVy+02GLucZNVBWtoarXBy0Msdpg==", + "requires": { + "@babel/core": "7.12.3", + "@babel/plugin-proposal-class-properties": "7.12.1", + "@babel/plugin-proposal-decorators": "7.12.1", + "@babel/plugin-proposal-nullish-coalescing-operator": "7.12.1", + "@babel/plugin-proposal-numeric-separator": "7.12.1", + "@babel/plugin-proposal-optional-chaining": "7.12.1", + "@babel/plugin-transform-flow-strip-types": "7.12.1", + "@babel/plugin-transform-react-display-name": "7.12.1", + "@babel/plugin-transform-runtime": "7.12.1", + "@babel/preset-env": "7.12.1", + "@babel/preset-react": "7.12.1", + "@babel/preset-typescript": "7.12.1", + "@babel/runtime": "7.12.1", + "babel-plugin-macros": "2.8.0", + "babel-plugin-transform-react-remove-prop-types": "0.4.24" + }, + "dependencies": { + "@babel/plugin-proposal-numeric-separator": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.12.1.tgz", + "integrity": "sha512-MR7Ok+Af3OhNTCxYVjJZHS0t97ydnJZt/DbR4WISO39iDnhiD8XHrY12xuSJ90FFEGjir0Fzyyn7g/zY6hxbxA==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + } + }, + "@babel/plugin-proposal-optional-chaining": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.12.1.tgz", + "integrity": "sha512-c2uRpY6WzaVDzynVY9liyykS+kVU+WRZPMPYpkelXH8KBt1oXoI89kPbZKKG/jDT5UK92FTW2fZkZaJhdiBabw==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-skip-transparent-expression-wrappers": "^7.12.1", + "@babel/plugin-syntax-optional-chaining": "^7.8.0" + } + }, + "@babel/preset-env": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.12.1.tgz", + "integrity": "sha512-H8kxXmtPaAGT7TyBvSSkoSTUK6RHh61So05SyEbpmr0MCZrsNYn7mGMzzeYoOUCdHzww61k8XBft2TaES+xPLg==", + "requires": { + "@babel/compat-data": "^7.12.1", + "@babel/helper-compilation-targets": "^7.12.1", + "@babel/helper-module-imports": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-validator-option": "^7.12.1", + "@babel/plugin-proposal-async-generator-functions": "^7.12.1", + "@babel/plugin-proposal-class-properties": "^7.12.1", + "@babel/plugin-proposal-dynamic-import": "^7.12.1", + "@babel/plugin-proposal-export-namespace-from": "^7.12.1", + "@babel/plugin-proposal-json-strings": "^7.12.1", + "@babel/plugin-proposal-logical-assignment-operators": "^7.12.1", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.1", + "@babel/plugin-proposal-numeric-separator": "^7.12.1", + "@babel/plugin-proposal-object-rest-spread": "^7.12.1", + "@babel/plugin-proposal-optional-catch-binding": "^7.12.1", + "@babel/plugin-proposal-optional-chaining": "^7.12.1", + "@babel/plugin-proposal-private-methods": "^7.12.1", + "@babel/plugin-proposal-unicode-property-regex": "^7.12.1", + "@babel/plugin-syntax-async-generators": "^7.8.0", + "@babel/plugin-syntax-class-properties": "^7.12.1", + "@babel/plugin-syntax-dynamic-import": "^7.8.0", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.0", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.0", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.0", + "@babel/plugin-syntax-top-level-await": "^7.12.1", + "@babel/plugin-transform-arrow-functions": "^7.12.1", + "@babel/plugin-transform-async-to-generator": "^7.12.1", + "@babel/plugin-transform-block-scoped-functions": "^7.12.1", + "@babel/plugin-transform-block-scoping": "^7.12.1", + "@babel/plugin-transform-classes": "^7.12.1", + "@babel/plugin-transform-computed-properties": "^7.12.1", + "@babel/plugin-transform-destructuring": "^7.12.1", + "@babel/plugin-transform-dotall-regex": "^7.12.1", + "@babel/plugin-transform-duplicate-keys": "^7.12.1", + "@babel/plugin-transform-exponentiation-operator": "^7.12.1", + "@babel/plugin-transform-for-of": "^7.12.1", + "@babel/plugin-transform-function-name": "^7.12.1", + "@babel/plugin-transform-literals": "^7.12.1", + "@babel/plugin-transform-member-expression-literals": "^7.12.1", + "@babel/plugin-transform-modules-amd": "^7.12.1", + "@babel/plugin-transform-modules-commonjs": "^7.12.1", + "@babel/plugin-transform-modules-systemjs": "^7.12.1", + "@babel/plugin-transform-modules-umd": "^7.12.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.12.1", + "@babel/plugin-transform-new-target": "^7.12.1", + "@babel/plugin-transform-object-super": "^7.12.1", + "@babel/plugin-transform-parameters": "^7.12.1", + "@babel/plugin-transform-property-literals": "^7.12.1", + "@babel/plugin-transform-regenerator": "^7.12.1", + "@babel/plugin-transform-reserved-words": "^7.12.1", + "@babel/plugin-transform-shorthand-properties": "^7.12.1", + "@babel/plugin-transform-spread": "^7.12.1", + "@babel/plugin-transform-sticky-regex": "^7.12.1", + "@babel/plugin-transform-template-literals": "^7.12.1", + "@babel/plugin-transform-typeof-symbol": "^7.12.1", + "@babel/plugin-transform-unicode-escapes": "^7.12.1", + "@babel/plugin-transform-unicode-regex": "^7.12.1", + "@babel/preset-modules": "^0.1.3", + "@babel/types": "^7.12.1", + "core-js-compat": "^3.6.2", + "semver": "^5.5.0" + } + }, + "@babel/preset-react": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.12.1.tgz", + "integrity": "sha512-euCExymHCi0qB9u5fKw7rvlw7AZSjw/NaB9h7EkdTt5+yHRrXdiRTh7fkG3uBPpJg82CqLfp1LHLqWGSCrab+g==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-transform-react-display-name": "^7.12.1", + "@babel/plugin-transform-react-jsx": "^7.12.1", + "@babel/plugin-transform-react-jsx-development": "^7.12.1", + "@babel/plugin-transform-react-jsx-self": "^7.12.1", + "@babel/plugin-transform-react-jsx-source": "^7.12.1", + "@babel/plugin-transform-react-pure-annotations": "^7.12.1" + } + }, + "@babel/runtime": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.1.tgz", + "integrity": "sha512-J5AIf3vPj3UwXaAzb5j1xM4WAQDX3EMgemF8rjCP3SoW09LfRKAXQKt6CoVYl230P6iWdRcBbnLDDdnqWxZSCA==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } + } + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "requires": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + }, + "dependencies": { + "core-js": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", + "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==" + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + } + } + }, + "babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==" + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=" + }, + "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" + } + }, + "bfj": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/bfj/-/bfj-7.0.2.tgz", + "integrity": "sha512-+e/UqUzwmzJamNF50tBV6tZPTORow7gQ96iFow+8b562OdMpEK0BcJEq2OSPEDmAbSMBQ7PKZ87ubFkgxpYWgw==", + "requires": { + "bluebird": "^3.5.5", + "check-types": "^11.1.1", + "hoopy": "^0.1.4", + "tryer": "^1.0.1" + } + }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==" + }, + "binary-extensions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", + "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", + "optional": true + }, + "bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" + }, + "bn.js": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.3.tgz", + "integrity": "sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ==" + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "dependencies": { + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + } + } + }, + "bonjour": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", + "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=", + "requires": { + "array-flatten": "^2.1.0", + "deep-equal": "^1.0.1", + "dns-equal": "^1.0.0", + "dns-txt": "^2.0.2", + "multicast-dns": "^6.0.1", + "multicast-dns-service-types": "^1.1.0" + } + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" + }, + "boxen": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", + "integrity": "sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==", + "requires": { + "ansi-align": "^2.0.0", + "camelcase": "^4.0.0", + "chalk": "^2.0.1", + "cli-boxes": "^1.0.0", + "string-width": "^2.0.0", + "term-size": "^1.2.0", + "widest-line": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "requires": { + "fill-range": "^7.0.1" + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" + }, + "browser-process-hrtime": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", + "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==" + }, + "browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "requires": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "requires": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "requires": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "browserify-rsa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", + "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", + "requires": { + "bn.js": "^5.0.0", + "randombytes": "^2.0.1" + } + }, + "browserify-sign": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", + "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", + "requires": { + "bn.js": "^5.1.1", + "browserify-rsa": "^4.0.1", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.5.3", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.5", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } + } + }, + "browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "requires": { + "pako": "~1.0.5" + } + }, + "browserslist": { + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.15.0.tgz", + "integrity": "sha512-IJ1iysdMkGmjjYeRlDU8PQejVwxvVO5QOfXH7ylW31GO6LwNRSmm/SgRXtNsEXqMLl2e+2H5eEJ7sfynF8TCaQ==", + "requires": { + "caniuse-lite": "^1.0.30001164", + "colorette": "^1.2.1", + "electron-to-chromium": "^1.3.612", + "escalade": "^3.1.1", + "node-releases": "^1.1.67" + } + }, + "bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "requires": { + "node-int64": "^0.4.0" + } + }, + "buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, + "buffer-indexof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz", + "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==" + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=" + }, + "builtin-modules": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.1.0.tgz", + "integrity": "sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw==" + }, + "builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=" + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + }, + "cacache": { + "version": "15.0.5", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.0.5.tgz", + "integrity": "sha512-lloiL22n7sOjEEXdL8NAjTgv9a1u43xICE9/203qonkZUCj5X1UEWIdf2/Y0d6QcCtMzbKQyhrcDbdvlZTs/+A==", + "requires": { + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.0", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "dependencies": { + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, + "call-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.0.tgz", + "integrity": "sha512-AEXsYIyyDY3MCzbwdhzG3Jx1R0J2wetQyUynn6dYHAO+bg8l1k7jwZtRv4ryryFs7EP+NDlikJlVe59jr0cM2w==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.0" + } + }, + "caller-callsite": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", + "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", + "requires": { + "callsites": "^2.0.0" + }, + "dependencies": { + "callsites": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=" + } + } + }, + "caller-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", + "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", + "requires": { + "caller-callsite": "^2.0.0" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" + }, + "camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "requires": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" + } + } + }, + "camelcase": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", + "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==" + }, + "caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "requires": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "caniuse-lite": { + "version": "1.0.30001164", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001164.tgz", + "integrity": "sha512-G+A/tkf4bu0dSp9+duNiXc7bGds35DioCyC6vgK2m/rjA4Krpy5WeZgZyfH2f0wj2kI6yAWWucyap6oOwmY1mg==" + }, + "capture-exit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", + "integrity": "sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==", + "requires": { + "rsvp": "^4.8.4" + } + }, + "case-sensitive-paths-webpack-plugin": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.3.0.tgz", + "integrity": "sha512-/4YgnZS8y1UXXmC02xD5rRrBEu6T5ub+mQHLNRj0fzTRbgdBYhsNo2V5EqwgqrExjxsjtF/OpAKAMkKsxbD5XQ==" + }, + "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" + } + }, + "char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==" + }, + "check-types": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.1.2.tgz", + "integrity": "sha512-tzWzvgePgLORb9/3a0YenggReLKAIb2owL03H2Xdoe5pKcUyWRSEQ8xfCar8t2SIAuEDwtmx2da1YB52YuHQMQ==" + }, + "chokidar": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.3.tgz", + "integrity": "sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ==", + "optional": true, + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.1.2", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.5.0" + }, + "dependencies": { + "fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "optional": true + } + } + }, + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" + }, + "chrome-trace-event": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz", + "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==", + "requires": { + "tslib": "^1.9.0" + } + }, + "ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==" + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "cjs-module-lexer": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-0.6.0.tgz", + "integrity": "sha512-uc2Vix1frTfnuzxxu1Hp4ktSvM3QaI4oXl4ZUqL1wjTu/BGki9TrCWoqLTg/drR1KwAEarXuRFCG2Svr1GxPFw==" + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "clean-css": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", + "integrity": "sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==", + "requires": { + "source-map": "~0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==" + }, + "cli-boxes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", + "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=" + }, + "clipboardy": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-1.2.3.tgz", + "integrity": "sha512-2WNImOvCRe6r63Gk9pShfkwXsVtKCroMAevIbiae021mS850UkWPbevxsBz3tnvjZIEGvlwaqCPsw+4ulzNgJA==", + "requires": { + "arch": "^2.1.0", + "execa": "^0.8.0" + }, + "dependencies": { + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "requires": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "execa": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.8.0.tgz", + "integrity": "sha1-2NdrvBtVIX7RkP1t1J08d07PyNo=", + "requires": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" + }, + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + } + } + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + }, + "dependencies": { + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + } + } + }, + "clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "requires": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + } + }, + "clsx": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz", + "integrity": "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==" + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "coa": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", + "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==", + "requires": { + "@types/q": "^1.5.1", + "chalk": "^2.4.1", + "q": "^1.1.2" + } + }, + "collect-v8-coverage": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", + "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==" + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/color/-/color-3.1.3.tgz", + "integrity": "sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ==", + "requires": { + "color-convert": "^1.9.1", + "color-string": "^1.5.4" + } + }, + "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=" + }, + "color-string": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.4.tgz", + "integrity": "sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw==", + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "colorette": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz", + "integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==" + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==" + }, + "common-tags": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.0.tgz", + "integrity": "sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw==" + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=" + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + }, + "compose-function": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/compose-function/-/compose-function-3.0.3.tgz", + "integrity": "sha1-ntZ18TzFRQHTCVCkhv9qe6OrGF8=", + "requires": { + "arity-n": "^1.0.4" + } + }, + "compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "requires": { + "mime-db": ">= 1.43.0 < 2" + } + }, + "compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "requires": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "confusing-browser-globals": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.10.tgz", + "integrity": "sha512-gNld/3lySHwuhaVluJUKLePYirM3QNCKzVxqAdhJII9/WXKVX5PURzMVJspS1jTslSqjeuG4KMVTSouit5YPHA==" + }, + "connect-history-api-fallback": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", + "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==" + }, + "console-browserify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", + "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==" + }, + "constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=" + }, + "contains-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", + "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=" + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "copy-concurrently": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", + "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", + "requires": { + "aproba": "^1.1.1", + "fs-write-stream-atomic": "^1.0.8", + "iferr": "^0.1.5", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.0" + } + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" + }, + "core-js": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.8.0.tgz", + "integrity": "sha512-W2VYNB0nwQQE7tKS7HzXd7r2y/y2SVJl4ga6oH/dnaLFzM0o2lB2P3zCkWj5Wc/zyMYjtgd5Hmhk0ObkQFZOIA==" + }, + "core-js-compat": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.8.0.tgz", + "integrity": "sha512-o9QKelQSxQMYWHXc/Gc4L8bx/4F7TTraE5rhuN8I7mKBt5dBIUpXpIR3omv70ebr8ST5R3PqbDQr+ZI3+Tt1FQ==", + "requires": { + "browserslist": "^4.14.7", + "semver": "7.0.0" + }, + "dependencies": { + "semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==" + } + } + }, + "core-js-pure": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.8.0.tgz", + "integrity": "sha512-fRjhg3NeouotRoIV0L1FdchA6CK7ZD+lyINyMoz19SyV+ROpC4noS1xItWHFtwZdlqfMfVPJEyEGdfri2bD1pA==" + }, + "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=" + }, + "cosmiconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.0.tgz", + "integrity": "sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA==", + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + } + }, + "create-ecdh": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", + "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", + "requires": { + "bn.js": "^4.1.0", + "elliptic": "^6.5.3" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" + } + } + }, + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } + } + }, + "crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "requires": { + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0", + "randomfill": "^1.0.3" + } + }, + "crypto-random-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", + "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=" + }, + "css": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz", + "integrity": "sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==", + "requires": { + "inherits": "^2.0.3", + "source-map": "^0.6.1", + "source-map-resolve": "^0.5.2", + "urix": "^0.1.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "css-blank-pseudo": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz", + "integrity": "sha512-LHz35Hr83dnFeipc7oqFDmsjHdljj3TQtxGGiNWSOsTLIAubSm4TEz8qCaKFpk7idaQ1GfWscF4E6mgpBysA1w==", + "requires": { + "postcss": "^7.0.5" + } + }, + "css-color-names": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", + "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=" + }, + "css-declaration-sorter": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz", + "integrity": "sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA==", + "requires": { + "postcss": "^7.0.1", + "timsort": "^0.3.0" + } + }, + "css-has-pseudo": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-0.10.0.tgz", + "integrity": "sha512-Z8hnfsZu4o/kt+AuFzeGpLVhFOGO9mluyHBaA2bA8aCGTwah5sT3WV/fTHH8UNZUytOIImuGPrl/prlb4oX4qQ==", + "requires": { + "postcss": "^7.0.6", + "postcss-selector-parser": "^5.0.0-rc.4" + }, + "dependencies": { + "cssesc": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz", + "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==" + }, + "postcss-selector-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz", + "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==", + "requires": { + "cssesc": "^2.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + } + } + }, + "css-loader": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-4.3.0.tgz", + "integrity": "sha512-rdezjCjScIrsL8BSYszgT4s476IcNKt6yX69t0pHjJVnPUTDpn4WfIpDQTN3wCJvUvfsz/mFjuGOekf3PY3NUg==", + "requires": { + "camelcase": "^6.0.0", + "cssesc": "^3.0.0", + "icss-utils": "^4.1.1", + "loader-utils": "^2.0.0", + "postcss": "^7.0.32", + "postcss-modules-extract-imports": "^2.0.0", + "postcss-modules-local-by-default": "^3.0.3", + "postcss-modules-scope": "^2.2.0", + "postcss-modules-values": "^3.0.0", + "postcss-value-parser": "^4.1.0", + "schema-utils": "^2.7.1", + "semver": "^7.3.2" + } + }, + "css-prefers-color-scheme": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-3.1.1.tgz", + "integrity": "sha512-MTu6+tMs9S3EUqzmqLXEcgNRbNkkD/TGFvowpeoWJn5Vfq7FMgsmRQs9X5NXAURiOBmOxm/lLjsDNXDE6k9bhg==", + "requires": { + "postcss": "^7.0.5" + } + }, + "css-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", + "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==", + "requires": { + "boolbase": "^1.0.0", + "css-what": "^3.2.1", + "domutils": "^1.7.0", + "nth-check": "^1.0.2" + } + }, + "css-select-base-adapter": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", + "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==" + }, + "css-tree": { + "version": "1.0.0-alpha.37", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", + "integrity": "sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==", + "requires": { + "mdn-data": "2.0.4", + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "css-vendor": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/css-vendor/-/css-vendor-2.0.8.tgz", + "integrity": "sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==", + "requires": { + "@babel/runtime": "^7.8.3", + "is-in-browser": "^1.0.2" + } + }, + "css-what": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz", + "integrity": "sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==" + }, + "css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s=" + }, + "cssdb": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-4.4.0.tgz", + "integrity": "sha512-LsTAR1JPEM9TpGhl/0p3nQecC2LJ0kD8X5YARu1hk/9I1gril5vDtMZyNxcEpxxDj34YNck/ucjuoUd66K03oQ==" + }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==" + }, + "cssnano": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-4.1.10.tgz", + "integrity": "sha512-5wny+F6H4/8RgNlaqab4ktc3e0/blKutmq8yNlBFXA//nSFFAqAngjNVRzUvCgYROULmZZUoosL/KSoZo5aUaQ==", + "requires": { + "cosmiconfig": "^5.0.0", + "cssnano-preset-default": "^4.0.7", + "is-resolvable": "^1.0.0", + "postcss": "^7.0.0" + }, + "dependencies": { + "cosmiconfig": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", + "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", + "requires": { + "import-fresh": "^2.0.0", + "is-directory": "^0.3.1", + "js-yaml": "^3.13.1", + "parse-json": "^4.0.0" + } + }, + "import-fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", + "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", + "requires": { + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=" + } + } + }, + "cssnano-preset-default": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-4.0.7.tgz", + "integrity": "sha512-x0YHHx2h6p0fCl1zY9L9roD7rnlltugGu7zXSKQx6k2rYw0Hi3IqxcoAGF7u9Q5w1nt7vK0ulxV8Lo+EvllGsA==", + "requires": { + "css-declaration-sorter": "^4.0.1", + "cssnano-util-raw-cache": "^4.0.1", + "postcss": "^7.0.0", + "postcss-calc": "^7.0.1", + "postcss-colormin": "^4.0.3", + "postcss-convert-values": "^4.0.1", + "postcss-discard-comments": "^4.0.2", + "postcss-discard-duplicates": "^4.0.2", + "postcss-discard-empty": "^4.0.1", + "postcss-discard-overridden": "^4.0.1", + "postcss-merge-longhand": "^4.0.11", + "postcss-merge-rules": "^4.0.3", + "postcss-minify-font-values": "^4.0.2", + "postcss-minify-gradients": "^4.0.2", + "postcss-minify-params": "^4.0.2", + "postcss-minify-selectors": "^4.0.2", + "postcss-normalize-charset": "^4.0.1", + "postcss-normalize-display-values": "^4.0.2", + "postcss-normalize-positions": "^4.0.2", + "postcss-normalize-repeat-style": "^4.0.2", + "postcss-normalize-string": "^4.0.2", + "postcss-normalize-timing-functions": "^4.0.2", + "postcss-normalize-unicode": "^4.0.1", + "postcss-normalize-url": "^4.0.1", + "postcss-normalize-whitespace": "^4.0.2", + "postcss-ordered-values": "^4.1.2", + "postcss-reduce-initial": "^4.0.3", + "postcss-reduce-transforms": "^4.0.2", + "postcss-svgo": "^4.0.2", + "postcss-unique-selectors": "^4.0.1" + } + }, + "cssnano-util-get-arguments": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz", + "integrity": "sha1-7ToIKZ8h11dBsg87gfGU7UnMFQ8=" + }, + "cssnano-util-get-match": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz", + "integrity": "sha1-wOTKB/U4a7F+xeUiULT1lhNlFW0=" + }, + "cssnano-util-raw-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz", + "integrity": "sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA==", + "requires": { + "postcss": "^7.0.0" + } + }, + "cssnano-util-same-parent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz", + "integrity": "sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q==" + }, + "csso": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", + "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", + "requires": { + "css-tree": "^1.1.2" + }, + "dependencies": { + "css-tree": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.2.tgz", + "integrity": "sha512-wCoWush5Aeo48GLhfHPbmvZs59Z+M7k5+B1xDnXbdWNcEF423DoFdqSWE0PM5aNk5nI5cp1q7ms36zGApY/sKQ==", + "requires": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + } + }, + "mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "cssom": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", + "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==" + }, + "cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "requires": { + "cssom": "~0.3.6" + }, + "dependencies": { + "cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==" + } + } + }, + "csstype": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.5.tgz", + "integrity": "sha512-uVDi8LpBUKQj6sdxNaTetL6FpeCqTjOvAQuQUa/qAqq8oOd4ivkbhgnqayl0dnPal8Tb/yB1tF+gOvCBiicaiQ==" + }, + "cyclist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", + "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=" + }, + "d": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", + "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", + "requires": { + "es5-ext": "^0.10.50", + "type": "^1.0.1" + } + }, + "damerau-levenshtein": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz", + "integrity": "sha512-JVrozIeElnj3QzfUIt8tB8YMluBJom4Vw9qTPpjGYQ9fYlB3D/rb6OordUxf3xeFB35LKWs0xqcO5U6ySvBtug==" + }, + "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" + } + }, + "data-urls": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", + "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", + "requires": { + "abab": "^2.0.3", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.0.0" + } + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + }, + "decimal.js": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.2.1.tgz", + "integrity": "sha512-KaL7+6Fw6i5A2XSnsbhm/6B+NuEA7TZ4vqxnd5tXz9sbKtrN9Srj8ab4vKVdK8YAqZO9P1kg45Y6YLoduPf+kw==" + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" + }, + "dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=" + }, + "deep-equal": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", + "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", + "requires": { + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.1", + "is-regex": "^1.0.4", + "object-is": "^1.0.1", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.2.0" + } + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" + }, + "deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==" + }, + "default-gateway": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz", + "integrity": "sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA==", + "requires": { + "execa": "^1.0.0", + "ip-regex": "^2.1.0" + } + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "requires": { + "object-keys": "^1.0.12" + } + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "del": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", + "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==", + "requires": { + "@types/glob": "^7.1.1", + "globby": "^6.1.0", + "is-path-cwd": "^2.0.0", + "is-path-in-cwd": "^2.0.0", + "p-map": "^2.0.0", + "pify": "^4.0.1", + "rimraf": "^2.6.3" + }, + "dependencies": { + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "requires": { + "array-uniq": "^1.0.1" + } + }, + "globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", + "requires": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + } + } + }, + "p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==" + } + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "des.js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", + "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", + "requires": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==" + }, + "detect-node": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz", + "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==" + }, + "detect-port-alt": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz", + "integrity": "sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q==", + "requires": { + "address": "^1.0.1", + "debug": "^2.6.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "diff-sequences": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", + "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==" + }, + "diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "requires": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" + } + } + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "requires": { + "path-type": "^4.0.0" + } + }, + "dns-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", + "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=" + }, + "dns-packet": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.1.tgz", + "integrity": "sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==", + "requires": { + "ip": "^1.1.0", + "safe-buffer": "^5.0.1" + } + }, + "dns-txt": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz", + "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=", + "requires": { + "buffer-indexof": "^1.0.0" + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "requires": { + "esutils": "^2.0.2" + } + }, + "dom-accessibility-api": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.3.0.tgz", + "integrity": "sha512-PzwHEmsRP3IGY4gv/Ug+rMeaTIyTJvadCb+ujYXYeIylbHJezIyNToe8KfEgHTCEYyC+/bUghYOGg8yMGlZ6vA==" + }, + "dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "requires": { + "utila": "~0.4" + } + }, + "dom-helpers": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.0.tgz", + "integrity": "sha512-Ru5o9+V8CpunKnz5LGgWXkmrH/20cGKwcHwS4m73zIvs54CN9epEmT/HLqFJW3kXpakAFkEdzgy1hzlJe3E4OQ==", + "requires": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "dom-serializer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", + "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", + "requires": { + "domelementtype": "^2.0.1", + "entities": "^2.0.0" + }, + "dependencies": { + "domelementtype": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.1.0.tgz", + "integrity": "sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w==" + } + } + }, + "domain-browser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==" + }, + "domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" + }, + "domexception": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", + "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", + "requires": { + "webidl-conversions": "^5.0.0" + }, + "dependencies": { + "webidl-conversions": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", + "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==" + } + } + }, + "domhandler": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", + "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "requires": { + "domelementtype": "1" + } + }, + "domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" + } + } + }, + "dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "requires": { + "is-obj": "^2.0.0" + } + }, + "dotenv": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", + "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==" + }, + "dotenv-expand": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", + "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==" + }, + "duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==" + }, + "duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "requires": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "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" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "ejs": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.7.4.tgz", + "integrity": "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==" + }, + "electron-to-chromium": { + "version": "1.3.613", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.613.tgz", + "integrity": "sha512-c3gkahddiUalk7HLhTC7PsKzPZmovYFtgh+g3rZJ+dGokk4n4dzEoOBnoV8VU8ptvnGJMhrjM/lyXKSltqf2hQ==" + }, + "elliptic": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", + "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", + "requires": { + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" + } + } + }, + "emittery": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.7.2.tgz", + "integrity": "sha512-A8OG5SR/ij3SsJdWDJdkkSYUjQdCUx6APQXem0SaEePBSRg4eymGYwBkKo1Y6DU+af/Jn2dBQqDBvjnr9Vi8nQ==" + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + }, + "emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, + "enhanced-resolve": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz", + "integrity": "sha512-3e87LvavsdxyoCfGusJnrZ5G8SLPOFeHSNpZI/ATL9a5leXo2k0w6MKnbqhdBad9qTobSfB20Ld7UmgoNbAZkQ==", + "requires": { + "graceful-fs": "^4.1.2", + "memory-fs": "^0.5.0", + "tapable": "^1.0.0" + }, + "dependencies": { + "memory-fs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", + "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "requires": { + "ansi-colors": "^4.1.1" + } + }, + "entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==" + }, + "errno": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", + "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", + "requires": { + "prr": "~1.0.1" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "error-stack-parser": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.0.6.tgz", + "integrity": "sha512-d51brTeqC+BHlwF0BhPtcYgF5nlzf9ZZ0ZIUQNZpc9ZB9qw5IJ2diTrBY9jlCJkTLITYPjmiX6OWCwH+fuyNgQ==", + "requires": { + "stackframe": "^1.1.1" + } + }, + "es-abstract": { + "version": "1.18.0-next.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", + "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "es5-ext": { + "version": "0.10.53", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", + "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", + "requires": { + "es6-iterator": "~2.0.3", + "es6-symbol": "~3.1.3", + "next-tick": "~1.0.0" + } + }, + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "requires": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "es6-symbol": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", + "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", + "requires": { + "d": "^1.0.1", + "ext": "^1.1.2" + } + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "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=" + }, + "escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "requires": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + }, + "dependencies": { + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "requires": { + "prelude-ls": "~1.1.2" + } + } + } + }, + "eslint": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.14.0.tgz", + "integrity": "sha512-5YubdnPXrlrYAFCKybPuHIAH++PINe1pmKNc5wQRB9HSbqIK1ywAnntE3Wwua4giKu0bjligf1gLF6qxMGOYRA==", + "requires": { + "@babel/code-frame": "^7.0.0", + "@eslint/eslintrc": "^0.2.1", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.0", + "esquery": "^1.2.0", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash": "^4.17.19", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "requires": { + "type-fest": "^0.8.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==" + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "eslint-config-react-app": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-6.0.0.tgz", + "integrity": "sha512-bpoAAC+YRfzq0dsTk+6v9aHm/uqnDwayNAXleMypGl6CpxI9oXXscVHo4fk3eJPIn+rsbtNetB4r/ZIidFIE8A==", + "requires": { + "confusing-browser-globals": "^1.0.10" + } + }, + "eslint-import-resolver-node": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz", + "integrity": "sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA==", + "requires": { + "debug": "^2.6.9", + "resolve": "^1.13.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "eslint-module-utils": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz", + "integrity": "sha512-6j9xxegbqe8/kZY8cYpcp0xhbK0EgJlg3g9mib3/miLaExuuwc3n5UEfSnU6hWMbT0FAYVvDbL9RrRgpUeQIvA==", + "requires": { + "debug": "^2.6.9", + "pkg-dir": "^2.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "requires": { + "locate-path": "^2.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + }, + "pkg-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", + "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", + "requires": { + "find-up": "^2.1.0" + } + } + } + }, + "eslint-plugin-flowtype": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-5.2.0.tgz", + "integrity": "sha512-z7ULdTxuhlRJcEe1MVljePXricuPOrsWfScRXFhNzVD5dmTHWjIF57AxD0e7AbEoLSbjSsaA5S+hCg43WvpXJQ==", + "requires": { + "lodash": "^4.17.15", + "string-natural-compare": "^3.0.1" + } + }, + "eslint-plugin-import": { + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.22.1.tgz", + "integrity": "sha512-8K7JjINHOpH64ozkAhpT3sd+FswIZTfMZTjdx052pnWrgRCVfp8op9tbjpAk3DdUeI/Ba4C8OjdC0r90erHEOw==", + "requires": { + "array-includes": "^3.1.1", + "array.prototype.flat": "^1.2.3", + "contains-path": "^0.1.0", + "debug": "^2.6.9", + "doctrine": "1.5.0", + "eslint-import-resolver-node": "^0.3.4", + "eslint-module-utils": "^2.6.0", + "has": "^1.0.3", + "minimatch": "^3.0.4", + "object.values": "^1.1.1", + "read-pkg-up": "^2.0.0", + "resolve": "^1.17.0", + "tsconfig-paths": "^3.9.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "doctrine": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", + "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", + "requires": { + "esutils": "^2.0.2", + "isarray": "^1.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "eslint-plugin-jest": { + "version": "24.1.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-24.1.3.tgz", + "integrity": "sha512-dNGGjzuEzCE3d5EPZQ/QGtmlMotqnYWD/QpCZ1UuZlrMAdhG5rldh0N0haCvhGnUkSeuORS5VNROwF9Hrgn3Lg==", + "requires": { + "@typescript-eslint/experimental-utils": "^4.0.1" + } + }, + "eslint-plugin-jsx-a11y": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.4.1.tgz", + "integrity": "sha512-0rGPJBbwHoGNPU73/QCLP/vveMlM1b1Z9PponxO87jfr6tuH5ligXbDT6nHSSzBC8ovX2Z+BQu7Bk5D/Xgq9zg==", + "requires": { + "@babel/runtime": "^7.11.2", + "aria-query": "^4.2.2", + "array-includes": "^3.1.1", + "ast-types-flow": "^0.0.7", + "axe-core": "^4.0.2", + "axobject-query": "^2.2.0", + "damerau-levenshtein": "^1.0.6", + "emoji-regex": "^9.0.0", + "has": "^1.0.3", + "jsx-ast-utils": "^3.1.0", + "language-tags": "^1.0.5" + }, + "dependencies": { + "emoji-regex": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.0.tgz", + "integrity": "sha512-DNc3KFPK18bPdElMJnf/Pkv5TXhxFU3YFDEuGLDRtPmV4rkmCjBkCSEp22u6rBHdSN9Vlp/GK7k98prmE1Jgug==" + } + } + }, + "eslint-plugin-react": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.21.5.tgz", + "integrity": "sha512-8MaEggC2et0wSF6bUeywF7qQ46ER81irOdWS4QWxnnlAEsnzeBevk1sWh7fhpCghPpXb+8Ks7hvaft6L/xsR6g==", + "requires": { + "array-includes": "^3.1.1", + "array.prototype.flatmap": "^1.2.3", + "doctrine": "^2.1.0", + "has": "^1.0.3", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "object.entries": "^1.1.2", + "object.fromentries": "^2.0.2", + "object.values": "^1.1.1", + "prop-types": "^15.7.2", + "resolve": "^1.18.1", + "string.prototype.matchall": "^4.0.2" + }, + "dependencies": { + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "requires": { + "esutils": "^2.0.2" + } + } + } + }, + "eslint-plugin-react-hooks": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.2.0.tgz", + "integrity": "sha512-623WEiZJqxR7VdxFCKLI6d6LLpwJkGPYKODnkH3D7WpOG5KM8yWueBd8TLsNAetEJNF5iJmolaAKO3F8yzyVBQ==" + }, + "eslint-plugin-testing-library": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-3.10.1.tgz", + "integrity": "sha512-nQIFe2muIFv2oR2zIuXE4vTbcFNx8hZKRzgHZqJg8rfopIWwoTwtlbCCNELT/jXzVe1uZF68ALGYoDXjLczKiQ==", + "requires": { + "@typescript-eslint/experimental-utils": "^3.10.1" + }, + "dependencies": { + "@typescript-eslint/experimental-utils": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-3.10.1.tgz", + "integrity": "sha512-DewqIgscDzmAfd5nOGe4zm6Bl7PKtMG2Ad0KG8CUZAHlXfAKTF9Ol5PXhiMh39yRL2ChRH1cuuUGOcVyyrhQIw==", + "requires": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/types": "3.10.1", + "@typescript-eslint/typescript-estree": "3.10.1", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" + } + }, + "@typescript-eslint/types": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-3.10.1.tgz", + "integrity": "sha512-+3+FCUJIahE9q0lDi1WleYzjCwJs5hIsbugIgnbB+dSCYUxl8L6PwmsyOPFZde2hc1DlTo/xnkOgiTLSyAbHiQ==" + }, + "@typescript-eslint/typescript-estree": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-3.10.1.tgz", + "integrity": "sha512-QbcXOuq6WYvnB3XPsZpIwztBoquEYLXh2MtwVU+kO8jgYCiv4G5xrSP/1wg4tkvrEE+esZVquIPX/dxPlePk1w==", + "requires": { + "@typescript-eslint/types": "3.10.1", + "@typescript-eslint/visitor-keys": "3.10.1", + "debug": "^4.1.1", + "glob": "^7.1.6", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-3.10.1.tgz", + "integrity": "sha512-9JgC82AaQeglebjZMgYR5wgmfUdUc+EitGUUMW8u2nDckaeimzW+VsoLV6FoimPv2id3VQzfjwBxEMVz08ameQ==", + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==" + } + } + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "requires": { + "eslint-visitor-keys": "^1.1.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==" + } + } + }, + "eslint-visitor-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz", + "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==" + }, + "eslint-webpack-plugin": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-2.4.1.tgz", + "integrity": "sha512-cj8iPWZKuAiVD8MMgTSunyMCAvxQxp5mxoPHZl1UMGkApFXaXJHdCFcCR+oZEJbBNhReNa5SjESIn34uqUbBtg==", + "requires": { + "@types/eslint": "^7.2.4", + "arrify": "^2.0.1", + "jest-worker": "^26.6.2", + "micromatch": "^4.0.2", + "schema-utils": "^3.0.0" + }, + "dependencies": { + "schema-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", + "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", + "requires": { + "@types/json-schema": "^7.0.6", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, + "espree": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.0.tgz", + "integrity": "sha512-dksIWsvKCixn1yrEXO8UosNSxaDoSYpq9reEjZSbHLpT5hpaCAKTLBwq0RHtLrIr+c0ByiYzWT8KTMRzoRCNlw==", + "requires": { + "acorn": "^7.4.0", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.3.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==" + } + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + }, + "esquery": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", + "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==" + } + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==" + } + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" + }, + "estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==" + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, + "events": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.2.0.tgz", + "integrity": "sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg==" + }, + "eventsource": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.0.7.tgz", + "integrity": "sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ==", + "requires": { + "original": "^1.0.0" + } + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "requires": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "exec-sh": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.4.tgz", + "integrity": "sha512-sEFIkc61v75sWeOe72qyrqg2Qg0OuLESziUDk/O/z2qgS15y2gWVFrI6f2Qn/qw/0/NCfCEsmNA4zOjkwEZT1A==" + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=" + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "expect": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/expect/-/expect-26.6.2.tgz", + "integrity": "sha512-9/hlOBkQl2l/PLHJx6JjoDF6xPKcJEsUlWKb23rKE7KzeDqUZKXKNMW27KIue5JMdBV9HgmoJPcc8HtO85t9IA==", + "requires": { + "@jest/types": "^26.6.2", + "ansi-styles": "^4.0.0", + "jest-get-type": "^26.3.0", + "jest-matcher-utils": "^26.6.2", + "jest-message-util": "^26.6.2", + "jest-regex-util": "^26.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + } + } + }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + } + } + }, + "ext": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz", + "integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==", + "requires": { + "type": "^2.0.0" + }, + "dependencies": { + "type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type/-/type-2.1.0.tgz", + "integrity": "sha512-G9absDWvhAWCV2gmF1zKud3OyC61nZDwWvBL2DApaVFogI07CprggiQAOOjvp2NRjYWFzPyu7vwtDrQFq8jeSA==" + } + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "fast-glob": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.4.tgz", + "integrity": "sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ==", + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.0", + "merge2": "^1.3.0", + "micromatch": "^4.0.2", + "picomatch": "^2.2.1" + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" + }, + "fast-url-parser": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz", + "integrity": "sha1-9K8+qfNNiicc9YrSs3WfQx8LMY0=", + "requires": { + "punycode": "^1.3.2" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + } + } + }, + "fastq": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.9.0.tgz", + "integrity": "sha512-i7FVWL8HhVY+CTkwFxkN2mk3h+787ixS5S63eb78diVRc1MCssarHq3W5cj0av7YDSwmaV928RNag+U1etRQ7w==", + "requires": { + "reusify": "^1.0.4" + } + }, + "faye-websocket": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", + "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=", + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "fb-watchman": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", + "integrity": "sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==", + "requires": { + "bser": "2.1.1" + } + }, + "figgy-pudding": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", + "integrity": "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==" + }, + "file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "requires": { + "flat-cache": "^2.0.1" + } + }, + "file-loader": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.1.1.tgz", + "integrity": "sha512-Klt8C4BjWSXYQAfhpYYkG4qHNTna4toMHEbWrI5IuVoxbU6uiDKeKAP99R8mmbJi3lvewn/jQBOgU4+NS3tDQw==", + "requires": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "dependencies": { + "schema-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", + "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", + "requires": { + "@types/json-schema": "^7.0.6", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, + "filesize": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz", + "integrity": "sha512-LpCHtPQ3sFx67z+uh2HnSyWSLLu5Jxo21795uRDuar/EOuYWXib5EmPaGIBuSnRqH2IODiKA2k5re/K9OnN/Yg==" + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "requires": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "requires": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + } + }, + "flatted": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==" + }, + "flatten": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.3.tgz", + "integrity": "sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg==" + }, + "flexboxgrid2": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/flexboxgrid2/-/flexboxgrid2-7.2.1.tgz", + "integrity": "sha512-O2bO5ZcBXnFy7cYmyt/CKb6CuwzNuUPxWJt8WOiaot8SetE9zyUahXGTSpKDm3+CTYQ5YeEMPeunMdjcxKJz4w==", + "requires": { + "normalize.css": "^7.0.0" + } + }, + "flush-write-stream": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", + "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", + "requires": { + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "follow-redirects": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.0.tgz", + "integrity": "sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==" + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "fork-ts-checker-webpack-plugin": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-4.1.6.tgz", + "integrity": "sha512-DUxuQaKoqfNne8iikd14SAkh5uw4+8vNifp6gmA73yYNS6ywLIWSLD/n/mBzHQRpW3J7rbATEakmiA8JvkTyZw==", + "requires": { + "@babel/code-frame": "^7.5.5", + "chalk": "^2.4.1", + "micromatch": "^3.1.10", + "minimatch": "^3.0.4", + "semver": "^5.6.0", + "tapable": "^1.0.0", + "worker-rpc": "^0.1.0" + }, + "dependencies": { + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, + "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" + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "requires": { + "map-cache": "^0.2.2" + } + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "fs-extra": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz", + "integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==", + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^1.0.0" + } + }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "requires": { + "minipass": "^3.0.0" + } + }, + "fs-write-stream-atomic": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", + "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", + "requires": { + "graceful-fs": "^4.1.2", + "iferr": "^0.1.5", + "imurmurhash": "^0.1.4", + "readable-stream": "1 || 2" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fsevents": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.2.1.tgz", + "integrity": "sha512-bTLYHSeC0UH/EFXS9KqWnXuOl/wHK5Z/d+ghd5AsFMYN7wIGkUCOJyzy88+wJKkZPGON8u4Z9f6U4FdgURE9qA==", + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=" + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==" + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "get-intrinsic": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.1.tgz", + "integrity": "sha512-ZnWP+AmS1VUaLgTRy47+zKtjTxz+0xMpx3I52i+aalBK1QP19ggLF3Db89KJX7kjfOfP2eoa01qc++GwPgufPg==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, + "get-own-enumerable-property-symbols": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==" + }, + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==" + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "requires": { + "pump": "^3.0.0" + } + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" + }, + "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.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "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" + } + }, + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "requires": { + "is-glob": "^4.0.1" + } + }, + "global-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", + "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", + "requires": { + "global-prefix": "^3.0.0" + } + }, + "global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "requires": { + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" + }, + "globby": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.1.tgz", + "integrity": "sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ==", + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" + } + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" + }, + "growly": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", + "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", + "optional": true + }, + "gzip-size": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz", + "integrity": "sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA==", + "requires": { + "duplexer": "^0.1.1", + "pify": "^4.0.1" + } + }, + "handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==" + }, + "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.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "requires": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + } + }, + "harmony-reflect": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.1.tgz", + "integrity": "sha512-WJTeyp0JzGtHcuMsi7rw2VwtkvLa+JyfEKJCFyfcS0+CDkjQ5lHPu7zEhFZP+PDSRrEgXa5Ah0l1MbgbE41XjA==" + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==" + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "hash-base": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", + "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", + "requires": { + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } + } + }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" + }, + "hex-color-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", + "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==" + }, + "history": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", + "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "requires": { + "@babel/runtime": "^7.1.2", + "loose-envify": "^1.2.0", + "resolve-pathname": "^3.0.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0", + "value-equal": "^1.0.1" + } + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "requires": { + "react-is": "^16.7.0" + } + }, + "hoopy": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", + "integrity": "sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==" + }, + "hosted-git-info": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==" + }, + "hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", + "requires": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "hsl-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsl-regex/-/hsl-regex-1.0.0.tgz", + "integrity": "sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4=" + }, + "hsla-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsla-regex/-/hsla-regex-1.0.0.tgz", + "integrity": "sha1-wc56MWjIxmFAM6S194d/OyJfnDg=" + }, + "html-comment-regex": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.2.tgz", + "integrity": "sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==" + }, + "html-encoding-sniffer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", + "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", + "requires": { + "whatwg-encoding": "^1.0.5" + } + }, + "html-entities": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.3.1.tgz", + "integrity": "sha512-rhE/4Z3hIhzHAUKbW8jVcCyuT5oJCXXqhN/6mXXVCpzTmvJnoH2HL/bt3EZ6p55jbFJBeAe1ZNpL5BugLujxNA==" + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==" + }, + "html-minifier-terser": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz", + "integrity": "sha512-ZPr5MNObqnV/T9akshPKbVgyOqLmy+Bxo7juKCfTfnjNniTAMdy4hz21YQqoofMBJD2kdREaqPPdThoR78Tgxg==", + "requires": { + "camel-case": "^4.1.1", + "clean-css": "^4.2.3", + "commander": "^4.1.1", + "he": "^1.2.0", + "param-case": "^3.0.3", + "relateurl": "^0.2.7", + "terser": "^4.6.3" + } + }, + "html-webpack-plugin": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-4.5.0.tgz", + "integrity": "sha512-MouoXEYSjTzCrjIxWwg8gxL5fE2X2WZJLmBYXlaJhQUH5K/b5OrqmV7T4dB7iu0xkmJ6JlUuV6fFVtnqbPopZw==", + "requires": { + "@types/html-minifier-terser": "^5.0.0", + "@types/tapable": "^1.0.5", + "@types/webpack": "^4.41.8", + "html-minifier-terser": "^5.0.1", + "loader-utils": "^1.2.3", + "lodash": "^4.17.15", + "pretty-error": "^2.1.1", + "tapable": "^1.1.3", + "util.promisify": "1.0.0" + }, + "dependencies": { + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", + "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + } + }, + "util.promisify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", + "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", + "requires": { + "define-properties": "^1.1.2", + "object.getownpropertydescriptors": "^2.0.3" + } + } + } + }, + "htmlparser2": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", + "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "requires": { + "domelementtype": "^1.3.1", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^3.1.1" + }, + "dependencies": { + "entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" + } + } + }, + "http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=" + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + } + } + }, + "http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "requires": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + } + }, + "http-proxy-middleware": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz", + "integrity": "sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q==", + "requires": { + "http-proxy": "^1.17.0", + "is-glob": "^4.0.0", + "lodash": "^4.17.11", + "micromatch": "^3.1.10" + }, + "dependencies": { + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, + "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" + } + }, + "https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=" + }, + "human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==" + }, + "hyphenate-style-name": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz", + "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==" + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "icss-utils": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz", + "integrity": "sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==", + "requires": { + "postcss": "^7.0.14" + } + }, + "identity-obj-proxy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", + "integrity": "sha1-lNK9qWCERT7zb7xarsN+D3nx/BQ=", + "requires": { + "harmony-reflect": "^1.4.6" + } + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "iferr": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", + "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=" + }, + "ignore": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==" + }, + "immer": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/immer/-/immer-7.0.9.tgz", + "integrity": "sha512-Vs/gxoM4DqNAYR7pugIxi0Xc8XAun/uy7AQu4fLLqaTBHxjOP9pJ266Q9MWA/ly4z6rAFZbvViOtihxUZ7O28A==" + }, + "import-cwd": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz", + "integrity": "sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk=", + "requires": { + "import-from": "^2.1.0" + } + }, + "import-fresh": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.2.tgz", + "integrity": "sha512-cTPNrlvJT6twpYy+YmKUKrTSjWFs3bjYjAhCwm+z4EOCubZxAuO+hHpRN64TqjEaYSHs7tJAE0w1CKMGmsG/lw==", + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "import-from": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-from/-/import-from-2.1.0.tgz", + "integrity": "sha1-M1238qev/VOqpHHUuAId7ja387E=", + "requires": { + "resolve-from": "^3.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=" + } + } + }, + "import-local": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.2.tgz", + "integrity": "sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==", + "requires": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "dependencies": { + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "requires": { + "find-up": "^4.0.0" + } + } + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" + }, + "indefinite-observable": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/indefinite-observable/-/indefinite-observable-2.0.1.tgz", + "integrity": "sha512-G8vgmork+6H9S8lUAg1gtXEj2JxIQTo0g2PbFiYOdjkziSI0F7UYBiVwhZRuixhBCNGczAls34+5HJPyZysvxQ==", + "requires": { + "symbol-observable": "1.2.0" + } + }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==" + }, + "indexes-of": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", + "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=" + }, + "infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" + }, + "internal-ip": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz", + "integrity": "sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg==", + "requires": { + "default-gateway": "^4.2.0", + "ipaddr.js": "^1.9.0" + } + }, + "internal-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.2.tgz", + "integrity": "sha512-2cQNfwhAfJIkU4KZPkDI+Gj5yNNnbqi40W9Gge6dfnk4TocEVm00B3bdiL+JINrbGJil2TeHvM4rETGzk/f/0g==", + "requires": { + "es-abstract": "^1.17.0-next.1", + "has": "^1.0.3", + "side-channel": "^1.0.2" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", + "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } + } + }, + "ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" + }, + "ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=" + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, + "is-absolute-url": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz", + "integrity": "sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=" + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-arguments": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", + "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==" + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "optional": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "is-callable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", + "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==" + }, + "is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "requires": { + "ci-info": "^2.0.0" + } + }, + "is-color-stop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz", + "integrity": "sha1-z/9HGu5N1cnhWFmPvhKWe1za00U=", + "requires": { + "css-color-names": "^0.0.4", + "hex-color-regex": "^1.1.0", + "hsl-regex": "^1.0.0", + "hsla-regex": "^1.0.0", + "rgb-regex": "^1.0.1", + "rgba-regex": "^1.0.0" + } + }, + "is-core-module": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz", + "integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==", + "requires": { + "has": "^1.0.3" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-date-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==" + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + } + } + }, + "is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=" + }, + "is-docker": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.1.1.tgz", + "integrity": "sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw==" + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==" + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-in-browser": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz", + "integrity": "sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU=" + }, + "is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=" + }, + "is-negative-zero": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.0.tgz", + "integrity": "sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE=" + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==" + }, + "is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==" + }, + "is-path-in-cwd": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz", + "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==", + "requires": { + "is-path-inside": "^2.1.0" + } + }, + "is-path-inside": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", + "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", + "requires": { + "path-is-inside": "^1.0.2" + } + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=" + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "requires": { + "isobject": "^3.0.1" + } + }, + "is-potential-custom-element-name": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz", + "integrity": "sha1-DFLlS8yjkbssSUsh6GJtczbG45c=" + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "requires": { + "has-symbols": "^1.0.1" + } + }, + "is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk=" + }, + "is-resolvable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", + "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==" + }, + "is-root": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz", + "integrity": "sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg==" + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + }, + "is-string": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", + "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==" + }, + "is-svg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-3.0.0.tgz", + "integrity": "sha512-gi4iHK53LR2ujhLVVj+37Ykh9GLqYHX6JOVXbLAucaG/Cqw9xwdFOjDM2qeifLs1sF1npXXFvDu0r5HNgCMrzQ==", + "requires": { + "html-comment-regex": "^1.1.0" + } + }, + "is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "requires": { + "has-symbols": "^1.0.1" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" + }, + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "requires": { + "is-docker": "^2.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "istanbul-lib-coverage": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", + "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==" + }, + "istanbul-lib-instrument": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "requires": { + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "requires": { + "semver": "^6.0.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz", + "integrity": "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==", + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "istanbul-reports": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz", + "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==", + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "jest": { + "version": "26.6.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-26.6.0.tgz", + "integrity": "sha512-jxTmrvuecVISvKFFhOkjsWRZV7sFqdSUAd1ajOKY+/QE/aLBVstsJ/dX8GczLzwiT6ZEwwmZqtCUHLHHQVzcfA==", + "requires": { + "@jest/core": "^26.6.0", + "import-local": "^3.0.2", + "jest-cli": "^26.6.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "jest-cli": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-26.6.3.tgz", + "integrity": "sha512-GF9noBSa9t08pSyl3CY4frMrqp+aQXFGFkf5hEPbh/pIUFYWMK6ZLTfbmadxJVcJrdRoChlWQsA2VkJcDFK8hg==", + "requires": { + "@jest/core": "^26.6.3", + "@jest/test-result": "^26.6.2", + "@jest/types": "^26.6.2", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.4", + "import-local": "^3.0.2", + "is-ci": "^2.0.0", + "jest-config": "^26.6.3", + "jest-util": "^26.6.2", + "jest-validate": "^26.6.2", + "prompts": "^2.0.1", + "yargs": "^15.4.1" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-changed-files": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-26.6.2.tgz", + "integrity": "sha512-fDS7szLcY9sCtIip8Fjry9oGf3I2ht/QT21bAHm5Dmf0mD4X3ReNUf17y+bO6fR8WgbIZTlbyG1ak/53cbRzKQ==", + "requires": { + "@jest/types": "^26.6.2", + "execa": "^4.0.0", + "throat": "^5.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "execa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", + "requires": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + } + }, + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "requires": { + "pump": "^3.0.0" + } + }, + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==" + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "requires": { + "path-key": "^3.0.0" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "jest-circus": { + "version": "26.6.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-26.6.0.tgz", + "integrity": "sha512-L2/Y9szN6FJPWFK8kzWXwfp+FOR7xq0cUL4lIsdbIdwz3Vh6P1nrpcqOleSzr28zOtSHQNV9Z7Tl+KkuK7t5Ng==", + "requires": { + "@babel/traverse": "^7.1.0", + "@jest/environment": "^26.6.0", + "@jest/test-result": "^26.6.0", + "@jest/types": "^26.6.0", + "@types/babel__traverse": "^7.0.4", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^0.7.0", + "expect": "^26.6.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^26.6.0", + "jest-matcher-utils": "^26.6.0", + "jest-message-util": "^26.6.0", + "jest-runner": "^26.6.0", + "jest-runtime": "^26.6.0", + "jest-snapshot": "^26.6.0", + "jest-util": "^26.6.0", + "pretty-format": "^26.6.0", + "stack-utils": "^2.0.2", + "throat": "^5.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-config": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-26.6.3.tgz", + "integrity": "sha512-t5qdIj/bCj2j7NFVHb2nFB4aUdfucDn3JRKgrZnplb8nieAirAzRSHP8uDEd+qV6ygzg9Pz4YG7UTJf94LPSyg==", + "requires": { + "@babel/core": "^7.1.0", + "@jest/test-sequencer": "^26.6.3", + "@jest/types": "^26.6.2", + "babel-jest": "^26.6.3", + "chalk": "^4.0.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.1", + "graceful-fs": "^4.2.4", + "jest-environment-jsdom": "^26.6.2", + "jest-environment-node": "^26.6.2", + "jest-get-type": "^26.3.0", + "jest-jasmine2": "^26.6.3", + "jest-regex-util": "^26.0.0", + "jest-resolve": "^26.6.2", + "jest-util": "^26.6.2", + "jest-validate": "^26.6.2", + "micromatch": "^4.0.2", + "pretty-format": "^26.6.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "jest-resolve": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-26.6.2.tgz", + "integrity": "sha512-sOxsZOq25mT1wRsfHcbtkInS+Ek7Q8jCHUB0ZUTP0tc/c41QHriU/NunqMfCUWsL4H3MHpvQD4QR9kSYhS7UvQ==", + "requires": { + "@jest/types": "^26.6.2", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^26.6.2", + "read-pkg-up": "^7.0.1", + "resolve": "^1.18.1", + "slash": "^3.0.0" + } + }, + "read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "requires": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "dependencies": { + "type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==" + } + } + }, + "read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "requires": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-diff": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", + "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==", + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^26.6.2", + "jest-get-type": "^26.3.0", + "pretty-format": "^26.6.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-docblock": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-26.0.0.tgz", + "integrity": "sha512-RDZ4Iz3QbtRWycd8bUEPxQsTlYazfYn/h5R65Fc6gOfwozFhoImx+affzky/FFBuqISPTqjXomoIGJVKBWoo0w==", + "requires": { + "detect-newline": "^3.0.0" + } + }, + "jest-each": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-26.6.2.tgz", + "integrity": "sha512-Mer/f0KaATbjl8MCJ+0GEpNdqmnVmDYqCTJYTvoo7rqmRiDllmp2AYN+06F93nXcY3ur9ShIjS+CO/uD+BbH4A==", + "requires": { + "@jest/types": "^26.6.2", + "chalk": "^4.0.0", + "jest-get-type": "^26.3.0", + "jest-util": "^26.6.2", + "pretty-format": "^26.6.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-environment-jsdom": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-26.6.2.tgz", + "integrity": "sha512-jgPqCruTlt3Kwqg5/WVFyHIOJHsiAvhcp2qiR2QQstuG9yWox5+iHpU3ZrcBxW14T4fe5Z68jAfLRh7joCSP2Q==", + "requires": { + "@jest/environment": "^26.6.2", + "@jest/fake-timers": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", + "jest-mock": "^26.6.2", + "jest-util": "^26.6.2", + "jsdom": "^16.4.0" + } + }, + "jest-environment-node": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-26.6.2.tgz", + "integrity": "sha512-zhtMio3Exty18dy8ee8eJ9kjnRyZC1N4C1Nt/VShN1apyXc8rWGtJ9lI7vqiWcyyXS4BVSEn9lxAM2D+07/Tag==", + "requires": { + "@jest/environment": "^26.6.2", + "@jest/fake-timers": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", + "jest-mock": "^26.6.2", + "jest-util": "^26.6.2" + } + }, + "jest-get-type": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", + "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==" + }, + "jest-haste-map": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-26.6.2.tgz", + "integrity": "sha512-easWIJXIw71B2RdR8kgqpjQrbMRWQBgiBwXYEhtGUTaX+doCjBheluShdDMeR8IMfJiTqH4+zfhtg29apJf/8w==", + "requires": { + "@jest/types": "^26.6.2", + "@types/graceful-fs": "^4.1.2", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "fsevents": "^2.1.2", + "graceful-fs": "^4.2.4", + "jest-regex-util": "^26.0.0", + "jest-serializer": "^26.6.2", + "jest-util": "^26.6.2", + "jest-worker": "^26.6.2", + "micromatch": "^4.0.2", + "sane": "^4.0.3", + "walker": "^1.0.7" + } + }, + "jest-jasmine2": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-26.6.3.tgz", + "integrity": "sha512-kPKUrQtc8aYwBV7CqBg5pu+tmYXlvFlSFYn18ev4gPFtrRzB15N2gW/Roew3187q2w2eHuu0MU9TJz6w0/nPEg==", + "requires": { + "@babel/traverse": "^7.1.0", + "@jest/environment": "^26.6.2", + "@jest/source-map": "^26.6.2", + "@jest/test-result": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "expect": "^26.6.2", + "is-generator-fn": "^2.0.0", + "jest-each": "^26.6.2", + "jest-matcher-utils": "^26.6.2", + "jest-message-util": "^26.6.2", + "jest-runtime": "^26.6.3", + "jest-snapshot": "^26.6.2", + "jest-util": "^26.6.2", + "pretty-format": "^26.6.2", + "throat": "^5.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-leak-detector": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-26.6.2.tgz", + "integrity": "sha512-i4xlXpsVSMeKvg2cEKdfhh0H39qlJlP5Ex1yQxwF9ubahboQYMgTtz5oML35AVA3B4Eu+YsmwaiKVev9KCvLxg==", + "requires": { + "jest-get-type": "^26.3.0", + "pretty-format": "^26.6.2" + } + }, + "jest-matcher-utils": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-26.6.2.tgz", + "integrity": "sha512-llnc8vQgYcNqDrqRDXWwMr9i7rS5XFiCwvh6DTP7Jqa2mqpcCBBlpCbn+trkG0KNhPu/h8rzyBkriOtBstvWhw==", + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^26.6.2", + "jest-get-type": "^26.3.0", + "pretty-format": "^26.6.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-message-util": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-26.6.2.tgz", + "integrity": "sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA==", + "requires": { + "@babel/code-frame": "^7.0.0", + "@jest/types": "^26.6.2", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "micromatch": "^4.0.2", + "pretty-format": "^26.6.2", + "slash": "^3.0.0", + "stack-utils": "^2.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-mock": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-26.6.2.tgz", + "integrity": "sha512-YyFjePHHp1LzpzYcmgqkJ0nm0gg/lJx2aZFzFy1S6eUqNjXsOqTK10zNRff2dNfssgokjkG65OlWNcIlgd3zew==", + "requires": { + "@jest/types": "^26.6.2", + "@types/node": "*" + } + }, + "jest-pnp-resolver": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", + "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==" + }, + "jest-regex-util": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-26.0.0.tgz", + "integrity": "sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A==" + }, + "jest-resolve": { + "version": "26.6.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-26.6.0.tgz", + "integrity": "sha512-tRAz2bwraHufNp+CCmAD8ciyCpXCs1NQxB5EJAmtCFy6BN81loFEGWKzYu26Y62lAJJe4X4jg36Kf+NsQyiStQ==", + "requires": { + "@jest/types": "^26.6.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^26.6.0", + "read-pkg-up": "^7.0.1", + "resolve": "^1.17.0", + "slash": "^3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "requires": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "dependencies": { + "type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==" + } + } + }, + "read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "requires": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-resolve-dependencies": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-26.6.3.tgz", + "integrity": "sha512-pVwUjJkxbhe4RY8QEWzN3vns2kqyuldKpxlxJlzEYfKSvY6/bMvxoFrYYzUO1Gx28yKWN37qyV7rIoIp2h8fTg==", + "requires": { + "@jest/types": "^26.6.2", + "jest-regex-util": "^26.0.0", + "jest-snapshot": "^26.6.2" + } + }, + "jest-runner": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-26.6.3.tgz", + "integrity": "sha512-atgKpRHnaA2OvByG/HpGA4g6CSPS/1LK0jK3gATJAoptC1ojltpmVlYC3TYgdmGp+GLuhzpH30Gvs36szSL2JQ==", + "requires": { + "@jest/console": "^26.6.2", + "@jest/environment": "^26.6.2", + "@jest/test-result": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.7.1", + "exit": "^0.1.2", + "graceful-fs": "^4.2.4", + "jest-config": "^26.6.3", + "jest-docblock": "^26.0.0", + "jest-haste-map": "^26.6.2", + "jest-leak-detector": "^26.6.2", + "jest-message-util": "^26.6.2", + "jest-resolve": "^26.6.2", + "jest-runtime": "^26.6.3", + "jest-util": "^26.6.2", + "jest-worker": "^26.6.2", + "source-map-support": "^0.5.6", + "throat": "^5.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "jest-resolve": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-26.6.2.tgz", + "integrity": "sha512-sOxsZOq25mT1wRsfHcbtkInS+Ek7Q8jCHUB0ZUTP0tc/c41QHriU/NunqMfCUWsL4H3MHpvQD4QR9kSYhS7UvQ==", + "requires": { + "@jest/types": "^26.6.2", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^26.6.2", + "read-pkg-up": "^7.0.1", + "resolve": "^1.18.1", + "slash": "^3.0.0" + } + }, + "read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "requires": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "dependencies": { + "type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==" + } + } + }, + "read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "requires": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-runtime": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-26.6.3.tgz", + "integrity": "sha512-lrzyR3N8sacTAMeonbqpnSka1dHNux2uk0qqDXVkMv2c/A3wYnvQ4EXuI013Y6+gSKSCxdaczvf4HF0mVXHRdw==", + "requires": { + "@jest/console": "^26.6.2", + "@jest/environment": "^26.6.2", + "@jest/fake-timers": "^26.6.2", + "@jest/globals": "^26.6.2", + "@jest/source-map": "^26.6.2", + "@jest/test-result": "^26.6.2", + "@jest/transform": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0", + "cjs-module-lexer": "^0.6.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.4", + "jest-config": "^26.6.3", + "jest-haste-map": "^26.6.2", + "jest-message-util": "^26.6.2", + "jest-mock": "^26.6.2", + "jest-regex-util": "^26.0.0", + "jest-resolve": "^26.6.2", + "jest-snapshot": "^26.6.2", + "jest-util": "^26.6.2", + "jest-validate": "^26.6.2", + "slash": "^3.0.0", + "strip-bom": "^4.0.0", + "yargs": "^15.4.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "jest-resolve": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-26.6.2.tgz", + "integrity": "sha512-sOxsZOq25mT1wRsfHcbtkInS+Ek7Q8jCHUB0ZUTP0tc/c41QHriU/NunqMfCUWsL4H3MHpvQD4QR9kSYhS7UvQ==", + "requires": { + "@jest/types": "^26.6.2", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^26.6.2", + "read-pkg-up": "^7.0.1", + "resolve": "^1.18.1", + "slash": "^3.0.0" + } + }, + "read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "requires": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "dependencies": { + "type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==" + } + } + }, + "read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "requires": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + } + }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-serializer": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-26.6.2.tgz", + "integrity": "sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g==", + "requires": { + "@types/node": "*", + "graceful-fs": "^4.2.4" + } + }, + "jest-snapshot": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-26.6.2.tgz", + "integrity": "sha512-OLhxz05EzUtsAmOMzuupt1lHYXCNib0ECyuZ/PZOx9TrZcC8vL0x+DUG3TL+GLX3yHG45e6YGjIm0XwDc3q3og==", + "requires": { + "@babel/types": "^7.0.0", + "@jest/types": "^26.6.2", + "@types/babel__traverse": "^7.0.4", + "@types/prettier": "^2.0.0", + "chalk": "^4.0.0", + "expect": "^26.6.2", + "graceful-fs": "^4.2.4", + "jest-diff": "^26.6.2", + "jest-get-type": "^26.3.0", + "jest-haste-map": "^26.6.2", + "jest-matcher-utils": "^26.6.2", + "jest-message-util": "^26.6.2", + "jest-resolve": "^26.6.2", + "natural-compare": "^1.4.0", + "pretty-format": "^26.6.2", + "semver": "^7.3.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "jest-resolve": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-26.6.2.tgz", + "integrity": "sha512-sOxsZOq25mT1wRsfHcbtkInS+Ek7Q8jCHUB0ZUTP0tc/c41QHriU/NunqMfCUWsL4H3MHpvQD4QR9kSYhS7UvQ==", + "requires": { + "@jest/types": "^26.6.2", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^26.6.2", + "read-pkg-up": "^7.0.1", + "resolve": "^1.18.1", + "slash": "^3.0.0" + } + }, + "read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "requires": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "dependencies": { + "type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==" + } + } + }, + "read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "requires": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-util": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", + "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", + "requires": { + "@jest/types": "^26.6.2", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-validate": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-26.6.2.tgz", + "integrity": "sha512-NEYZ9Aeyj0i5rQqbq+tpIOom0YS1u2MVu6+euBsvpgIme+FOfRmoC4R5p0JiAUpaFvFy24xgrpMknarR/93XjQ==", + "requires": { + "@jest/types": "^26.6.2", + "camelcase": "^6.0.0", + "chalk": "^4.0.0", + "jest-get-type": "^26.3.0", + "leven": "^3.1.0", + "pretty-format": "^26.6.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-watch-typeahead": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/jest-watch-typeahead/-/jest-watch-typeahead-0.6.1.tgz", + "integrity": "sha512-ITVnHhj3Jd/QkqQcTqZfRgjfyRhDFM/auzgVo2RKvSwi18YMvh0WvXDJFoFED6c7jd/5jxtu4kSOb9PTu2cPVg==", + "requires": { + "ansi-escapes": "^4.3.1", + "chalk": "^4.0.0", + "jest-regex-util": "^26.0.0", + "jest-watcher": "^26.3.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-watcher": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-26.6.2.tgz", + "integrity": "sha512-WKJob0P/Em2csiVthsI68p6aGKTIcsfjH9Gsx1f0A3Italz43e3ho0geSAVsmj09RWOELP1AZ/DXyJgOgDKxXQ==", + "requires": { + "@jest/test-result": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "jest-util": "^26.6.2", + "string-length": "^4.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-worker": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", + "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "js-yaml": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", + "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", + "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=" + }, + "jsdom": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.4.0.tgz", + "integrity": "sha512-lYMm3wYdgPhrl7pDcRmvzPhhrGVBeVhPIqeHjzeiHN3DFmD1RBpbExbi8vU7BJdH8VAZYovR8DMt0PNNDM7k8w==", + "requires": { + "abab": "^2.0.3", + "acorn": "^7.1.1", + "acorn-globals": "^6.0.0", + "cssom": "^0.4.4", + "cssstyle": "^2.2.0", + "data-urls": "^2.0.0", + "decimal.js": "^10.2.0", + "domexception": "^2.0.1", + "escodegen": "^1.14.1", + "html-encoding-sniffer": "^2.0.1", + "is-potential-custom-element-name": "^1.0.0", + "nwsapi": "^2.2.0", + "parse5": "5.1.1", + "request": "^2.88.2", + "request-promise-native": "^1.0.8", + "saxes": "^5.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^3.0.1", + "w3c-hr-time": "^1.0.2", + "w3c-xmlserializer": "^2.0.0", + "webidl-conversions": "^6.1.0", + "whatwg-encoding": "^1.0.5", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.0.0", + "ws": "^7.2.3", + "xml-name-validator": "^3.0.0" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "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-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=" + }, + "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=" + }, + "json3": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.3.tgz", + "integrity": "sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA==" + }, + "json5": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", + "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", + "requires": { + "minimist": "^1.2.5" + } + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + }, + "dependencies": { + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" + } + } + }, + "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" + } + }, + "jss": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/jss/-/jss-10.5.0.tgz", + "integrity": "sha512-B6151NvG+thUg3murLNHRPLxTLwQ13ep4SH5brj4d8qKtogOx/jupnpfkPGSHPqvcwKJaCLctpj2lEk+5yGwMw==", + "requires": { + "@babel/runtime": "^7.3.1", + "csstype": "^3.0.2", + "indefinite-observable": "^2.0.1", + "is-in-browser": "^1.1.3", + "tiny-warning": "^1.0.2" + } + }, + "jss-plugin-camel-case": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/jss-plugin-camel-case/-/jss-plugin-camel-case-10.5.0.tgz", + "integrity": "sha512-GSjPL0adGAkuoqeYiXTgO7PlIrmjv5v8lA6TTBdfxbNYpxADOdGKJgIEkffhlyuIZHlPuuiFYTwUreLUmSn7rg==", + "requires": { + "@babel/runtime": "^7.3.1", + "hyphenate-style-name": "^1.0.3", + "jss": "10.5.0" + } + }, + "jss-plugin-default-unit": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/jss-plugin-default-unit/-/jss-plugin-default-unit-10.5.0.tgz", + "integrity": "sha512-rsbTtZGCMrbcb9beiDd+TwL991NGmsAgVYH0hATrYJtue9e+LH/Gn4yFD1ENwE+3JzF3A+rPnM2JuD9L/SIIWw==", + "requires": { + "@babel/runtime": "^7.3.1", + "jss": "10.5.0" + } + }, + "jss-plugin-global": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/jss-plugin-global/-/jss-plugin-global-10.5.0.tgz", + "integrity": "sha512-FZd9+JE/3D7HMefEG54fEC0XiQ9rhGtDHAT/ols24y8sKQ1D5KIw6OyXEmIdKFmACgxZV2ARQ5pAUypxkk2IFQ==", + "requires": { + "@babel/runtime": "^7.3.1", + "jss": "10.5.0" + } + }, + "jss-plugin-nested": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/jss-plugin-nested/-/jss-plugin-nested-10.5.0.tgz", + "integrity": "sha512-ejPlCLNlEGgx8jmMiDk/zarsCZk+DV0YqXfddpgzbO9Toamo0HweCFuwJ3ZO40UFOfqKwfpKMVH/3HUXgxkTMg==", + "requires": { + "@babel/runtime": "^7.3.1", + "jss": "10.5.0", + "tiny-warning": "^1.0.2" + } + }, + "jss-plugin-props-sort": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/jss-plugin-props-sort/-/jss-plugin-props-sort-10.5.0.tgz", + "integrity": "sha512-kTLRvrOetFKz5vM88FAhLNeJIxfjhCepnvq65G7xsAQ/Wgy7HwO1BS/2wE5mx8iLaAWC6Rj5h16mhMk9sKdZxg==", + "requires": { + "@babel/runtime": "^7.3.1", + "jss": "10.5.0" + } + }, + "jss-plugin-rule-value-function": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.5.0.tgz", + "integrity": "sha512-jXINGr8BSsB13JVuK274oEtk0LoooYSJqTBCGeBu2cG/VJ3+4FPs1gwLgsq24xTgKshtZ+WEQMVL34OprLidRA==", + "requires": { + "@babel/runtime": "^7.3.1", + "jss": "10.5.0", + "tiny-warning": "^1.0.2" + } + }, + "jss-plugin-vendor-prefixer": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.5.0.tgz", + "integrity": "sha512-rux3gmfwDdOKCLDx0IQjTwTm03IfBa+Rm/hs747cOw5Q7O3RaTUIMPKjtVfc31Xr/XI9Abz2XEupk1/oMQ7zRA==", + "requires": { + "@babel/runtime": "^7.3.1", + "css-vendor": "^2.0.8", + "jss": "10.5.0" + } + }, + "jsx-ast-utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.1.0.tgz", + "integrity": "sha512-d4/UOjg+mxAWxCiF0c5UTSwyqbchkbqCvK87aBovhnh8GtysTjWmgC63tY0cJx/HzGgm9qnA147jVBdpOiQ2RA==", + "requires": { + "array-includes": "^3.1.1", + "object.assign": "^4.1.1" + } + }, + "killable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", + "integrity": "sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg==" + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" + }, + "kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==" + }, + "language-subtag-registry": { + "version": "0.3.21", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz", + "integrity": "sha512-L0IqwlIXjilBVVYKFT37X9Ih11Um5NEl9cbJIuU/SwP/zEEAbBPOnEeeuxVMf45ydWQRDQN3Nqc96OgbH1K+Pg==" + }, + "language-tags": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz", + "integrity": "sha1-0yHbxNowuovzAk4ED6XBRmH5GTo=", + "requires": { + "language-subtag-registry": "~0.3.2" + } + }, + "last-call-webpack-plugin": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz", + "integrity": "sha512-7KI2l2GIZa9p2spzPIVZBYyNKkN+e/SQPpnjlTiPhdbDW3F86tdKKELxKpzJ5sgU19wQWsACULZmpTPYHeWO5w==", + "requires": { + "lodash": "^4.17.5", + "webpack-sources": "^1.1.0" + } + }, + "leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==" + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "lines-and-columns": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", + "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=" + }, + "load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "requires": { + "error-ex": "^1.2.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + } + } + }, + "loader-runner": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", + "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==" + }, + "loader-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", + "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "requires": { + "p-locate": "^4.1.0" + } + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" + }, + "lodash._reinterpolate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", + "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=" + }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=" + }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=" + }, + "lodash.template": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", + "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", + "requires": { + "lodash._reinterpolate": "^3.0.0", + "lodash.templatesettings": "^4.0.0" + } + }, + "lodash.templatesettings": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", + "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", + "requires": { + "lodash._reinterpolate": "^3.0.0" + } + }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=" + }, + "loglevel": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.7.1.tgz", + "integrity": "sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw==" + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "requires": { + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" + } + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "magic-string": { + "version": "0.25.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", + "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", + "requires": { + "sourcemap-codec": "^1.4.4" + } + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } + } + }, + "makeerror": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", + "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=", + "requires": { + "tmpl": "1.0.x" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=" + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "requires": { + "object-visit": "^1.0.0" + } + }, + "md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "mdn-data": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", + "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "microevent.ts": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/microevent.ts/-/microevent.ts-0.1.1.tgz", + "integrity": "sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g==" + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "requires": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" + } + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" + }, + "mime-types": { + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "requires": { + "mime-db": "1.44.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + }, + "min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==" + }, + "mini-create-react-context": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz", + "integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==", + "requires": { + "@babel/runtime": "^7.12.1", + "tiny-warning": "^1.0.3" + } + }, + "mini-css-extract-plugin": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.11.3.tgz", + "integrity": "sha512-n9BA8LonkOkW1/zn+IbLPQmovsL0wMb9yx75fMJQZf2X1Zoec9yTZtyMePcyu19wPkmFbzZZA6fLTotpFhQsOA==", + "requires": { + "loader-utils": "^1.1.0", + "normalize-url": "1.9.1", + "schema-utils": "^1.0.0", + "webpack-sources": "^1.1.0" + }, + "dependencies": { + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", + "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + } + }, + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + } + } + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, + "minipass": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", + "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "requires": { + "yallist": "^4.0.0" + } + }, + "minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "requires": { + "minipass": "^3.0.0" + } + }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + } + }, + "mississippi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", + "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", + "requires": { + "concat-stream": "^1.5.0", + "duplexify": "^3.4.2", + "end-of-stream": "^1.1.0", + "flush-write-stream": "^1.0.0", + "from2": "^2.1.0", + "parallel-transform": "^1.1.0", + "pump": "^3.0.0", + "pumpify": "^1.3.3", + "stream-each": "^1.1.0", + "through2": "^2.0.0" + } + }, + "mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "requires": { + "minimist": "^1.2.5" + } + }, + "move-concurrently": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", + "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", + "requires": { + "aproba": "^1.1.1", + "copy-concurrently": "^1.0.0", + "fs-write-stream-atomic": "^1.0.8", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.3" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "multicast-dns": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", + "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==", + "requires": { + "dns-packet": "^1.3.1", + "thunky": "^1.0.2" + } + }, + "multicast-dns-service-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", + "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=" + }, + "nanoid": { + "version": "3.1.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz", + "integrity": "sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==" + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + } + }, + "native-url": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/native-url/-/native-url-0.2.6.tgz", + "integrity": "sha512-k4bDC87WtgrdD362gZz6zoiXQrl40kYlBmpfmSjwRO1VU0V5ccwJTlxuE72F6m3V0vc1xOf6n3UCP9QyerRqmA==", + "requires": { + "querystring": "^0.2.0" + } + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=" + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + }, + "next-tick": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" + }, + "no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "requires": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" + } + } + }, + "node-forge": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", + "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==" + }, + "node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=" + }, + "node-libs-browser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", + "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==", + "requires": { + "assert": "^1.1.1", + "browserify-zlib": "^0.2.0", + "buffer": "^4.3.0", + "console-browserify": "^1.1.0", + "constants-browserify": "^1.0.0", + "crypto-browserify": "^3.11.0", + "domain-browser": "^1.1.1", + "events": "^3.0.0", + "https-browserify": "^1.0.0", + "os-browserify": "^0.3.0", + "path-browserify": "0.0.1", + "process": "^0.11.10", + "punycode": "^1.2.4", + "querystring-es3": "^0.2.0", + "readable-stream": "^2.3.3", + "stream-browserify": "^2.0.1", + "stream-http": "^2.7.2", + "string_decoder": "^1.0.0", + "timers-browserify": "^2.0.4", + "tty-browserify": "0.0.0", + "url": "^0.11.0", + "util": "^0.11.0", + "vm-browserify": "^1.0.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + }, + "dependencies": { + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + } + } + }, + "node-modules-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", + "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=" + }, + "node-notifier": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.0.tgz", + "integrity": "sha512-46z7DUmcjoYdaWyXouuFNNfUo6eFa94t23c53c+lG/9Cvauk4a98rAUp9672X5dxGdQmLpPzTxzu8f/OeEPaFA==", + "optional": true, + "requires": { + "growly": "^1.3.0", + "is-wsl": "^2.2.0", + "semver": "^7.3.2", + "shellwords": "^0.1.1", + "uuid": "^8.3.0", + "which": "^2.0.2" + }, + "dependencies": { + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "optional": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "node-releases": { + "version": "1.1.67", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.67.tgz", + "integrity": "sha512-V5QF9noGFl3EymEwUYzO+3NTDpGfQB4ve6Qfnzf3UNydMhjQRVPR1DZTuvWiLzaFJYw2fmDwAfnRNEVb64hSIg==" + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + }, + "normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=" + }, + "normalize-url": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz", + "integrity": "sha1-LMDWazHqIwNkWENuNiDYWVTGbDw=", + "requires": { + "object-assign": "^4.0.1", + "prepend-http": "^1.0.0", + "query-string": "^4.1.0", + "sort-keys": "^1.0.0" + } + }, + "normalize.css": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/normalize.css/-/normalize.css-7.0.0.tgz", + "integrity": "sha1-q/sd2CRwZ04DIrU86xqvQSk45L8=" + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "requires": { + "path-key": "^2.0.0" + } + }, + "nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "requires": { + "boolbase": "~1.0.0" + } + }, + "num2fraction": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", + "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=" + }, + "nwsapi": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", + "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==" + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "object-inspect": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz", + "integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==" + }, + "object-is": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.4.tgz", + "integrity": "sha512-1ZvAZ4wlF7IyPVOcE1Omikt7UpaFlOQq0HlSti+ZvDH3UiD2brwGMwDbyV43jao2bKJ+4+WdPJHSd7kgzKYVqg==", + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + } + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "requires": { + "isobject": "^3.0.0" + } + }, + "object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + }, + "object.entries": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.3.tgz", + "integrity": "sha512-ym7h7OZebNS96hn5IJeyUmaWhaSM4SVtAPPfNLQEI2MYWCO2egsITb9nab2+i/Pwibx+R0mtn+ltKJXRSeTMGg==", + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1", + "has": "^1.0.3" + } + }, + "object.fromentries": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.3.tgz", + "integrity": "sha512-IDUSMXs6LOSJBWE++L0lzIbSqHl9KDCfff2x/JSEIDtEUavUnyMYC2ZGay/04Zq4UT8lvd4xNhU4/YHKibAOlw==", + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1", + "has": "^1.0.3" + } + }, + "object.getownpropertydescriptors": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.1.tgz", + "integrity": "sha512-6DtXgZ/lIZ9hqx4GtZETobXLR/ZLaa0aqV0kzbn80Rf8Z2e/XFnhA0I7p07N2wH8bBBltr2xQPi6sbKWAY2Eng==", + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "requires": { + "isobject": "^3.0.1" + } + }, + "object.values": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.2.tgz", + "integrity": "sha512-MYC0jvJopr8EK6dPBiO8Nb9mvjdypOachO5REGk6MXzujbBrAisKo3HmdEI6kZDL6fC31Mwee/5YbtMebixeag==", + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1", + "has": "^1.0.3" + } + }, + "obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "open": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/open/-/open-7.3.0.tgz", + "integrity": "sha512-mgLwQIx2F/ye9SmbrUkurZCnkoXyXyu9EbHtJZrICjVAJfyMArdHp3KkixGdZx1ZHFPNIwl0DDM1dFFqXbTLZw==", + "requires": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + } + }, + "opn": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz", + "integrity": "sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==", + "requires": { + "is-wsl": "^1.1.0" + }, + "dependencies": { + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=" + } + } + }, + "optimize-css-assets-webpack-plugin": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/optimize-css-assets-webpack-plugin/-/optimize-css-assets-webpack-plugin-5.0.4.tgz", + "integrity": "sha512-wqd6FdI2a5/FdoiCNNkEvLeA//lHHfG24Ln2Xm2qqdIk4aOlsR18jwpyOihqQ8849W3qu2DX8fOYxpvTMj+93A==", + "requires": { + "cssnano": "^4.1.10", + "last-call-webpack-plugin": "^3.0.0" + } + }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "original": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz", + "integrity": "sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==", + "requires": { + "url-parse": "^1.4.3" + } + }, + "os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=" + }, + "p-each-series": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-2.2.0.tgz", + "integrity": "sha512-ycIL2+1V32th+8scbpTvyHNaHe02z0sjgh91XXjAk+ZeXoPN4Z46DVUnzdso0aX4KckKw0FNNFHdjZ2UsZvxiA==" + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "p-retry": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-3.0.1.tgz", + "integrity": "sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w==", + "requires": { + "retry": "^0.12.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + }, + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, + "parallel-transform": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz", + "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==", + "requires": { + "cyclist": "^1.0.1", + "inherits": "^2.0.3", + "readable-stream": "^2.1.5" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "requires": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" + } + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-asn1": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", + "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", + "requires": { + "asn1.js": "^5.2.0", + "browserify-aes": "^1.0.0", + "evp_bytestokey": "^1.0.0", + "pbkdf2": "^3.0.3", + "safe-buffer": "^5.1.1" + } + }, + "parse-json": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz", + "integrity": "sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ==", + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==" + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" + } + } + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" + }, + "path-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", + "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==" + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=" + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, + "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=" + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=" + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" + }, + "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==" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" + }, + "pbkdf2": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.1.tgz", + "integrity": "sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg==", + "requires": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==" + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "requires": { + "pinkie": "^2.0.0" + } + }, + "pirates": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz", + "integrity": "sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==", + "requires": { + "node-modules-regexp": "^1.0.0" + } + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "requires": { + "find-up": "^3.0.0" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + } + } + }, + "pkg-up": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", + "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", + "requires": { + "find-up": "^3.0.0" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + } + } + }, + "pnp-webpack-plugin": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz", + "integrity": "sha512-7Wjy+9E3WwLOEL30D+m8TSTF7qJJUJLONBnwQp0518siuMxUQUbgZwssaFX+QKlZkjHZcw/IpZCt/H0srrntSg==", + "requires": { + "ts-pnp": "^1.1.6" + } + }, + "popper.js": { + "version": "1.16.1-lts", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1-lts.tgz", + "integrity": "sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA==" + }, + "portfinder": { + "version": "1.0.28", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz", + "integrity": "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==", + "requires": { + "async": "^2.6.2", + "debug": "^3.1.1", + "mkdirp": "^0.5.5" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" + }, + "postcss": { + "version": "7.0.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", + "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-attribute-case-insensitive": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-4.0.2.tgz", + "integrity": "sha512-clkFxk/9pcdb4Vkn0hAHq3YnxBQ2p0CGD1dy24jN+reBck+EWxMbxSUqN4Yj7t0w8csl87K6p0gxBe1utkJsYA==", + "requires": { + "postcss": "^7.0.2", + "postcss-selector-parser": "^6.0.2" + } + }, + "postcss-browser-comments": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-browser-comments/-/postcss-browser-comments-3.0.0.tgz", + "integrity": "sha512-qfVjLfq7HFd2e0HW4s1dvU8X080OZdG46fFbIBFjW7US7YPDcWfRvdElvwMJr2LI6hMmD+7LnH2HcmXTs+uOig==", + "requires": { + "postcss": "^7" + } + }, + "postcss-calc": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-7.0.5.tgz", + "integrity": "sha512-1tKHutbGtLtEZF6PT4JSihCHfIVldU72mZ8SdZHIYriIZ9fh9k9aWSppaT8rHsyI3dX+KSR+W+Ix9BMY3AODrg==", + "requires": { + "postcss": "^7.0.27", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.0.2" + } + }, + "postcss-color-functional-notation": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-2.0.1.tgz", + "integrity": "sha512-ZBARCypjEDofW4P6IdPVTLhDNXPRn8T2s1zHbZidW6rPaaZvcnCS2soYFIQJrMZSxiePJ2XIYTlcb2ztr/eT2g==", + "requires": { + "postcss": "^7.0.2", + "postcss-values-parser": "^2.0.0" + } + }, + "postcss-color-gray": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-color-gray/-/postcss-color-gray-5.0.0.tgz", + "integrity": "sha512-q6BuRnAGKM/ZRpfDascZlIZPjvwsRye7UDNalqVz3s7GDxMtqPY6+Q871liNxsonUw8oC61OG+PSaysYpl1bnw==", + "requires": { + "@csstools/convert-colors": "^1.4.0", + "postcss": "^7.0.5", + "postcss-values-parser": "^2.0.0" + } + }, + "postcss-color-hex-alpha": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-5.0.3.tgz", + "integrity": "sha512-PF4GDel8q3kkreVXKLAGNpHKilXsZ6xuu+mOQMHWHLPNyjiUBOr75sp5ZKJfmv1MCus5/DWUGcK9hm6qHEnXYw==", + "requires": { + "postcss": "^7.0.14", + "postcss-values-parser": "^2.0.1" + } + }, + "postcss-color-mod-function": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/postcss-color-mod-function/-/postcss-color-mod-function-3.0.3.tgz", + "integrity": "sha512-YP4VG+xufxaVtzV6ZmhEtc+/aTXH3d0JLpnYfxqTvwZPbJhWqp8bSY3nfNzNRFLgB4XSaBA82OE4VjOOKpCdVQ==", + "requires": { + "@csstools/convert-colors": "^1.4.0", + "postcss": "^7.0.2", + "postcss-values-parser": "^2.0.0" + } + }, + "postcss-color-rebeccapurple": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-4.0.1.tgz", + "integrity": "sha512-aAe3OhkS6qJXBbqzvZth2Au4V3KieR5sRQ4ptb2b2O8wgvB3SJBsdG+jsn2BZbbwekDG8nTfcCNKcSfe/lEy8g==", + "requires": { + "postcss": "^7.0.2", + "postcss-values-parser": "^2.0.0" + } + }, + "postcss-colormin": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-4.0.3.tgz", + "integrity": "sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw==", + "requires": { + "browserslist": "^4.0.0", + "color": "^3.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" + } + } + }, + "postcss-convert-values": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz", + "integrity": "sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ==", + "requires": { + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" + } + } + }, + "postcss-custom-media": { + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-7.0.8.tgz", + "integrity": "sha512-c9s5iX0Ge15o00HKbuRuTqNndsJUbaXdiNsksnVH8H4gdc+zbLzr/UasOwNG6CTDpLFekVY4672eWdiiWu2GUg==", + "requires": { + "postcss": "^7.0.14" + } + }, + "postcss-custom-properties": { + "version": "8.0.11", + "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-8.0.11.tgz", + "integrity": "sha512-nm+o0eLdYqdnJ5abAJeXp4CEU1c1k+eB2yMCvhgzsds/e0umabFrN6HoTy/8Q4K5ilxERdl/JD1LO5ANoYBeMA==", + "requires": { + "postcss": "^7.0.17", + "postcss-values-parser": "^2.0.1" + } + }, + "postcss-custom-selectors": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-5.1.2.tgz", + "integrity": "sha512-DSGDhqinCqXqlS4R7KGxL1OSycd1lydugJ1ky4iRXPHdBRiozyMHrdu0H3o7qNOCiZwySZTUI5MV0T8QhCLu+w==", + "requires": { + "postcss": "^7.0.2", + "postcss-selector-parser": "^5.0.0-rc.3" + }, + "dependencies": { + "cssesc": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz", + "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==" + }, + "postcss-selector-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz", + "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==", + "requires": { + "cssesc": "^2.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + } + } + }, + "postcss-dir-pseudo-class": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-5.0.0.tgz", + "integrity": "sha512-3pm4oq8HYWMZePJY+5ANriPs3P07q+LW6FAdTlkFH2XqDdP4HeeJYMOzn0HYLhRSjBO3fhiqSwwU9xEULSrPgw==", + "requires": { + "postcss": "^7.0.2", + "postcss-selector-parser": "^5.0.0-rc.3" + }, + "dependencies": { + "cssesc": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz", + "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==" + }, + "postcss-selector-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz", + "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==", + "requires": { + "cssesc": "^2.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + } + } + }, + "postcss-discard-comments": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz", + "integrity": "sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg==", + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-discard-duplicates": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.2.tgz", + "integrity": "sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ==", + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-discard-empty": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-4.0.1.tgz", + "integrity": "sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w==", + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-discard-overridden": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-4.0.1.tgz", + "integrity": "sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg==", + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-double-position-gradients": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-1.0.0.tgz", + "integrity": "sha512-G+nV8EnQq25fOI8CH/B6krEohGWnF5+3A6H/+JEpOncu5dCnkS1QQ6+ct3Jkaepw1NGVqqOZH6lqrm244mCftA==", + "requires": { + "postcss": "^7.0.5", + "postcss-values-parser": "^2.0.0" + } + }, + "postcss-env-function": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/postcss-env-function/-/postcss-env-function-2.0.2.tgz", + "integrity": "sha512-rwac4BuZlITeUbiBq60h/xbLzXY43qOsIErngWa4l7Mt+RaSkT7QBjXVGTcBHupykkblHMDrBFh30zchYPaOUw==", + "requires": { + "postcss": "^7.0.2", + "postcss-values-parser": "^2.0.0" + } + }, + "postcss-flexbugs-fixes": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-4.2.1.tgz", + "integrity": "sha512-9SiofaZ9CWpQWxOwRh1b/r85KD5y7GgvsNt1056k6OYLvWUun0czCvogfJgylC22uJTwW1KzY3Gz65NZRlvoiQ==", + "requires": { + "postcss": "^7.0.26" + } + }, + "postcss-focus-visible": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-4.0.0.tgz", + "integrity": "sha512-Z5CkWBw0+idJHSV6+Bgf2peDOFf/x4o+vX/pwcNYrWpXFrSfTkQ3JQ1ojrq9yS+upnAlNRHeg8uEwFTgorjI8g==", + "requires": { + "postcss": "^7.0.2" + } + }, + "postcss-focus-within": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-3.0.0.tgz", + "integrity": "sha512-W0APui8jQeBKbCGZudW37EeMCjDeVxKgiYfIIEo8Bdh5SpB9sxds/Iq8SEuzS0Q4YFOlG7EPFulbbxujpkrV2w==", + "requires": { + "postcss": "^7.0.2" + } + }, + "postcss-font-variant": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-4.0.1.tgz", + "integrity": "sha512-I3ADQSTNtLTTd8uxZhtSOrTCQ9G4qUVKPjHiDk0bV75QSxXjVWiJVJ2VLdspGUi9fbW9BcjKJoRvxAH1pckqmA==", + "requires": { + "postcss": "^7.0.2" + } + }, + "postcss-gap-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-2.0.0.tgz", + "integrity": "sha512-QZSqDaMgXCHuHTEzMsS2KfVDOq7ZFiknSpkrPJY6jmxbugUPTuSzs/vuE5I3zv0WAS+3vhrlqhijiprnuQfzmg==", + "requires": { + "postcss": "^7.0.2" + } + }, + "postcss-image-set-function": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-3.0.1.tgz", + "integrity": "sha512-oPTcFFip5LZy8Y/whto91L9xdRHCWEMs3e1MdJxhgt4jy2WYXfhkng59fH5qLXSCPN8k4n94p1Czrfe5IOkKUw==", + "requires": { + "postcss": "^7.0.2", + "postcss-values-parser": "^2.0.0" + } + }, + "postcss-initial": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-3.0.2.tgz", + "integrity": "sha512-ugA2wKonC0xeNHgirR4D3VWHs2JcU08WAi1KFLVcnb7IN89phID6Qtg2RIctWbnvp1TM2BOmDtX8GGLCKdR8YA==", + "requires": { + "lodash.template": "^4.5.0", + "postcss": "^7.0.2" + } + }, + "postcss-lab-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-2.0.1.tgz", + "integrity": "sha512-whLy1IeZKY+3fYdqQFuDBf8Auw+qFuVnChWjmxm/UhHWqNHZx+B99EwxTvGYmUBqe3Fjxs4L1BoZTJmPu6usVg==", + "requires": { + "@csstools/convert-colors": "^1.4.0", + "postcss": "^7.0.2", + "postcss-values-parser": "^2.0.0" + } + }, + "postcss-load-config": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-2.1.2.tgz", + "integrity": "sha512-/rDeGV6vMUo3mwJZmeHfEDvwnTKKqQ0S7OHUi/kJvvtx3aWtyWG2/0ZWnzCt2keEclwN6Tf0DST2v9kITdOKYw==", + "requires": { + "cosmiconfig": "^5.0.0", + "import-cwd": "^2.0.0" + }, + "dependencies": { + "cosmiconfig": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", + "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", + "requires": { + "import-fresh": "^2.0.0", + "is-directory": "^0.3.1", + "js-yaml": "^3.13.1", + "parse-json": "^4.0.0" + } + }, + "import-fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", + "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", + "requires": { + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=" + } + } + }, + "postcss-loader": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-3.0.0.tgz", + "integrity": "sha512-cLWoDEY5OwHcAjDnkyRQzAXfs2jrKjXpO/HQFcc5b5u/r7aa471wdmChmwfnv7x2u840iat/wi0lQ5nbRgSkUA==", + "requires": { + "loader-utils": "^1.1.0", + "postcss": "^7.0.0", + "postcss-load-config": "^2.0.0", + "schema-utils": "^1.0.0" + }, + "dependencies": { + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", + "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + } + }, + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + } + } + }, + "postcss-logical": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-3.0.0.tgz", + "integrity": "sha512-1SUKdJc2vuMOmeItqGuNaC+N8MzBWFWEkAnRnLpFYj1tGGa7NqyVBujfRtgNa2gXR+6RkGUiB2O5Vmh7E2RmiA==", + "requires": { + "postcss": "^7.0.2" + } + }, + "postcss-media-minmax": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-4.0.0.tgz", + "integrity": "sha512-fo9moya6qyxsjbFAYl97qKO9gyre3qvbMnkOZeZwlsW6XYFsvs2DMGDlchVLfAd8LHPZDxivu/+qW2SMQeTHBw==", + "requires": { + "postcss": "^7.0.2" + } + }, + "postcss-merge-longhand": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz", + "integrity": "sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw==", + "requires": { + "css-color-names": "0.0.4", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0", + "stylehacks": "^4.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" + } + } + }, + "postcss-merge-rules": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-4.0.3.tgz", + "integrity": "sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ==", + "requires": { + "browserslist": "^4.0.0", + "caniuse-api": "^3.0.0", + "cssnano-util-same-parent": "^4.0.0", + "postcss": "^7.0.0", + "postcss-selector-parser": "^3.0.0", + "vendors": "^1.0.0" + }, + "dependencies": { + "postcss-selector-parser": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz", + "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", + "requires": { + "dot-prop": "^5.2.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + } + } + }, + "postcss-minify-font-values": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-4.0.2.tgz", + "integrity": "sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg==", + "requires": { + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" + } + } + }, + "postcss-minify-gradients": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-4.0.2.tgz", + "integrity": "sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q==", + "requires": { + "cssnano-util-get-arguments": "^4.0.0", + "is-color-stop": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" + } + } + }, + "postcss-minify-params": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-4.0.2.tgz", + "integrity": "sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg==", + "requires": { + "alphanum-sort": "^1.0.0", + "browserslist": "^4.0.0", + "cssnano-util-get-arguments": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0", + "uniqs": "^2.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" + } + } + }, + "postcss-minify-selectors": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-4.0.2.tgz", + "integrity": "sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g==", + "requires": { + "alphanum-sort": "^1.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-selector-parser": "^3.0.0" + }, + "dependencies": { + "postcss-selector-parser": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz", + "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", + "requires": { + "dot-prop": "^5.2.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + } + } + }, + "postcss-modules-extract-imports": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz", + "integrity": "sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==", + "requires": { + "postcss": "^7.0.5" + } + }, + "postcss-modules-local-by-default": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz", + "integrity": "sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw==", + "requires": { + "icss-utils": "^4.1.1", + "postcss": "^7.0.32", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-modules-scope": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz", + "integrity": "sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ==", + "requires": { + "postcss": "^7.0.6", + "postcss-selector-parser": "^6.0.0" + } + }, + "postcss-modules-values": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz", + "integrity": "sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg==", + "requires": { + "icss-utils": "^4.0.0", + "postcss": "^7.0.6" + } + }, + "postcss-nesting": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-7.0.1.tgz", + "integrity": "sha512-FrorPb0H3nuVq0Sff7W2rnc3SmIcruVC6YwpcS+k687VxyxO33iE1amna7wHuRVzM8vfiYofXSBHNAZ3QhLvYg==", + "requires": { + "postcss": "^7.0.2" + } + }, + "postcss-normalize": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize/-/postcss-normalize-8.0.1.tgz", + "integrity": "sha512-rt9JMS/m9FHIRroDDBGSMsyW1c0fkvOJPy62ggxSHUldJO7B195TqFMqIf+lY5ezpDcYOV4j86aUp3/XbxzCCQ==", + "requires": { + "@csstools/normalize.css": "^10.1.0", + "browserslist": "^4.6.2", + "postcss": "^7.0.17", + "postcss-browser-comments": "^3.0.0", + "sanitize.css": "^10.0.0" + } + }, + "postcss-normalize-charset": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz", + "integrity": "sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g==", + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-normalize-display-values": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.2.tgz", + "integrity": "sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ==", + "requires": { + "cssnano-util-get-match": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" + } + } + }, + "postcss-normalize-positions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-4.0.2.tgz", + "integrity": "sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA==", + "requires": { + "cssnano-util-get-arguments": "^4.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" + } + } + }, + "postcss-normalize-repeat-style": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.2.tgz", + "integrity": "sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q==", + "requires": { + "cssnano-util-get-arguments": "^4.0.0", + "cssnano-util-get-match": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" + } + } + }, + "postcss-normalize-string": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-4.0.2.tgz", + "integrity": "sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA==", + "requires": { + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" + } + } + }, + "postcss-normalize-timing-functions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.2.tgz", + "integrity": "sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A==", + "requires": { + "cssnano-util-get-match": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" + } + } + }, + "postcss-normalize-unicode": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.1.tgz", + "integrity": "sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg==", + "requires": { + "browserslist": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" + } + } + }, + "postcss-normalize-url": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-4.0.1.tgz", + "integrity": "sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA==", + "requires": { + "is-absolute-url": "^2.0.0", + "normalize-url": "^3.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "normalize-url": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz", + "integrity": "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==" + }, + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" + } + } + }, + "postcss-normalize-whitespace": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.2.tgz", + "integrity": "sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA==", + "requires": { + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" + } + } + }, + "postcss-ordered-values": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-4.1.2.tgz", + "integrity": "sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw==", + "requires": { + "cssnano-util-get-arguments": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" + } + } + }, + "postcss-overflow-shorthand": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-2.0.0.tgz", + "integrity": "sha512-aK0fHc9CBNx8jbzMYhshZcEv8LtYnBIRYQD5i7w/K/wS9c2+0NSR6B3OVMu5y0hBHYLcMGjfU+dmWYNKH0I85g==", + "requires": { + "postcss": "^7.0.2" + } + }, + "postcss-page-break": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-2.0.0.tgz", + "integrity": "sha512-tkpTSrLpfLfD9HvgOlJuigLuk39wVTbbd8RKcy8/ugV2bNBUW3xU+AIqyxhDrQr1VUj1RmyJrBn1YWrqUm9zAQ==", + "requires": { + "postcss": "^7.0.2" + } + }, + "postcss-place": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-4.0.1.tgz", + "integrity": "sha512-Zb6byCSLkgRKLODj/5mQugyuj9bvAAw9LqJJjgwz5cYryGeXfFZfSXoP1UfveccFmeq0b/2xxwcTEVScnqGxBg==", + "requires": { + "postcss": "^7.0.2", + "postcss-values-parser": "^2.0.0" + } + }, + "postcss-preset-env": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-6.7.0.tgz", + "integrity": "sha512-eU4/K5xzSFwUFJ8hTdTQzo2RBLbDVt83QZrAvI07TULOkmyQlnYlpwep+2yIK+K+0KlZO4BvFcleOCCcUtwchg==", + "requires": { + "autoprefixer": "^9.6.1", + "browserslist": "^4.6.4", + "caniuse-lite": "^1.0.30000981", + "css-blank-pseudo": "^0.1.4", + "css-has-pseudo": "^0.10.0", + "css-prefers-color-scheme": "^3.1.1", + "cssdb": "^4.4.0", + "postcss": "^7.0.17", + "postcss-attribute-case-insensitive": "^4.0.1", + "postcss-color-functional-notation": "^2.0.1", + "postcss-color-gray": "^5.0.0", + "postcss-color-hex-alpha": "^5.0.3", + "postcss-color-mod-function": "^3.0.3", + "postcss-color-rebeccapurple": "^4.0.1", + "postcss-custom-media": "^7.0.8", + "postcss-custom-properties": "^8.0.11", + "postcss-custom-selectors": "^5.1.2", + "postcss-dir-pseudo-class": "^5.0.0", + "postcss-double-position-gradients": "^1.0.0", + "postcss-env-function": "^2.0.2", + "postcss-focus-visible": "^4.0.0", + "postcss-focus-within": "^3.0.0", + "postcss-font-variant": "^4.0.0", + "postcss-gap-properties": "^2.0.0", + "postcss-image-set-function": "^3.0.1", + "postcss-initial": "^3.0.0", + "postcss-lab-function": "^2.0.1", + "postcss-logical": "^3.0.0", + "postcss-media-minmax": "^4.0.0", + "postcss-nesting": "^7.0.0", + "postcss-overflow-shorthand": "^2.0.0", + "postcss-page-break": "^2.0.0", + "postcss-place": "^4.0.1", + "postcss-pseudo-class-any-link": "^6.0.0", + "postcss-replace-overflow-wrap": "^3.0.0", + "postcss-selector-matches": "^4.0.0", + "postcss-selector-not": "^4.0.0" + } + }, + "postcss-pseudo-class-any-link": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-6.0.0.tgz", + "integrity": "sha512-lgXW9sYJdLqtmw23otOzrtbDXofUdfYzNm4PIpNE322/swES3VU9XlXHeJS46zT2onFO7V1QFdD4Q9LiZj8mew==", + "requires": { + "postcss": "^7.0.2", + "postcss-selector-parser": "^5.0.0-rc.3" + }, + "dependencies": { + "cssesc": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz", + "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==" + }, + "postcss-selector-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz", + "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==", + "requires": { + "cssesc": "^2.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + } + } + }, + "postcss-reduce-initial": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-4.0.3.tgz", + "integrity": "sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA==", + "requires": { + "browserslist": "^4.0.0", + "caniuse-api": "^3.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0" + } + }, + "postcss-reduce-transforms": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.2.tgz", + "integrity": "sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg==", + "requires": { + "cssnano-util-get-match": "^4.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" + } + } + }, + "postcss-replace-overflow-wrap": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-3.0.0.tgz", + "integrity": "sha512-2T5hcEHArDT6X9+9dVSPQdo7QHzG4XKclFT8rU5TzJPDN7RIRTbO9c4drUISOVemLj03aezStHCR2AIcr8XLpw==", + "requires": { + "postcss": "^7.0.2" + } + }, + "postcss-safe-parser": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-5.0.2.tgz", + "integrity": "sha512-jDUfCPJbKOABhwpUKcqCVbbXiloe/QXMcbJ6Iipf3sDIihEzTqRCeMBfRaOHxhBuTYqtASrI1KJWxzztZU4qUQ==", + "requires": { + "postcss": "^8.1.0" + }, + "dependencies": { + "postcss": { + "version": "8.1.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.1.10.tgz", + "integrity": "sha512-iBXEV5VTTYaRRdxiFYzTtuv2lGMQBExqkZKSzkJe+Fl6rvQrA/49UVGKqB+LG54hpW/TtDBMGds8j33GFNW7pg==", + "requires": { + "colorette": "^1.2.1", + "nanoid": "^3.1.18", + "source-map": "^0.6.1", + "vfile-location": "^3.2.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "postcss-selector-matches": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-matches/-/postcss-selector-matches-4.0.0.tgz", + "integrity": "sha512-LgsHwQR/EsRYSqlwdGzeaPKVT0Ml7LAT6E75T8W8xLJY62CE4S/l03BWIt3jT8Taq22kXP08s2SfTSzaraoPww==", + "requires": { + "balanced-match": "^1.0.0", + "postcss": "^7.0.2" + } + }, + "postcss-selector-not": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-4.0.0.tgz", + "integrity": "sha512-W+bkBZRhqJaYN8XAnbbZPLWMvZD1wKTu0UxtFKdhtGjWYmxhkUneoeOhRJKdAE5V7ZTlnbHfCR+6bNwK9e1dTQ==", + "requires": { + "balanced-match": "^1.0.0", + "postcss": "^7.0.2" + } + }, + "postcss-selector-parser": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz", + "integrity": "sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw==", + "requires": { + "cssesc": "^3.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1", + "util-deprecate": "^1.0.2" + } + }, + "postcss-svgo": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-4.0.2.tgz", + "integrity": "sha512-C6wyjo3VwFm0QgBy+Fu7gCYOkCmgmClghO+pjcxvrcBKtiKt0uCF+hvbMO1fyv5BMImRK90SMb+dwUnfbGd+jw==", + "requires": { + "is-svg": "^3.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0", + "svgo": "^1.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" + } + } + }, + "postcss-unique-selectors": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz", + "integrity": "sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg==", + "requires": { + "alphanum-sort": "^1.0.0", + "postcss": "^7.0.0", + "uniqs": "^2.0.0" + } + }, + "postcss-value-parser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", + "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==" + }, + "postcss-values-parser": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-2.0.1.tgz", + "integrity": "sha512-2tLuBsA6P4rYTNKCXYG/71C7j1pU6pK503suYOmn4xYrQIzW+opD+7FAFNuGSdZC/3Qfy334QbeMu7MEb8gOxg==", + "requires": { + "flatten": "^1.0.2", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==" + }, + "prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=" + }, + "pretty-bytes": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.4.1.tgz", + "integrity": "sha512-s1Iam6Gwz3JI5Hweaz4GoCD1WUNUIyzePFy5+Js2hjwGVt2Z79wNN+ZKOZ2vB6C+Xs6njyB84Z1IthQg8d9LxA==" + }, + "pretty-error": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.2.tgz", + "integrity": "sha512-EY5oDzmsX5wvuynAByrmY0P0hcp+QpnAKbJng2A2MPjVKXCxrDSUkzghVJ4ZGPIv+JC4gX8fPUWscC0RtjsWGw==", + "requires": { + "lodash": "^4.17.20", + "renderkid": "^2.0.4" + } + }, + "pretty-format": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", + "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", + "requires": { + "@jest/types": "^26.6.2", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^17.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "react-is": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.1.tgz", + "integrity": "sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==" + } + } + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" + }, + "promise": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/promise/-/promise-8.1.0.tgz", + "integrity": "sha512-W04AqnILOL/sPRXziNicCjSNRruLAuIHEOVBazepu0545DDNGYHz7ar9ZgZ1fMU8/MA4mVxp5rkBWRi6OXIy3Q==", + "requires": { + "asap": "~2.0.6" + } + }, + "promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=" + }, + "prompts": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.0.tgz", + "integrity": "sha512-awZAKrk3vN6CroQukBL+R9051a4R3zCZBlJm/HBfrSZ8iTpYix3VX1vU4mveiLpiwmOJT4wokTF9m6HUk4KqWQ==", + "requires": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + } + }, + "prop-types": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + } + }, + "proxy-addr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", + "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.1" + } + }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=" + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" + }, + "psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" + }, + "public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "requires": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" + } + } + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "requires": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + }, + "dependencies": { + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "query-string": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", + "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=", + "requires": { + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + } + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + }, + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=" + }, + "querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, + "raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "requires": { + "performance-now": "^2.1.0" + } + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "requires": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "dependencies": { + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + } + } + }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + } + } + }, + "react": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/react/-/react-17.0.1.tgz", + "integrity": "sha512-lG9c9UuMHdcAexXtigOZLX8exLWkW0Ku29qPRU8uhF2R9BN96dLCt0psvzPLlHc5OWkgymP3qwTRgbnw5BKx3w==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "react-app-polyfill": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/react-app-polyfill/-/react-app-polyfill-2.0.0.tgz", + "integrity": "sha512-0sF4ny9v/B7s6aoehwze9vJNWcmCemAUYBVasscVr92+UYiEqDXOxfKjXN685mDaMRNF3WdhHQs76oTODMocFA==", + "requires": { + "core-js": "^3.6.5", + "object-assign": "^4.1.1", + "promise": "^8.1.0", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "whatwg-fetch": "^3.4.1" + } + }, + "react-dev-utils": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-11.0.1.tgz", + "integrity": "sha512-rlgpCupaW6qQqvu0hvv2FDv40QG427fjghV56XyPcP5aKtOAPzNAhQ7bHqk1YdS2vpW1W7aSV3JobedxuPlBAA==", + "requires": { + "@babel/code-frame": "7.10.4", + "address": "1.1.2", + "browserslist": "4.14.2", + "chalk": "2.4.2", + "cross-spawn": "7.0.3", + "detect-port-alt": "1.1.6", + "escape-string-regexp": "2.0.0", + "filesize": "6.1.0", + "find-up": "4.1.0", + "fork-ts-checker-webpack-plugin": "4.1.6", + "global-modules": "2.0.0", + "globby": "11.0.1", + "gzip-size": "5.1.1", + "immer": "7.0.9", + "is-root": "2.1.0", + "loader-utils": "2.0.0", + "open": "^7.0.2", + "pkg-up": "3.1.0", + "prompts": "2.4.0", + "react-error-overlay": "^6.0.8", + "recursive-readdir": "2.2.2", + "shell-quote": "1.7.2", + "strip-ansi": "6.0.0", + "text-table": "0.2.0" + }, + "dependencies": { + "browserslist": { + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.2.tgz", + "integrity": "sha512-HI4lPveGKUR0x2StIz+2FXfDk9SfVMrxn6PLh1JeGUwcuoDkdKZebWiyLRJ68iIPDpMI4JLVDf7S7XzslgWOhw==", + "requires": { + "caniuse-lite": "^1.0.30001125", + "electron-to-chromium": "^1.3.564", + "escalade": "^3.0.2", + "node-releases": "^1.1.61" + } + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==" + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "react-dom": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.1.tgz", + "integrity": "sha512-6eV150oJZ9U2t9svnsspTMrWNyHc6chX0KzDeAOXftRa8bNeOKTTfCJ7KorIwenkHd2xqVTBTCZd79yk/lx/Ug==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "scheduler": "^0.20.1" + } + }, + "react-error-overlay": { + "version": "6.0.8", + "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.8.tgz", + "integrity": "sha512-HvPuUQnLp5H7TouGq3kzBeioJmXms1wHy9EGjz2OURWBp4qZO6AfGEcnxts1D/CbwPLRAgTMPCEgYhA3sEM4vw==" + }, + "react-flexbox-grid": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/react-flexbox-grid/-/react-flexbox-grid-2.1.2.tgz", + "integrity": "sha512-lj1oVnIJ7TY3W6tPjFUxlUYd1DLFxEg8RiX3HAYVvreE3O9HU9n2390CFoPQ+qk1E+5MXa2t/mLMafFLAa8+7Q==", + "requires": { + "flexboxgrid2": "^7.2.0", + "prop-types": "^15.5.8" + } + }, + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "react-redux": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.2.tgz", + "integrity": "sha512-8+CQ1EvIVFkYL/vu6Olo7JFLWop1qRUeb46sGtIMDCSpgwPQq8fPLpirIB0iTqFe9XYEFPHssdX8/UwN6pAkEA==", + "requires": { + "@babel/runtime": "^7.12.1", + "hoist-non-react-statics": "^3.3.2", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^16.13.1" + } + }, + "react-refresh": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz", + "integrity": "sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg==" + }, + "react-router": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz", + "integrity": "sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==", + "requires": { + "@babel/runtime": "^7.1.2", + "history": "^4.9.0", + "hoist-non-react-statics": "^3.1.0", + "loose-envify": "^1.3.1", + "mini-create-react-context": "^0.4.0", + "path-to-regexp": "^1.7.0", + "prop-types": "^15.6.2", + "react-is": "^16.6.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "requires": { + "isarray": "0.0.1" + } + } + } + }, + "react-router-dom": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.0.tgz", + "integrity": "sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA==", + "requires": { + "@babel/runtime": "^7.1.2", + "history": "^4.9.0", + "loose-envify": "^1.3.1", + "prop-types": "^15.6.2", + "react-router": "5.2.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + } + }, + "react-scripts": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-4.0.1.tgz", + "integrity": "sha512-NnniMSC/wjwhcJAyPJCWtxx6CWONqgvGgV9+QXj1bwoW/JI++YF1eEf3Upf/mQ9KmP57IBdjzWs1XvnPq7qMTQ==", + "requires": { + "@babel/core": "7.12.3", + "@pmmmwh/react-refresh-webpack-plugin": "0.4.2", + "@svgr/webpack": "5.4.0", + "@typescript-eslint/eslint-plugin": "^4.5.0", + "@typescript-eslint/parser": "^4.5.0", + "babel-eslint": "^10.1.0", + "babel-jest": "^26.6.0", + "babel-loader": "8.1.0", + "babel-plugin-named-asset-import": "^0.3.7", + "babel-preset-react-app": "^10.0.0", + "bfj": "^7.0.2", + "camelcase": "^6.1.0", + "case-sensitive-paths-webpack-plugin": "2.3.0", + "css-loader": "4.3.0", + "dotenv": "8.2.0", + "dotenv-expand": "5.1.0", + "eslint": "^7.11.0", + "eslint-config-react-app": "^6.0.0", + "eslint-plugin-flowtype": "^5.2.0", + "eslint-plugin-import": "^2.22.1", + "eslint-plugin-jest": "^24.1.0", + "eslint-plugin-jsx-a11y": "^6.3.1", + "eslint-plugin-react": "^7.21.5", + "eslint-plugin-react-hooks": "^4.2.0", + "eslint-plugin-testing-library": "^3.9.2", + "eslint-webpack-plugin": "^2.1.0", + "file-loader": "6.1.1", + "fs-extra": "^9.0.1", + "fsevents": "^2.1.3", + "html-webpack-plugin": "4.5.0", + "identity-obj-proxy": "3.0.0", + "jest": "26.6.0", + "jest-circus": "26.6.0", + "jest-resolve": "26.6.0", + "jest-watch-typeahead": "0.6.1", + "mini-css-extract-plugin": "0.11.3", + "optimize-css-assets-webpack-plugin": "5.0.4", + "pnp-webpack-plugin": "1.6.4", + "postcss-flexbugs-fixes": "4.2.1", + "postcss-loader": "3.0.0", + "postcss-normalize": "8.0.1", + "postcss-preset-env": "6.7.0", + "postcss-safe-parser": "5.0.2", + "prompts": "2.4.0", + "react-app-polyfill": "^2.0.0", + "react-dev-utils": "^11.0.1", + "react-refresh": "^0.8.3", + "resolve": "1.18.1", + "resolve-url-loader": "^3.1.2", + "sass-loader": "8.0.2", + "semver": "7.3.2", + "style-loader": "1.3.0", + "terser-webpack-plugin": "4.2.3", + "ts-pnp": "1.2.0", + "url-loader": "4.1.1", + "webpack": "4.44.2", + "webpack-dev-server": "3.11.0", + "webpack-manifest-plugin": "2.2.0", + "workbox-webpack-plugin": "5.1.4" + } + }, + "react-transition-group": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.1.tgz", + "integrity": "sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw==", + "requires": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + } + }, + "read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "requires": { + "load-json-file": "^2.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^2.0.0" + }, + "dependencies": { + "path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "requires": { + "pify": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + } + } + }, + "read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^2.0.0" + }, + "dependencies": { + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "requires": { + "locate-path": "^2.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + } + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readdirp": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", + "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", + "optional": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "recursive-readdir": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz", + "integrity": "sha512-nRCcW9Sj7NuZwa2XvH9co8NPeXUBhZP7CRKJtU+cS6PW9FpCIFoI5ib0NT1ZrbNuPoRy0ylyCaUL8Gih4LSyFg==", + "requires": { + "minimatch": "3.0.4" + } + }, + "redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "requires": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + } + }, + "redux": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz", + "integrity": "sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==", + "requires": { + "loose-envify": "^1.4.0", + "symbol-observable": "^1.2.0" + } + }, + "redux-thunk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz", + "integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==" + }, + "regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==" + }, + "regenerate-unicode-properties": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz", + "integrity": "sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA==", + "requires": { + "regenerate": "^1.4.0" + } + }, + "regenerator-runtime": { + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==" + }, + "regenerator-transform": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.5.tgz", + "integrity": "sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw==", + "requires": { + "@babel/runtime": "^7.8.4" + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "regex-parser": { + "version": "2.2.11", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.11.tgz", + "integrity": "sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==" + }, + "regexp.prototype.flags": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz", + "integrity": "sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", + "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } + } + }, + "regexpp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", + "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==" + }, + "regexpu-core": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.1.tgz", + "integrity": "sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ==", + "requires": { + "regenerate": "^1.4.0", + "regenerate-unicode-properties": "^8.2.0", + "regjsgen": "^0.5.1", + "regjsparser": "^0.6.4", + "unicode-match-property-ecmascript": "^1.0.4", + "unicode-match-property-value-ecmascript": "^1.2.0" + } + }, + "registry-auth-token": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz", + "integrity": "sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==", + "requires": { + "rc": "^1.1.6", + "safe-buffer": "^5.0.1" + } + }, + "registry-url": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", + "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", + "requires": { + "rc": "^1.0.1" + } + }, + "regjsgen": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.2.tgz", + "integrity": "sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A==" + }, + "regjsparser": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.4.tgz", + "integrity": "sha512-64O87/dPDgfk8/RQqC4gkZoGyyWFIEUTTh80CU6CWuK5vkCGyekIx+oKcEIYtP/RAxSQltCZHCNu/mdd7fqlJw==", + "requires": { + "jsesc": "~0.5.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=" + } + } + }, + "relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=" + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" + }, + "renderkid": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.4.tgz", + "integrity": "sha512-K2eXrSOJdq+HuKzlcjOlGoOarUu5SDguDEhE7+Ah4zuOWL40j8A/oHvLlLob9PSTNvVnBd+/q0Er1QfpEuem5g==", + "requires": { + "css-select": "^1.1.0", + "dom-converter": "^0.2", + "htmlparser2": "^3.3.0", + "lodash": "^4.17.20", + "strip-ansi": "^3.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "css-select": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", + "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", + "requires": { + "boolbase": "~1.0.0", + "css-what": "2.1", + "domutils": "1.5.1", + "nth-check": "~1.0.1" + } + }, + "css-what": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", + "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==" + }, + "domutils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, + "repeat-element": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==" + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + }, + "request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "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.3", + "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.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + } + } + }, + "request-promise-core": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz", + "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==", + "requires": { + "lodash": "^4.17.19" + } + }, + "request-promise-native": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.9.tgz", + "integrity": "sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==", + "requires": { + "request-promise-core": "1.1.4", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + }, + "dependencies": { + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + } + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" + }, + "reselect": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.0.0.tgz", + "integrity": "sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==" + }, + "resolve": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.18.1.tgz", + "integrity": "sha512-lDfCPaMKfOJXjy0dPayzPdF1phampNWr3qFCjAu+rw/qbQmr5jWH5xN2hwh9QKfw9E5v4hwV7A+jrCmL8yjjqA==", + "requires": { + "is-core-module": "^2.0.0", + "path-parse": "^1.0.6" + } + }, + "resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "requires": { + "resolve-from": "^5.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==" + } + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" + }, + "resolve-pathname": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", + "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==" + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" + }, + "resolve-url-loader": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-3.1.2.tgz", + "integrity": "sha512-QEb4A76c8Mi7I3xNKXlRKQSlLBwjUV/ULFMP+G7n3/7tJZ8MG5wsZ3ucxP1Jz8Vevn6fnJsxDx9cIls+utGzPQ==", + "requires": { + "adjust-sourcemap-loader": "3.0.0", + "camelcase": "5.3.1", + "compose-function": "3.0.3", + "convert-source-map": "1.7.0", + "es6-iterator": "2.0.3", + "loader-utils": "1.2.3", + "postcss": "7.0.21", + "rework": "1.0.1", + "rework-visit": "1.0.0", + "source-map": "0.6.1" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, + "emojis-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", + "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=" + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + } + }, + "postcss": { + "version": "7.0.21", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.21.tgz", + "integrity": "sha512-uIFtJElxJo29QC753JzhidoAhvp/e/Exezkdhfmt8AymWT6/5B7W1WmponYWkHk2eg6sONyTch0A3nkMPun3SQ==", + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" + }, + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=" + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" + }, + "rework": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rework/-/rework-1.0.1.tgz", + "integrity": "sha1-MIBqhBNCtUUQqkEQhQzUhTQUSqc=", + "requires": { + "convert-source-map": "^0.3.3", + "css": "^2.0.0" + }, + "dependencies": { + "convert-source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-0.3.5.tgz", + "integrity": "sha1-8dgClQr33SYxof6+BZZVDIarMZA=" + } + } + }, + "rework-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rework-visit/-/rework-visit-1.0.0.tgz", + "integrity": "sha1-mUWygD8hni96ygCtuLyfZA+ELJo=" + }, + "rgb-regex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz", + "integrity": "sha1-wODWiC3w4jviVKR16O3UGRX+rrE=" + }, + "rgba-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", + "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=" + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "requires": { + "glob": "^7.1.3" + } + }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "rollup": { + "version": "1.32.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-1.32.1.tgz", + "integrity": "sha512-/2HA0Ec70TvQnXdzynFffkjA6XN+1e2pEv/uKS5Ulca40g2L7KuOE3riasHoNVHOsFD5KKZgDsMk1CP3Tw9s+A==", + "requires": { + "@types/estree": "*", + "@types/node": "*", + "acorn": "^7.1.0" + } + }, + "rollup-plugin-babel": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-babel/-/rollup-plugin-babel-4.4.0.tgz", + "integrity": "sha512-Lek/TYp1+7g7I+uMfJnnSJ7YWoD58ajo6Oarhlex7lvUce+RCKRuGRSgztDO3/MF/PuGKmUL5iTHKf208UNszw==", + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "rollup-pluginutils": "^2.8.1" + } + }, + "rollup-plugin-terser": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-5.3.1.tgz", + "integrity": "sha512-1pkwkervMJQGFYvM9nscrUoncPwiKR/K+bHdjv6PFgRo3cgPHoRT83y2Aa3GvINj4539S15t/tpFPb775TDs6w==", + "requires": { + "@babel/code-frame": "^7.5.5", + "jest-worker": "^24.9.0", + "rollup-pluginutils": "^2.8.2", + "serialize-javascript": "^4.0.0", + "terser": "^4.6.2" + }, + "dependencies": { + "jest-worker": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-24.9.0.tgz", + "integrity": "sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw==", + "requires": { + "merge-stream": "^2.0.0", + "supports-color": "^6.1.0" + } + }, + "serialize-javascript": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "requires": { + "randombytes": "^2.1.0" + } + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "rollup-pluginutils": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", + "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", + "requires": { + "estree-walker": "^0.6.1" + }, + "dependencies": { + "estree-walker": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", + "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==" + } + } + }, + "rsvp": { + "version": "4.8.5", + "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", + "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==" + }, + "run-parallel": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.10.tgz", + "integrity": "sha512-zb/1OuZ6flOlH6tQyMPUrE3x3Ulxjlo9WIVXR4yVYi4H9UXQaeIsPbLn2R3O3vQCnDKkAl2qHiuocKKX4Tz/Sw==" + }, + "run-queue": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", + "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", + "requires": { + "aproba": "^1.1.1" + } + }, + "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==" + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "requires": { + "ret": "~0.1.10" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "sane": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/sane/-/sane-4.1.0.tgz", + "integrity": "sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==", + "requires": { + "@cnakazawa/watch": "^1.0.3", + "anymatch": "^2.0.0", + "capture-exit": "^2.0.0", + "exec-sh": "^0.3.2", + "execa": "^1.0.0", + "fb-watchman": "^2.0.0", + "micromatch": "^3.1.4", + "minimist": "^1.1.1", + "walker": "~1.0.5" + }, + "dependencies": { + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + } + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "requires": { + "remove-trailing-separator": "^1.0.1" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, + "sanitize.css": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/sanitize.css/-/sanitize.css-10.0.0.tgz", + "integrity": "sha512-vTxrZz4dX5W86M6oVWVdOVe72ZiPs41Oi7Z6Km4W5Turyz28mrXSJhhEBZoRtzJWIv3833WKVwLSDWWkEfupMg==" + }, + "sass-loader": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-8.0.2.tgz", + "integrity": "sha512-7o4dbSK8/Ol2KflEmSco4jTjQoV988bM82P9CZdmo9hR3RLnvNc0ufMNdMrB0caq38JQ/FgF4/7RcbcfKzxoFQ==", + "requires": { + "clone-deep": "^4.0.1", + "loader-utils": "^1.2.3", + "neo-async": "^2.6.1", + "schema-utils": "^2.6.1", + "semver": "^6.3.0" + }, + "dependencies": { + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", + "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "requires": { + "xmlchars": "^2.2.0" + } + }, + "scheduler": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.1.tgz", + "integrity": "sha512-LKTe+2xNJBNxu/QhHvDR14wUXHRQbVY5ZOYpOGWRzhydZUqrLb2JBvLPY7cAqFmqrWuDED0Mjk7013SZiOz6Bw==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "schema-utils": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", + "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", + "requires": { + "@types/json-schema": "^7.0.5", + "ajv": "^6.12.4", + "ajv-keywords": "^3.5.2" + } + }, + "select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=" + }, + "selfsigned": { + "version": "1.10.8", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.8.tgz", + "integrity": "sha512-2P4PtieJeEwVgTU9QEcwIRDQ/mXJLX8/+I3ur+Pg16nS8oNbrGxEso9NyYWy8NAmXiNl4dlAp5MwoNeCWzON4w==", + "requires": { + "node-forge": "^0.10.0" + } + }, + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==" + }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } + }, + "serialize-javascript": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz", + "integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==", + "requires": { + "randombytes": "^2.1.0" + } + }, + "serve": { + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/serve/-/serve-11.3.2.tgz", + "integrity": "sha512-yKWQfI3xbj/f7X1lTBg91fXBP0FqjJ4TEi+ilES5yzH0iKJpN5LjNb1YzIfQg9Rqn4ECUS2SOf2+Kmepogoa5w==", + "requires": { + "@zeit/schemas": "2.6.0", + "ajv": "6.5.3", + "arg": "2.0.0", + "boxen": "1.3.0", + "chalk": "2.4.1", + "clipboardy": "1.2.3", + "compression": "1.7.3", + "serve-handler": "6.1.3", + "update-check": "1.5.2" + }, + "dependencies": { + "ajv": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.3.tgz", + "integrity": "sha512-LqZ9wY+fx3UMiiPd741yB2pj3hhil+hQc8taf4o2QGRFpWgZ2V5C8HA165DY9sS3fJwsk7uT7ZlFEyC3Ig3lLg==", + "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" + } + }, + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "compression": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.3.tgz", + "integrity": "sha512-HSjyBG5N1Nnz7tF2+O7A9XUhyjru71/fwgNb7oIsEVHR0WShfs2tIS/EySLgiTe98aOK18YDlMXpzjCXY/n9mg==", + "requires": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.14", + "debug": "2.6.9", + "on-headers": "~1.0.1", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "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=" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "serve-handler": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.3.tgz", + "integrity": "sha512-FosMqFBNrLyeiIDvP1zgO6YoTzFYHxLDEIavhlmQ+knB2Z7l1t+kGLHkZIDN7UVWqQAmKI3D20A6F6jo3nDd4w==", + "requires": { + "bytes": "3.0.0", + "content-disposition": "0.5.2", + "fast-url-parser": "1.1.3", + "mime-types": "2.1.18", + "minimatch": "3.0.4", + "path-is-inside": "1.0.2", + "path-to-regexp": "2.2.1", + "range-parser": "1.2.0" + }, + "dependencies": { + "content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" + }, + "mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" + }, + "mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "requires": { + "mime-db": "~1.33.0" + } + }, + "path-to-regexp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.2.1.tgz", + "integrity": "sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ==" + }, + "range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" + } + } + }, + "serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", + "requires": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + } + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "requires": { + "kind-of": "^6.0.2" + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" + }, + "shell-quote": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz", + "integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==" + }, + "shellwords": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", + "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", + "optional": true + }, + "side-channel": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.3.tgz", + "integrity": "sha512-A6+ByhlLkksFoUepsGxfj5x1gTSrs+OydsRptUxeNCabQpCFUvcwIczgOigI8vhY/OJCnPnyE9rGiwgvr9cS1g==", + "requires": { + "es-abstract": "^1.18.0-next.0", + "object-inspect": "^1.8.0" + } + }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" + }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "requires": { + "is-arrayish": "^0.3.1" + }, + "dependencies": { + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + } + } + }, + "sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==" + }, + "slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "requires": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + } + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "requires": { + "kind-of": "^3.2.0" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "sockjs": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.20.tgz", + "integrity": "sha512-SpmVOVpdq0DJc0qArhF3E5xsxvaiqGNb73XfgBpK1y3UD5gs8DSo8aCTsuT5pX8rssdc2NDIzANwP9eCAiSdTA==", + "requires": { + "faye-websocket": "^0.10.0", + "uuid": "^3.4.0", + "websocket-driver": "0.6.5" + }, + "dependencies": { + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + } + } + }, + "sockjs-client": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.4.0.tgz", + "integrity": "sha512-5zaLyO8/nri5cua0VtOrFXBPK1jbL4+1cebT/mmKA1E1ZXOvJrII75bPu0l0k843G/+iAbhEqzyKr0w/eCCj7g==", + "requires": { + "debug": "^3.2.5", + "eventsource": "^1.0.7", + "faye-websocket": "~0.11.1", + "inherits": "^2.0.3", + "json3": "^3.3.2", + "url-parse": "^1.4.3" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "faye-websocket": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz", + "integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==", + "requires": { + "websocket-driver": ">=0.5.1" + } + } + } + }, + "sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", + "requires": { + "is-plain-obj": "^1.0.0" + } + }, + "source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==" + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "source-map-resolve": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "requires": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" + }, + "sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" + }, + "spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==" + }, + "spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz", + "integrity": "sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==" + }, + "spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "requires": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + } + }, + "spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "requires": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, + "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" + } + }, + "ssri": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.0.tgz", + "integrity": "sha512-aq/pz989nxVYwn16Tsbj1TqFpD5LLrQxHf5zaHuieFV+R0Bbr4y8qUsOA45hXT/N4/9UNXTarBjnjVmjSOVaAA==", + "requires": { + "minipass": "^3.1.1" + } + }, + "stable": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==" + }, + "stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-gL//fkxfWUsIlFL2Tl42Cl6+HFALEaB1FU76I/Fy+oZjRreP7OPMXFlGbxM7NQsI0ZpUfw76sHnv0WNYuTb7Iw==", + "requires": { + "escape-string-regexp": "^2.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==" + } + } + }, + "stackframe": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.2.0.tgz", + "integrity": "sha512-GrdeshiRmS1YLMYgzF16olf2jJ/IzxXY9lhKOskuVziubpTYcYqyOwYeJKzQkwy7uN0fYSsbsC4RQaXf9LCrYA==" + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=" + }, + "stream-browserify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", + "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", + "requires": { + "inherits": "~2.0.1", + "readable-stream": "^2.0.2" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "stream-each": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", + "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", + "requires": { + "end-of-stream": "^1.1.0", + "stream-shift": "^1.0.0" + } + }, + "stream-http": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", + "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", + "requires": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.3.6", + "to-arraybuffer": "^1.0.0", + "xtend": "^4.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" + }, + "strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=" + }, + "string-length": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.1.tgz", + "integrity": "sha512-PKyXUd0LK0ePjSOnWn34V2uD6acUWev9uy0Ft05k0E8xRW+SKcA0F7eMr7h5xlzfn+4O3N+55rduYyet3Jk+jw==", + "requires": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + } + }, + "string-natural-compare": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/string-natural-compare/-/string-natural-compare-3.0.1.tgz", + "integrity": "sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==" + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "string.prototype.matchall": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.3.tgz", + "integrity": "sha512-OBxYDA2ifZQ2e13cP82dWFMaCV9CGF8GzmN4fljBVw5O5wep0lu4gacm1OL6MjROoUnB8VbkWRThqkV2YFLNxw==", + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1", + "has-symbols": "^1.0.1", + "internal-slot": "^1.0.2", + "regexp.prototype.flags": "^1.3.0", + "side-channel": "^1.0.3" + } + }, + "string.prototype.trimend": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.3.tgz", + "integrity": "sha512-ayH0pB+uf0U28CtjlLvL7NaohvR1amUvVZk+y3DYb0Ey2PUV5zPkkKy9+U1ndVEIXO8hNg18eIv9Jntbii+dKw==", + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + } + }, + "string.prototype.trimstart": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.3.tgz", + "integrity": "sha512-oBIBUy5lea5tt0ovtOFiEQaBkoBBkyJhZXzJYrSmDo5IUUqbOPvVezuRs/agBIdZ2p2Eo1FD6bD9USyBLfl3xg==", + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } + } + }, + "stringify-object": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", + "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "requires": { + "get-own-enumerable-property-symbols": "^3.0.0", + "is-obj": "^1.0.1", + "is-regexp": "^1.0.0" + }, + "dependencies": { + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=" + } + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" + }, + "strip-comments": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-1.0.2.tgz", + "integrity": "sha512-kL97alc47hoyIQSV165tTt9rG5dn4w1dNnBhOQ3bOU1Nc1hel09jnXANaHJ7vzHLd4Ju8kseDGzlev96pghLFw==", + "requires": { + "babel-extract-comments": "^1.0.0", + "babel-plugin-transform-object-rest-spread": "^6.26.0" + } + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==" + }, + "strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "requires": { + "min-indent": "^1.0.0" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" + }, + "style-loader": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-1.3.0.tgz", + "integrity": "sha512-V7TCORko8rs9rIqkSrlMfkqA63DfoGBBJmK1kKGCcSi+BWb4cqz0SRsnp4l6rU5iwOEd0/2ePv68SV22VXon4Q==", + "requires": { + "loader-utils": "^2.0.0", + "schema-utils": "^2.7.0" + } + }, + "stylehacks": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-4.0.3.tgz", + "integrity": "sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g==", + "requires": { + "browserslist": "^4.0.0", + "postcss": "^7.0.0", + "postcss-selector-parser": "^3.0.0" + }, + "dependencies": { + "postcss-selector-parser": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz", + "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", + "requires": { + "dot-prop": "^5.2.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + } + } + }, + "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" + } + }, + "supports-hyperlinks": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.1.0.tgz", + "integrity": "sha512-zoE5/e+dnEijk6ASB6/qrK+oYdm2do1hjoLWrqUC/8WEIW1gbxFcKuBof7sW8ArN6e+AYvsE8HBGiVRWL/F5CA==", + "requires": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "svg-parser": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", + "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==" + }, + "svgo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz", + "integrity": "sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==", + "requires": { + "chalk": "^2.4.1", + "coa": "^2.0.2", + "css-select": "^2.0.0", + "css-select-base-adapter": "^0.1.1", + "css-tree": "1.0.0-alpha.37", + "csso": "^4.0.2", + "js-yaml": "^3.13.1", + "mkdirp": "~0.5.1", + "object.values": "^1.1.0", + "sax": "~1.2.4", + "stable": "^0.1.8", + "unquote": "~1.1.1", + "util.promisify": "~1.0.0" + } + }, + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" + }, + "symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" + }, + "table": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "requires": { + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + } + }, + "tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==" + }, + "tar": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.0.5.tgz", + "integrity": "sha512-0b4HOimQHj9nXNEAA7zWwMM91Zhhba3pspja6sQbgTpynOJf+bkjBnfybNYzbpLbnwXnbyB4LOREvlyXLkCHSg==", + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "dependencies": { + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + } + } + }, + "temp-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz", + "integrity": "sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0=" + }, + "tempy": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/tempy/-/tempy-0.3.0.tgz", + "integrity": "sha512-WrH/pui8YCwmeiAoxV+lpRH9HpRtgBhSR2ViBPgpGb/wnYDzp21R4MN45fsCGvLROvY67o3byhJRYRONJyImVQ==", + "requires": { + "temp-dir": "^1.0.0", + "type-fest": "^0.3.1", + "unique-string": "^1.0.0" + }, + "dependencies": { + "type-fest": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz", + "integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==" + } + } + }, + "term-size": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", + "integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=", + "requires": { + "execa": "^0.7.0" + }, + "dependencies": { + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "requires": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "requires": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" + }, + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + } + } + }, + "terminal-link": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", + "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", + "requires": { + "ansi-escapes": "^4.2.1", + "supports-hyperlinks": "^2.0.0" + } + }, + "terser": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", + "integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==", + "requires": { + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "terser-webpack-plugin": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-4.2.3.tgz", + "integrity": "sha512-jTgXh40RnvOrLQNgIkwEKnQ8rmHjHK4u+6UBEi+W+FPmvb+uo+chJXntKe7/3lW5mNysgSWD60KyesnhW8D6MQ==", + "requires": { + "cacache": "^15.0.5", + "find-cache-dir": "^3.3.1", + "jest-worker": "^26.5.0", + "p-limit": "^3.0.2", + "schema-utils": "^3.0.0", + "serialize-javascript": "^5.0.1", + "source-map": "^0.6.1", + "terser": "^5.3.4", + "webpack-sources": "^1.4.3" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "find-cache-dir": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", + "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "requires": { + "semver": "^6.0.0" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "requires": { + "find-up": "^4.0.0" + } + }, + "schema-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", + "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", + "requires": { + "@types/json-schema": "^7.0.6", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "terser": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.5.1.tgz", + "integrity": "sha512-6VGWZNVP2KTUcltUQJ25TtNjx/XgdDsBDKGt8nN0MpydU36LmbPPcMBd2kmtZNNGVVDLg44k7GKeHHj+4zPIBQ==", + "requires": { + "commander": "^2.20.0", + "source-map": "~0.7.2", + "source-map-support": "~0.5.19" + }, + "dependencies": { + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" + } + } + } + } + }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=" + }, + "throat": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", + "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==" + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==" + }, + "timers-browserify": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", + "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==", + "requires": { + "setimmediate": "^1.0.4" + } + }, + "timsort": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", + "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=" + }, + "tiny-invariant": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz", + "integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==" + }, + "tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + }, + "tmpl": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", + "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=" + }, + "to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=" + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "requires": { + "is-number": "^7.0.0" + } + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, + "tough-cookie": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", + "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==", + "requires": { + "ip-regex": "^2.1.0", + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "tr46": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.0.2.tgz", + "integrity": "sha512-3n1qG+/5kg+jrbTzwAykB5yRYtQCTqOGKq5U5PE3b0a1/mzo6snDhjGS0zJVJunO0NrT3Dg1MLy5TjWP/UJppg==", + "requires": { + "punycode": "^2.1.1" + } + }, + "tryer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", + "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==" + }, + "ts-pnp": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ts-pnp/-/ts-pnp-1.2.0.tgz", + "integrity": "sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==" + }, + "tsconfig-paths": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz", + "integrity": "sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw==", + "requires": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.0", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "requires": { + "minimist": "^1.2.0" + } + } + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "tsutils": { + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", + "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==", + "requires": { + "tslib": "^1.8.1" + } + }, + "tty-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=" + }, + "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=" + }, + "type": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", + "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==" + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "requires": { + "is-typedarray": "^1.0.0" + } + }, + "unicode-canonical-property-names-ecmascript": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", + "integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==" + }, + "unicode-match-property-ecmascript": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz", + "integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==", + "requires": { + "unicode-canonical-property-names-ecmascript": "^1.0.4", + "unicode-property-aliases-ecmascript": "^1.0.4" + } + }, + "unicode-match-property-value-ecmascript": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz", + "integrity": "sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ==" + }, + "unicode-property-aliases-ecmascript": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz", + "integrity": "sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==" + }, + "union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + } + }, + "uniq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", + "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=" + }, + "uniqs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz", + "integrity": "sha1-/+3ks2slKQaW5uFl1KWe25mOawI=" + }, + "unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "requires": { + "unique-slug": "^2.0.0" + } + }, + "unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "requires": { + "imurmurhash": "^0.1.4" + } + }, + "unique-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", + "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", + "requires": { + "crypto-random-string": "^1.0.0" + } + }, + "universalify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz", + "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==" + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "unquote": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", + "integrity": "sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=" + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" + } + } + }, + "upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==" + }, + "update-check": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/update-check/-/update-check-1.5.2.tgz", + "integrity": "sha512-1TrmYLuLj/5ZovwUS7fFd1jMH3NnFDN1y1A8dboedIDt7zs/zJMo6TwwlhYKkSeEwzleeiSBV5/3c9ufAQWDaQ==", + "requires": { + "registry-auth-token": "3.3.2", + "registry-url": "3.1.0" + } + }, + "uri-js": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz", + "integrity": "sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g==", + "requires": { + "punycode": "^2.1.0" + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + } + } + }, + "url-loader": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-4.1.1.tgz", + "integrity": "sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA==", + "requires": { + "loader-utils": "^2.0.0", + "mime-types": "^2.1.27", + "schema-utils": "^3.0.0" + }, + "dependencies": { + "schema-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", + "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", + "requires": { + "@types/json-schema": "^7.0.6", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, + "url-parse": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz", + "integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==", + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" + }, + "util": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", + "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", + "requires": { + "inherits": "2.0.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + } + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "util.promisify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.1.tgz", + "integrity": "sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.2", + "has-symbols": "^1.0.1", + "object.getownpropertydescriptors": "^2.1.0" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", + "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } + } + }, + "utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "uuid": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz", + "integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==", + "optional": true + }, + "v8-compile-cache": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz", + "integrity": "sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q==" + }, + "v8-to-istanbul": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-7.0.0.tgz", + "integrity": "sha512-fLL2rFuQpMtm9r8hrAV2apXX/WqHJ6+IC4/eQVdMDGBUgH/YMV4Gv3duk3kjmyg6uiQWBAA9nJwue4iJUOkHeA==", + "requires": { + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0", + "source-map": "^0.7.3" + }, + "dependencies": { + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" + } + } + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "value-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", + "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "vendors": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.4.tgz", + "integrity": "sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==" + }, + "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" + } + }, + "vfile-location": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-3.2.0.tgz", + "integrity": "sha512-aLEIZKv/oxuCDZ8lkJGhuhztf/BW4M+iHdCwglA/eWc+vtuRFJj8EtgceYFX4LRjOhCAAiNHsKGssC6onJ+jbA==" + }, + "vm-browserify": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", + "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==" + }, + "w3c-hr-time": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", + "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", + "requires": { + "browser-process-hrtime": "^1.0.0" + } + }, + "w3c-xmlserializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", + "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", + "requires": { + "xml-name-validator": "^3.0.0" + } + }, + "wait-for-expect": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/wait-for-expect/-/wait-for-expect-3.0.2.tgz", + "integrity": "sha512-cfS1+DZxuav1aBYbaO/kE06EOS8yRw7qOFoD3XtjTkYvCvh3zUvNST8DXK/nPaeqIzIv3P3kL3lRJn8iwOiSag==" + }, + "walker": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", + "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", + "requires": { + "makeerror": "1.0.x" + } + }, + "watchpack": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.5.tgz", + "integrity": "sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ==", + "requires": { + "chokidar": "^3.4.1", + "graceful-fs": "^4.1.2", + "neo-async": "^2.5.0", + "watchpack-chokidar2": "^2.0.1" + } + }, + "watchpack-chokidar2": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz", + "integrity": "sha512-nCFfBIPKr5Sh61s4LPpy1Wtfi0HE8isJ3d2Yb5/Ppw2P2B/3eVSEBjKfN0fmHJSK14+31KwMKmcrzs2GM4P0Ww==", + "optional": true, + "requires": { + "chokidar": "^2.1.8" + }, + "dependencies": { + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "optional": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "optional": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "optional": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "optional": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "optional": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "optional": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "optional": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "optional": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fsevents": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "optional": true + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "optional": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "optional": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "optional": true, + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "optional": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "optional": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "optional": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "optional": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "optional": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, + "wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "requires": { + "minimalistic-assert": "^1.0.0" + } + }, + "webidl-conversions": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", + "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==" + }, + "webpack": { + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.44.2.tgz", + "integrity": "sha512-6KJVGlCxYdISyurpQ0IPTklv+DULv05rs2hseIXer6D7KrUicRDLFb4IUM1S6LUAKypPM/nSiVSuv8jHu1m3/Q==", + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-module-context": "1.9.0", + "@webassemblyjs/wasm-edit": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0", + "acorn": "^6.4.1", + "ajv": "^6.10.2", + "ajv-keywords": "^3.4.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^4.3.0", + "eslint-scope": "^4.0.3", + "json-parse-better-errors": "^1.0.2", + "loader-runner": "^2.4.0", + "loader-utils": "^1.2.3", + "memory-fs": "^0.4.1", + "micromatch": "^3.1.10", + "mkdirp": "^0.5.3", + "neo-async": "^2.6.1", + "node-libs-browser": "^2.2.1", + "schema-utils": "^1.0.0", + "tapable": "^1.1.3", + "terser-webpack-plugin": "^1.4.3", + "watchpack": "^1.7.4", + "webpack-sources": "^1.4.1" + }, + "dependencies": { + "acorn": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", + "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==" + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "cacache": { + "version": "12.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.4.tgz", + "integrity": "sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==", + "requires": { + "bluebird": "^3.5.5", + "chownr": "^1.1.1", + "figgy-pudding": "^3.5.1", + "glob": "^7.1.4", + "graceful-fs": "^4.1.15", + "infer-owner": "^1.0.3", + "lru-cache": "^5.1.1", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.3", + "ssri": "^6.0.1", + "unique-filename": "^1.1.1", + "y18n": "^4.0.0" + } + }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "eslint-scope": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=" + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", + "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + } + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "requires": { + "yallist": "^3.0.2" + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + }, + "serialize-javascript": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "requires": { + "randombytes": "^2.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "ssri": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", + "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", + "requires": { + "figgy-pudding": "^3.5.1" + } + }, + "terser-webpack-plugin": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz", + "integrity": "sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==", + "requires": { + "cacache": "^12.0.2", + "find-cache-dir": "^2.1.0", + "is-wsl": "^1.1.0", + "schema-utils": "^1.0.0", + "serialize-javascript": "^4.0.0", + "source-map": "^0.6.1", + "terser": "^4.1.2", + "webpack-sources": "^1.4.0", + "worker-farm": "^1.7.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + } + } + }, + "webpack-dev-middleware": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.7.2.tgz", + "integrity": "sha512-1xC42LxbYoqLNAhV6YzTYacicgMZQTqRd27Sim9wn5hJrX3I5nxYy1SxSd4+gjUFsz1dQFj+yEe6zEVmSkeJjw==", + "requires": { + "memory-fs": "^0.4.1", + "mime": "^2.4.4", + "mkdirp": "^0.5.1", + "range-parser": "^1.2.1", + "webpack-log": "^2.0.0" + }, + "dependencies": { + "mime": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", + "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==" + } + } + }, + "webpack-dev-server": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.11.0.tgz", + "integrity": "sha512-PUxZ+oSTxogFQgkTtFndEtJIPNmml7ExwufBZ9L2/Xyyd5PnOL5UreWe5ZT7IU25DSdykL9p1MLQzmLh2ljSeg==", + "requires": { + "ansi-html": "0.0.7", + "bonjour": "^3.5.0", + "chokidar": "^2.1.8", + "compression": "^1.7.4", + "connect-history-api-fallback": "^1.6.0", + "debug": "^4.1.1", + "del": "^4.1.1", + "express": "^4.17.1", + "html-entities": "^1.3.1", + "http-proxy-middleware": "0.19.1", + "import-local": "^2.0.0", + "internal-ip": "^4.3.0", + "ip": "^1.1.5", + "is-absolute-url": "^3.0.3", + "killable": "^1.0.1", + "loglevel": "^1.6.8", + "opn": "^5.5.0", + "p-retry": "^3.0.1", + "portfinder": "^1.0.26", + "schema-utils": "^1.0.0", + "selfsigned": "^1.10.7", + "semver": "^6.3.0", + "serve-index": "^1.9.1", + "sockjs": "0.3.20", + "sockjs-client": "1.4.0", + "spdy": "^4.0.2", + "strip-ansi": "^3.0.1", + "supports-color": "^6.1.0", + "url": "^0.11.0", + "webpack-dev-middleware": "^3.7.2", + "webpack-log": "^2.0.0", + "ws": "^6.2.1", + "yargs": "^13.3.2" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==" + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, + "chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + } + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "requires": { + "locate-path": "^3.0.0" + } + }, + "fsevents": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "optional": true + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "import-local": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", + "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", + "requires": { + "pkg-dir": "^3.0.0", + "resolve-cwd": "^2.0.0" + } + }, + "is-absolute-url": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-3.0.3.tgz", + "integrity": "sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==" + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } + }, + "resolve-cwd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", + "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", + "requires": { + "resolve-from": "^3.0.0" + } + }, + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=" + }, + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "ws": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", + "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", + "requires": { + "async-limiter": "~1.0.0" + } + }, + "yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + } + }, + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "webpack-log": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-2.0.0.tgz", + "integrity": "sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==", + "requires": { + "ansi-colors": "^3.0.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "ansi-colors": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", + "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==" + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + } + } + }, + "webpack-manifest-plugin": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/webpack-manifest-plugin/-/webpack-manifest-plugin-2.2.0.tgz", + "integrity": "sha512-9S6YyKKKh/Oz/eryM1RyLVDVmy3NSPV0JXMRhZ18fJsq+AwGxUY34X54VNwkzYcEmEkDwNxuEOboCZEebJXBAQ==", + "requires": { + "fs-extra": "^7.0.0", + "lodash": ">=3.5 <5", + "object.entries": "^1.1.0", + "tapable": "^1.0.0" + }, + "dependencies": { + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + } + } + }, + "webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "websocket-driver": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.6.5.tgz", + "integrity": "sha1-XLJVbOuF9Dc8bYI4qmkchFThOjY=", + "requires": { + "websocket-extensions": ">=0.1.1" + } + }, + "websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==" + }, + "whatwg-encoding": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", + "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "requires": { + "iconv-lite": "0.4.24" + } + }, + "whatwg-fetch": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.5.0.tgz", + "integrity": "sha512-jXkLtsR42xhXg7akoDKvKWE40eJeI+2KZqcp2h3NsOrRnDvtWX36KcKl30dy+hxECivdk2BVUHVNrPtoMBUx6A==" + }, + "whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==" + }, + "whatwg-url": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.4.0.tgz", + "integrity": "sha512-vwTUFf6V4zhcPkWp/4CQPr1TW9Ml6SF4lVyaIMBdJw5i6qUUJ1QWM4Z6YYVkfka0OUIzVo/0aNtGVGk256IKWw==", + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^2.0.2", + "webidl-conversions": "^6.1.0" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + }, + "widest-line": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz", + "integrity": "sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==", + "requires": { + "string-width": "^2.1.1" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" + }, + "workbox-background-sync": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-5.1.4.tgz", + "integrity": "sha512-AH6x5pYq4vwQvfRDWH+vfOePfPIYQ00nCEB7dJRU1e0n9+9HMRyvI63FlDvtFT2AvXVRsXvUt7DNMEToyJLpSA==", + "requires": { + "workbox-core": "^5.1.4" + } + }, + "workbox-broadcast-update": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-5.1.4.tgz", + "integrity": "sha512-HTyTWkqXvHRuqY73XrwvXPud/FN6x3ROzkfFPsRjtw/kGZuZkPzfeH531qdUGfhtwjmtO/ZzXcWErqVzJNdXaA==", + "requires": { + "workbox-core": "^5.1.4" + } + }, + "workbox-build": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-5.1.4.tgz", + "integrity": "sha512-xUcZn6SYU8usjOlfLb9Y2/f86Gdo+fy1fXgH8tJHjxgpo53VVsqRX0lUDw8/JuyzNmXuo8vXX14pXX2oIm9Bow==", + "requires": { + "@babel/core": "^7.8.4", + "@babel/preset-env": "^7.8.4", + "@babel/runtime": "^7.8.4", + "@hapi/joi": "^15.1.0", + "@rollup/plugin-node-resolve": "^7.1.1", + "@rollup/plugin-replace": "^2.3.1", + "@surma/rollup-plugin-off-main-thread": "^1.1.1", + "common-tags": "^1.8.0", + "fast-json-stable-stringify": "^2.1.0", + "fs-extra": "^8.1.0", + "glob": "^7.1.6", + "lodash.template": "^4.5.0", + "pretty-bytes": "^5.3.0", + "rollup": "^1.31.1", + "rollup-plugin-babel": "^4.3.3", + "rollup-plugin-terser": "^5.3.1", + "source-map": "^0.7.3", + "source-map-url": "^0.4.0", + "stringify-object": "^3.3.0", + "strip-comments": "^1.0.2", + "tempy": "^0.3.0", + "upath": "^1.2.0", + "workbox-background-sync": "^5.1.4", + "workbox-broadcast-update": "^5.1.4", + "workbox-cacheable-response": "^5.1.4", + "workbox-core": "^5.1.4", + "workbox-expiration": "^5.1.4", + "workbox-google-analytics": "^5.1.4", + "workbox-navigation-preload": "^5.1.4", + "workbox-precaching": "^5.1.4", + "workbox-range-requests": "^5.1.4", + "workbox-routing": "^5.1.4", + "workbox-strategies": "^5.1.4", + "workbox-streams": "^5.1.4", + "workbox-sw": "^5.1.4", + "workbox-window": "^5.1.4" + }, + "dependencies": { + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + } + } + }, + "workbox-cacheable-response": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-5.1.4.tgz", + "integrity": "sha512-0bfvMZs0Of1S5cdswfQK0BXt6ulU5kVD4lwer2CeI+03czHprXR3V4Y8lPTooamn7eHP8Iywi5QjyAMjw0qauA==", + "requires": { + "workbox-core": "^5.1.4" + } + }, + "workbox-core": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-5.1.4.tgz", + "integrity": "sha512-+4iRQan/1D8I81nR2L5vcbaaFskZC2CL17TLbvWVzQ4qiF/ytOGF6XeV54pVxAvKUtkLANhk8TyIUMtiMw2oDg==" + }, + "workbox-expiration": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-5.1.4.tgz", + "integrity": "sha512-oDO/5iC65h2Eq7jctAv858W2+CeRW5e0jZBMNRXpzp0ZPvuT6GblUiHnAsC5W5lANs1QS9atVOm4ifrBiYY7AQ==", + "requires": { + "workbox-core": "^5.1.4" + } + }, + "workbox-google-analytics": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-5.1.4.tgz", + "integrity": "sha512-0IFhKoEVrreHpKgcOoddV+oIaVXBFKXUzJVBI+nb0bxmcwYuZMdteBTp8AEDJacENtc9xbR0wa9RDCnYsCDLjA==", + "requires": { + "workbox-background-sync": "^5.1.4", + "workbox-core": "^5.1.4", + "workbox-routing": "^5.1.4", + "workbox-strategies": "^5.1.4" + } + }, + "workbox-navigation-preload": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-5.1.4.tgz", + "integrity": "sha512-Wf03osvK0wTflAfKXba//QmWC5BIaIZARU03JIhAEO2wSB2BDROWI8Q/zmianf54kdV7e1eLaIEZhth4K4MyfQ==", + "requires": { + "workbox-core": "^5.1.4" + } + }, + "workbox-precaching": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-5.1.4.tgz", + "integrity": "sha512-gCIFrBXmVQLFwvAzuGLCmkUYGVhBb7D1k/IL7pUJUO5xacjLcFUaLnnsoVepBGAiKw34HU1y/YuqvTKim9qAZA==", + "requires": { + "workbox-core": "^5.1.4" + } + }, + "workbox-range-requests": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-5.1.4.tgz", + "integrity": "sha512-1HSujLjgTeoxHrMR2muDW2dKdxqCGMc1KbeyGcmjZZAizJTFwu7CWLDmLv6O1ceWYrhfuLFJO+umYMddk2XMhw==", + "requires": { + "workbox-core": "^5.1.4" + } + }, + "workbox-routing": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-5.1.4.tgz", + "integrity": "sha512-8ljknRfqE1vEQtnMtzfksL+UXO822jJlHTIR7+BtJuxQ17+WPZfsHqvk1ynR/v0EHik4x2+826Hkwpgh4GKDCw==", + "requires": { + "workbox-core": "^5.1.4" + } + }, + "workbox-strategies": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-5.1.4.tgz", + "integrity": "sha512-VVS57LpaJTdjW3RgZvPwX0NlhNmscR7OQ9bP+N/34cYMDzXLyA6kqWffP6QKXSkca1OFo/v6v7hW7zrrguo6EA==", + "requires": { + "workbox-core": "^5.1.4", + "workbox-routing": "^5.1.4" + } + }, + "workbox-streams": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-5.1.4.tgz", + "integrity": "sha512-xU8yuF1hI/XcVhJUAfbQLa1guQUhdLMPQJkdT0kn6HP5CwiPOGiXnSFq80rAG4b1kJUChQQIGPrq439FQUNVrw==", + "requires": { + "workbox-core": "^5.1.4", + "workbox-routing": "^5.1.4" + } + }, + "workbox-sw": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-5.1.4.tgz", + "integrity": "sha512-9xKnKw95aXwSNc8kk8gki4HU0g0W6KXu+xks7wFuC7h0sembFnTrKtckqZxbSod41TDaGh+gWUA5IRXrL0ECRA==" + }, + "workbox-webpack-plugin": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/workbox-webpack-plugin/-/workbox-webpack-plugin-5.1.4.tgz", + "integrity": "sha512-PZafF4HpugZndqISi3rZ4ZK4A4DxO8rAqt2FwRptgsDx7NF8TVKP86/huHquUsRjMGQllsNdn4FNl8CD/UvKmQ==", + "requires": { + "@babel/runtime": "^7.5.5", + "fast-json-stable-stringify": "^2.0.0", + "source-map-url": "^0.4.0", + "upath": "^1.1.2", + "webpack-sources": "^1.3.0", + "workbox-build": "^5.1.4" + } + }, + "workbox-window": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-5.1.4.tgz", + "integrity": "sha512-vXQtgTeMCUq/4pBWMfQX8Ee7N2wVC4Q7XYFqLnfbXJ2hqew/cU1uMTD2KqGEgEpE4/30luxIxgE+LkIa8glBYw==", + "requires": { + "workbox-core": "^5.1.4" + } + }, + "worker-farm": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz", + "integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==", + "requires": { + "errno": "~0.1.7" + } + }, + "worker-rpc": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/worker-rpc/-/worker-rpc-0.1.1.tgz", + "integrity": "sha512-P1WjMrUB3qgJNI9jfmpZ/htmBEjFh//6l/5y8SD9hg1Ef5zTTVVoRjTrTEzPrNBQvmhMxkoTsjOXN10GWU7aCg==", + "requires": { + "microevent.ts": "~0.1.1" + } + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "write": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", + "requires": { + "mkdirp": "^0.5.1" + } + }, + "write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "ws": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.0.tgz", + "integrity": "sha512-kyFwXuV/5ymf+IXhS6f0+eAFvydbaBW3zjpT6hUdAh/hbVjTIB5EHBGi0bPoCLSK2wcuz3BrEkB9LrYv1Nm4NQ==" + }, + "xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==" + }, + "xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + }, + "y18n": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", + "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "yaml": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz", + "integrity": "sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==" + }, + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "dependencies": { + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + } + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + } + } + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" + } + } +} diff --git a/.internal/wui/package.json b/.internal/wui/package.json new file mode 100644 index 000000000..c129bfc13 --- /dev/null +++ b/.internal/wui/package.json @@ -0,0 +1,43 @@ +{ + "name": "iotstack_wui", + "version": "0.0.1", + "private": true, + "dependencies": { + "@material-ui/core": "^4.11.2", + "@material-ui/icons": "^4.11.2", + "@material-ui/lab": "^4.0.0-alpha.57", + "@reduxjs/toolkit": "^1.5.0", + "@testing-library/jest-dom": "^4.2.4", + "@testing-library/react": "^9.5.0", + "@testing-library/user-event": "^7.2.1", + "react": "^17.0.1", + "react-dom": "^17.0.1", + "react-flexbox-grid": "^2.1.2", + "react-redux": "^7.2.2", + "react-router-dom": "^5.2.0", + "react-scripts": "4.0.1", + "serve": "^11.3.2" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject", + "serve": "serve -s build" + }, + "eslintConfig": { + "extends": "react-app" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/.internal/wui/public/favicon.ico b/.internal/wui/public/favicon.ico new file mode 100644 index 000000000..354202a4e Binary files /dev/null and b/.internal/wui/public/favicon.ico differ diff --git a/.internal/wui/public/index.html b/.internal/wui/public/index.html new file mode 100644 index 000000000..430b58fa3 --- /dev/null +++ b/.internal/wui/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React Redux App + + + +
+ + + diff --git a/.internal/wui/public/logo192.png b/.internal/wui/public/logo192.png new file mode 100644 index 000000000..33624109c Binary files /dev/null and b/.internal/wui/public/logo192.png differ diff --git a/.internal/wui/public/logo512.png b/.internal/wui/public/logo512.png new file mode 100644 index 000000000..b3516222f Binary files /dev/null and b/.internal/wui/public/logo512.png differ diff --git a/.internal/wui/public/logos/adguardhome.png b/.internal/wui/public/logos/adguardhome.png new file mode 100644 index 000000000..0bde6eda5 Binary files /dev/null and b/.internal/wui/public/logos/adguardhome.png differ diff --git a/.internal/wui/public/logos/adminer.png b/.internal/wui/public/logos/adminer.png new file mode 100644 index 000000000..0f9d83e8c Binary files /dev/null and b/.internal/wui/public/logos/adminer.png differ diff --git a/.internal/wui/public/logos/blynk.png b/.internal/wui/public/logos/blynk.png new file mode 100644 index 000000000..ff42a96ff Binary files /dev/null and b/.internal/wui/public/logos/blynk.png differ diff --git a/.internal/wui/public/logos/dashmachine.png b/.internal/wui/public/logos/dashmachine.png new file mode 100644 index 000000000..66887ac8c Binary files /dev/null and b/.internal/wui/public/logos/dashmachine.png differ diff --git a/.internal/wui/public/logos/deconz.png b/.internal/wui/public/logos/deconz.png new file mode 100644 index 000000000..582024664 Binary files /dev/null and b/.internal/wui/public/logos/deconz.png differ diff --git a/.internal/wui/public/logos/diyhue.png b/.internal/wui/public/logos/diyhue.png new file mode 100644 index 000000000..10f2cb7d4 Binary files /dev/null and b/.internal/wui/public/logos/diyhue.png differ diff --git a/.internal/wui/public/logos/domoticz.png b/.internal/wui/public/logos/domoticz.png new file mode 100644 index 000000000..066af0c11 Binary files /dev/null and b/.internal/wui/public/logos/domoticz.png differ diff --git a/.internal/wui/public/logos/dozzle.svg b/.internal/wui/public/logos/dozzle.svg new file mode 100644 index 000000000..07cc88842 --- /dev/null +++ b/.internal/wui/public/logos/dozzle.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/.internal/wui/public/logos/espruinohub.png b/.internal/wui/public/logos/espruinohub.png new file mode 100644 index 000000000..65b3274dc Binary files /dev/null and b/.internal/wui/public/logos/espruinohub.png differ diff --git a/.internal/wui/public/logos/gitea.png b/.internal/wui/public/logos/gitea.png new file mode 100644 index 000000000..5dacd7735 Binary files /dev/null and b/.internal/wui/public/logos/gitea.png differ diff --git a/.internal/wui/public/logos/grafana.svg b/.internal/wui/public/logos/grafana.svg new file mode 100644 index 000000000..e3ca2727a --- /dev/null +++ b/.internal/wui/public/logos/grafana.svg @@ -0,0 +1,64 @@ + + + + + + + + + + diff --git a/.internal/wui/public/logos/heimdall.png b/.internal/wui/public/logos/heimdall.png new file mode 100644 index 000000000..49a1787e2 Binary files /dev/null and b/.internal/wui/public/logos/heimdall.png differ diff --git a/.internal/wui/public/logos/homeassistant.png b/.internal/wui/public/logos/homeassistant.png new file mode 100644 index 000000000..b0c61416a Binary files /dev/null and b/.internal/wui/public/logos/homeassistant.png differ diff --git a/.internal/wui/public/logos/homebridge.png b/.internal/wui/public/logos/homebridge.png new file mode 100644 index 000000000..5f1f3343a Binary files /dev/null and b/.internal/wui/public/logos/homebridge.png differ diff --git a/.internal/wui/public/logos/homer.png b/.internal/wui/public/logos/homer.png new file mode 100644 index 000000000..e088a4aa3 Binary files /dev/null and b/.internal/wui/public/logos/homer.png differ diff --git a/.internal/wui/public/logos/influxdb.svg b/.internal/wui/public/logos/influxdb.svg new file mode 100644 index 000000000..c38fd251c --- /dev/null +++ b/.internal/wui/public/logos/influxdb.svg @@ -0,0 +1,23 @@ + + + + + + + diff --git a/.internal/wui/public/logos/mariadb.png b/.internal/wui/public/logos/mariadb.png new file mode 100644 index 000000000..b97338c2a Binary files /dev/null and b/.internal/wui/public/logos/mariadb.png differ diff --git a/.internal/wui/public/logos/mosquitto.png b/.internal/wui/public/logos/mosquitto.png new file mode 100644 index 000000000..eb287a7cd Binary files /dev/null and b/.internal/wui/public/logos/mosquitto.png differ diff --git a/.internal/wui/public/logos/motioneye.png b/.internal/wui/public/logos/motioneye.png new file mode 100644 index 000000000..25696605e Binary files /dev/null and b/.internal/wui/public/logos/motioneye.png differ diff --git a/.internal/wui/public/logos/nextcloud.png b/.internal/wui/public/logos/nextcloud.png new file mode 100644 index 000000000..6d22548d1 Binary files /dev/null and b/.internal/wui/public/logos/nextcloud.png differ diff --git a/.internal/wui/public/logos/nodered.png b/.internal/wui/public/logos/nodered.png new file mode 100644 index 000000000..8be1d6434 Binary files /dev/null and b/.internal/wui/public/logos/nodered.png differ diff --git a/.internal/wui/public/logos/octoprint.png b/.internal/wui/public/logos/octoprint.png new file mode 100644 index 000000000..a8ce6fb4f Binary files /dev/null and b/.internal/wui/public/logos/octoprint.png differ diff --git a/.internal/wui/public/logos/openhab.png b/.internal/wui/public/logos/openhab.png new file mode 100644 index 000000000..034d76f7a Binary files /dev/null and b/.internal/wui/public/logos/openhab.png differ diff --git a/.internal/wui/public/logos/pihole.png b/.internal/wui/public/logos/pihole.png new file mode 100644 index 000000000..9758df4ef Binary files /dev/null and b/.internal/wui/public/logos/pihole.png differ diff --git a/.internal/wui/public/logos/plex.png b/.internal/wui/public/logos/plex.png new file mode 100644 index 000000000..e486ebfd9 Binary files /dev/null and b/.internal/wui/public/logos/plex.png differ diff --git a/.internal/wui/public/logos/portainer.png b/.internal/wui/public/logos/portainer.png new file mode 100644 index 000000000..3b82f4631 Binary files /dev/null and b/.internal/wui/public/logos/portainer.png differ diff --git a/.internal/wui/public/logos/postgres.png b/.internal/wui/public/logos/postgres.png new file mode 100644 index 000000000..50c9196e1 Binary files /dev/null and b/.internal/wui/public/logos/postgres.png differ diff --git a/.internal/wui/public/logos/prometheus.svg b/.internal/wui/public/logos/prometheus.svg new file mode 100644 index 000000000..b914095ec --- /dev/null +++ b/.internal/wui/public/logos/prometheus.svg @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/.internal/wui/public/logos/qbittorrent.svg b/.internal/wui/public/logos/qbittorrent.svg new file mode 100644 index 000000000..69d8cf62a --- /dev/null +++ b/.internal/wui/public/logos/qbittorrent.svg @@ -0,0 +1,16 @@ + + + qbittorrent-new-light + + + + + + + + + + + + + \ No newline at end of file diff --git a/.internal/wui/public/logos/redis.png b/.internal/wui/public/logos/redis.png new file mode 100644 index 000000000..0a0754bf3 Binary files /dev/null and b/.internal/wui/public/logos/redis.png differ diff --git a/.internal/wui/public/logos/tasmoadmin.png b/.internal/wui/public/logos/tasmoadmin.png new file mode 100644 index 000000000..16646dd37 Binary files /dev/null and b/.internal/wui/public/logos/tasmoadmin.png differ diff --git a/.internal/wui/public/logos/telegraf.png b/.internal/wui/public/logos/telegraf.png new file mode 100644 index 000000000..ea6eca0f6 Binary files /dev/null and b/.internal/wui/public/logos/telegraf.png differ diff --git a/.internal/wui/public/logos/timescaledb.png b/.internal/wui/public/logos/timescaledb.png new file mode 100644 index 000000000..63857a65d Binary files /dev/null and b/.internal/wui/public/logos/timescaledb.png differ diff --git a/.internal/wui/public/logos/transmission.png b/.internal/wui/public/logos/transmission.png new file mode 100644 index 000000000..e6771036f Binary files /dev/null and b/.internal/wui/public/logos/transmission.png differ diff --git a/.internal/wui/public/logos/webthings.png b/.internal/wui/public/logos/webthings.png new file mode 100644 index 000000000..7614ddacf Binary files /dev/null and b/.internal/wui/public/logos/webthings.png differ diff --git a/.internal/wui/public/logos/wireguard.png b/.internal/wui/public/logos/wireguard.png new file mode 100644 index 000000000..f0e9e2fbf Binary files /dev/null and b/.internal/wui/public/logos/wireguard.png differ diff --git a/.internal/wui/public/logos/zigbee2mqtt.png b/.internal/wui/public/logos/zigbee2mqtt.png new file mode 100644 index 000000000..69cfcd9c0 Binary files /dev/null and b/.internal/wui/public/logos/zigbee2mqtt.png differ diff --git a/.internal/wui/public/manifest.json b/.internal/wui/public/manifest.json new file mode 100644 index 000000000..2f2be64fe --- /dev/null +++ b/.internal/wui/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/.internal/wui/public/robots.txt b/.internal/wui/public/robots.txt new file mode 100644 index 000000000..fa06a7563 --- /dev/null +++ b/.internal/wui/public/robots.txt @@ -0,0 +1,2 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * diff --git a/.internal/wui/src/App.css b/.internal/wui/src/App.css new file mode 100644 index 000000000..355c49bf9 --- /dev/null +++ b/.internal/wui/src/App.css @@ -0,0 +1,39 @@ +.App { + text-align: center; +} + +.App-logo { + height: 40vmin; + pointer-events: none; +} + +@media (prefers-reduced-motion: no-preference) { + .App-logo { + animation: App-logo-float infinite 3s ease-in-out; + } +} + +.App-header { + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: calc(10px + 2vmin); +} + +.App-link { + color: rgb(112, 76, 182); +} + +@keyframes App-logo-float { + 0% { + transform: translateY(0); + } + 50% { + transform: translateY(10px) + } + 100% { + transform: translateY(0px) + } +} diff --git a/.internal/wui/src/App.js b/.internal/wui/src/App.js new file mode 100644 index 000000000..451367cbc --- /dev/null +++ b/.internal/wui/src/App.js @@ -0,0 +1,42 @@ +import React, { Fragment } from 'react'; +import useMediaQuery from '@material-ui/core/useMediaQuery'; +import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles'; +import blueGrey from "@material-ui/core/colors/blueGrey"; +import lightGreen from "@material-ui/core/colors/lightGreen"; +import CssBaseline from '@material-ui/core/CssBaseline'; +import ReactRouterBootstrap from './router' +import './App.css'; + +function App() { + const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); + const theme = React.useMemo( + () => + createMuiTheme({ + palette: { + type: prefersDarkMode ? 'dark' : 'light', + primary: { + light: lightGreen[300], + main: lightGreen[500], + dark: lightGreen[700] + }, + secondary: { + light: blueGrey[300], + main: blueGrey[500], + dark: blueGrey[700] + } + }, + }), + [prefersDarkMode], + ); + + return ( + + + + + + + ); +} + +export default App; diff --git a/.internal/wui/src/actions/buildStack.action.js b/.internal/wui/src/actions/buildStack.action.js new file mode 100644 index 000000000..631568ca5 --- /dev/null +++ b/.internal/wui/src/actions/buildStack.action.js @@ -0,0 +1,24 @@ +import { createAndBuildStack } from '../services/builds' + +const CREATE_AND_BUILD_STACK = 'CREATE_AND_BUILD_STACK'; +const RESET_STATE_CREATE_AND_BUILD_STACK = 'RESET_STATE_CREATE_AND_BUILD_STACK'; + +const createAndBuildStackAction = (selectedServices, configurations) => { + return { + type: CREATE_AND_BUILD_STACK, + promise: createAndBuildStack({ selectedServices, configurations }) + } +}; + +const clearBuildStateAction = () => { + return { + type: RESET_STATE_CREATE_AND_BUILD_STACK + } +}; + +export { + CREATE_AND_BUILD_STACK, + RESET_STATE_CREATE_AND_BUILD_STACK, + createAndBuildStackAction, + clearBuildStateAction +}; diff --git a/.internal/wui/src/actions/checkBuildIssues.action.js b/.internal/wui/src/actions/checkBuildIssues.action.js new file mode 100644 index 000000000..3a02ccdcd --- /dev/null +++ b/.internal/wui/src/actions/checkBuildIssues.action.js @@ -0,0 +1,15 @@ +import { getBuildIssues } from '../services/builds' + +const CHECK_BUILD_ISSUES = 'CHECK_BUILD_ISSUES'; + +const getBuildIssuesAction = (selectedServices, configurations) => { + return { + type: CHECK_BUILD_ISSUES, + promise: getBuildIssues({ selectedServices, configurations }) + } +}; + +export { + CHECK_BUILD_ISSUES, + getBuildIssuesAction +}; diff --git a/.internal/wui/src/actions/deleteBuild.action.js b/.internal/wui/src/actions/deleteBuild.action.js new file mode 100644 index 000000000..107e10a7e --- /dev/null +++ b/.internal/wui/src/actions/deleteBuild.action.js @@ -0,0 +1,24 @@ +import { deleteBuild } from '../services/builds' + +const DELETE_BUILD = 'DELETE_BUILD'; +const RESET_STATE_DELETE_BUILD = 'RESET_STATE_DELETE_BUILD'; + +const deleteBuildAction = ({ build }) => { + return { + type: DELETE_BUILD, + promise: deleteBuild({ build }) + } +}; + +const clearDeleteBuildAction = () => { + return { + type: RESET_STATE_DELETE_BUILD + } +}; + +export { + DELETE_BUILD, + RESET_STATE_DELETE_BUILD, + deleteBuildAction, + clearDeleteBuildAction +}; diff --git a/.internal/wui/src/actions/downloadBuild.action.js b/.internal/wui/src/actions/downloadBuild.action.js new file mode 100644 index 000000000..41823ebf0 --- /dev/null +++ b/.internal/wui/src/actions/downloadBuild.action.js @@ -0,0 +1,16 @@ +import { downloadBuild } from '../services/builds' + +const DOWNLOAD_BUILD = 'DOWNLOAD_BUILD'; + +const downloadBuildFile = ({ build, type, linkRef }) => { + return { + type: DOWNLOAD_BUILD, + promise: downloadBuild({ build, type, linkRef }), + label: build + } +}; + +export { + DOWNLOAD_BUILD, + downloadBuildFile +}; diff --git a/.internal/wui/src/actions/getAllServicesConfigHelp.action.js b/.internal/wui/src/actions/getAllServicesConfigHelp.action.js new file mode 100644 index 000000000..d50cb8edf --- /dev/null +++ b/.internal/wui/src/actions/getAllServicesConfigHelp.action.js @@ -0,0 +1,15 @@ +import { getAllServicesConfigHelp } from '../services/configs'; + +const GET_ALL_SERVICES_CONFIG_HELP_ACTION = 'GET_ALL_SERVICES_CONFIG_HELP_ACTION'; + +const getAllServicesConfigHelpAction = () => { + return { + type: GET_ALL_SERVICES_CONFIG_HELP_ACTION, + promise: getAllServicesConfigHelp() + } +}; + +export { + GET_ALL_SERVICES_CONFIG_HELP_ACTION, + getAllServicesConfigHelpAction +}; diff --git a/.internal/wui/src/actions/getAllServicesConfigOptions.action.js b/.internal/wui/src/actions/getAllServicesConfigOptions.action.js new file mode 100644 index 000000000..4aa6aa8e2 --- /dev/null +++ b/.internal/wui/src/actions/getAllServicesConfigOptions.action.js @@ -0,0 +1,15 @@ +import { getAllServicesConfigOptions } from '../services/configs'; + +const GET_ALL_SERVICES_CONFIG_OPTIONS_ACTION = 'GET_ALL_SERVICES_CONFIG_OPTIONS_ACTION'; + +const getAllServicesConfigOptionsAction = () => { + return { + type: GET_ALL_SERVICES_CONFIG_OPTIONS_ACTION, + promise: getAllServicesConfigOptions() + } +}; + +export { + GET_ALL_SERVICES_CONFIG_OPTIONS_ACTION, + getAllServicesConfigOptionsAction +}; diff --git a/.internal/wui/src/actions/getAllServicesMetadata.action.js b/.internal/wui/src/actions/getAllServicesMetadata.action.js new file mode 100644 index 000000000..7c286a163 --- /dev/null +++ b/.internal/wui/src/actions/getAllServicesMetadata.action.js @@ -0,0 +1,15 @@ +import { getAllServicesMetadata } from '../services/configs'; + +const GET_ALL_SERVICES_METADATA_ACTION = 'GET_ALL_SERVICES_METADATA_ACTION'; + +const getAllServicesMetadataAction = () => { + return { + type: GET_ALL_SERVICES_METADATA_ACTION, + promise: getAllServicesMetadata() + } +}; + +export { + GET_ALL_SERVICES_METADATA_ACTION, + getAllServicesMetadataAction +}; diff --git a/.internal/wui/src/actions/getBuildFile.action.js b/.internal/wui/src/actions/getBuildFile.action.js new file mode 100644 index 000000000..5dbeb8941 --- /dev/null +++ b/.internal/wui/src/actions/getBuildFile.action.js @@ -0,0 +1,16 @@ +import { getBuildFile } from '../services/builds' + +const GET_BUILD_FILE_ACTION = 'GET_BUILD_FILE_ACTION'; + +const getBuildFileAction = ({ build, type, label }) => { + return { + type: GET_BUILD_FILE_ACTION, + promise: getBuildFile({ build, type }), + label + } +}; + +export { + GET_BUILD_FILE_ACTION, + getBuildFileAction +}; diff --git a/.internal/wui/src/actions/getBuildHistoryList.action.js b/.internal/wui/src/actions/getBuildHistoryList.action.js new file mode 100644 index 000000000..874e67a25 --- /dev/null +++ b/.internal/wui/src/actions/getBuildHistoryList.action.js @@ -0,0 +1,15 @@ +import { getBuildHistoryList } from '../services/builds' + +const GET_BUILD_HISTORY_LIST_ACTION = 'GET_BUILD_HISTORY_LIST_ACTION'; + +const getBuildHistoryListAction = () => { + return { + type: GET_BUILD_HISTORY_LIST_ACTION, + promise: getBuildHistoryList() + } +}; + +export { + GET_BUILD_HISTORY_LIST_ACTION, + getBuildHistoryListAction +}; diff --git a/.internal/wui/src/actions/getNetworkTemplateList.action.js b/.internal/wui/src/actions/getNetworkTemplateList.action.js new file mode 100644 index 000000000..6fa999f58 --- /dev/null +++ b/.internal/wui/src/actions/getNetworkTemplateList.action.js @@ -0,0 +1,15 @@ +import { getNetworkTemplatesList } from '../services/templates' + +const GET_NETWORK_TEMPLATE_LIST_ACTION = 'GET_NETWORK_TEMPLATE_LIST_ACTION'; + +const getNetworkTemplateListAction = () => { + return { + type: GET_NETWORK_TEMPLATE_LIST_ACTION, + promise: getNetworkTemplatesList() + } +}; + +export { + GET_NETWORK_TEMPLATE_LIST_ACTION, + getNetworkTemplateListAction +}; diff --git a/.internal/wui/src/actions/getScript.action.js b/.internal/wui/src/actions/getScript.action.js new file mode 100644 index 000000000..8b01dd579 --- /dev/null +++ b/.internal/wui/src/actions/getScript.action.js @@ -0,0 +1,16 @@ +import { getScriptTemplate } from '../services/templates' + +const GET_SCRIPT_TEMPLATE = 'GET_SCRIPT_TEMPLATE'; + +const getScriptFromTemplateAction = ({ scriptName, options, linkRef }) => { + return { + type: GET_SCRIPT_TEMPLATE, + promise: getScriptTemplate({ scriptName, options, linkRef }), + label: scriptName + } +}; + +export { + GET_SCRIPT_TEMPLATE, + getScriptFromTemplateAction +}; diff --git a/.internal/wui/src/actions/getServiceConfigOptions.action.js b/.internal/wui/src/actions/getServiceConfigOptions.action.js new file mode 100644 index 000000000..0f83dc263 --- /dev/null +++ b/.internal/wui/src/actions/getServiceConfigOptions.action.js @@ -0,0 +1,16 @@ +import { getServiceConfigOptions } from '../services/configs' + +const GET_CONFIG_SERVICE_CONFIG_OPTIONS = 'GET_CONFIG_SERVICE_CONFIG_OPTIONS'; + +const getServiceConfigOptionsAction = (serviceName) => { + return { + type: GET_CONFIG_SERVICE_CONFIG_OPTIONS, + promise: getServiceConfigOptions(serviceName), + label: serviceName + } +}; + +export { + GET_CONFIG_SERVICE_CONFIG_OPTIONS, + getServiceConfigOptionsAction +}; diff --git a/.internal/wui/src/actions/getServiceMetadata.action.js b/.internal/wui/src/actions/getServiceMetadata.action.js new file mode 100644 index 000000000..9336c9ea4 --- /dev/null +++ b/.internal/wui/src/actions/getServiceMetadata.action.js @@ -0,0 +1,16 @@ +import { getServiceMetadata } from '../services/configs' + +const GET_CONFIG_SERVICE_METADATA = 'GET_CONFIG_SERVICE_METADATA'; + +const getServiceMetadataAction = (serviceName) => { + return { + type: GET_CONFIG_SERVICE_METADATA, + promise: getServiceMetadata(serviceName), + label: serviceName + } +}; + +export { + GET_CONFIG_SERVICE_METADATA, + getServiceMetadataAction +}; diff --git a/.internal/wui/src/actions/getServiceTemplateList.action.js b/.internal/wui/src/actions/getServiceTemplateList.action.js new file mode 100644 index 000000000..361383dd7 --- /dev/null +++ b/.internal/wui/src/actions/getServiceTemplateList.action.js @@ -0,0 +1,15 @@ +import { getServiceTemplatesList } from '../services/templates' + +const GET_SERVICE_TEMPLATE_LIST_ACTION = 'GET_SERVICE_TEMPLATE_LIST_ACTION'; + +const getServiceTemplateListAction = () => { + return { + type: GET_SERVICE_TEMPLATE_LIST_ACTION, + promise: getServiceTemplatesList() + } +}; + +export { + GET_SERVICE_TEMPLATE_LIST_ACTION, + getServiceTemplateListAction +}; diff --git a/.internal/wui/src/actions/getServiceTemplates.action.js b/.internal/wui/src/actions/getServiceTemplates.action.js new file mode 100644 index 000000000..f0aaa8c71 --- /dev/null +++ b/.internal/wui/src/actions/getServiceTemplates.action.js @@ -0,0 +1,15 @@ +import { getServiceTemplates } from '../services/templates'; + +const GET_SERVICE_TEMPLATES_ACTION = 'GET_SERVICE_TEMPLATES_ACTION'; + +const getServiceTemplatesAction = () => { + return { + type: GET_SERVICE_TEMPLATES_ACTION, + promise: getServiceTemplates() + } +}; + +export { + GET_SERVICE_TEMPLATES_ACTION, + getServiceTemplatesAction +}; diff --git a/.internal/wui/src/actions/updateFilterTags.action.js b/.internal/wui/src/actions/updateFilterTags.action.js new file mode 100644 index 000000000..8f9e5c3a2 --- /dev/null +++ b/.internal/wui/src/actions/updateFilterTags.action.js @@ -0,0 +1,27 @@ +const ADD_TO_FILTER_HIDE_LIST = 'ADD_TO_FILTER_SHOW_LIST'; +const REMOVE_FROM_FILTER_HIDE_LIST = 'REMOVE_FROM_FILTER_HIDE_LIST'; + +const addTagToHideListAction = (filterTag) => { + return { + type: ADD_TO_FILTER_HIDE_LIST, + data: { + filterTag + } + }; +}; + +const removeTagFromHideListAction = (filterTag) => { + return { + type: REMOVE_FROM_FILTER_HIDE_LIST, + data: { + filterTag + } + }; +}; + +export { + ADD_TO_FILTER_HIDE_LIST, + REMOVE_FROM_FILTER_HIDE_LIST, + addTagToHideListAction, + removeTagFromHideListAction +}; diff --git a/.internal/wui/src/actions/updateSelectedServices.action.js b/.internal/wui/src/actions/updateSelectedServices.action.js new file mode 100644 index 000000000..10be10d08 --- /dev/null +++ b/.internal/wui/src/actions/updateSelectedServices.action.js @@ -0,0 +1,37 @@ +const ADD_TO_SELECTED_SERVICES = 'ADD_TO_SELECTED_SERVICES'; +const REMOVE_FROM_SELECTED_SERVICES = 'REMOVE_FROM_SELECTED_SERVICES'; +const CLEAR_ALL_SELECTED_SERVICES = 'CLEAR_ALL_SELECTED_SERVICES'; + +const addSelectedService = (serviceName) => { + return { + type: ADD_TO_SELECTED_SERVICES, + data: { + serviceName + } + } +}; + +const removeSelectedService = (serviceName) => { + return { + type: REMOVE_FROM_SELECTED_SERVICES, + data: { + serviceName + } + } +}; + +const clearAllSelectedServicesAction = () => { + return { + type: CLEAR_ALL_SELECTED_SERVICES, + data: {} + } +}; + +export { + ADD_TO_SELECTED_SERVICES, + REMOVE_FROM_SELECTED_SERVICES, + CLEAR_ALL_SELECTED_SERVICES, + addSelectedService, + removeSelectedService, + clearAllSelectedServicesAction +}; diff --git a/.internal/wui/src/config.js b/.internal/wui/src/config.js new file mode 100644 index 000000000..da7544a3a --- /dev/null +++ b/.internal/wui/src/config.js @@ -0,0 +1,7 @@ +const config = { + apiUrl: window?.location?.hostname ?? '[::1]', + apiPort: '32128', + apiProtocol: `${window?.location?.protocol}//` ?? 'http://' +}; + +export default config; diff --git a/.internal/wui/src/constants.js b/.internal/wui/src/constants.js new file mode 100644 index 000000000..2f00de768 --- /dev/null +++ b/.internal/wui/src/constants.js @@ -0,0 +1,10 @@ +const API_STATUS = Object.freeze({ + UNINIT: 'UNINIT', + PENDING: 'PENDING', + SUCCESS: 'SUCCESS', + FAILURE: 'FAILURE' +}); + +module.exports = { + API_STATUS +}; diff --git a/.internal/wui/src/features/BuildSidebar/build-sidebar.module.css b/.internal/wui/src/features/BuildSidebar/build-sidebar.module.css new file mode 100644 index 000000000..0bc7efa3d --- /dev/null +++ b/.internal/wui/src/features/BuildSidebar/build-sidebar.module.css @@ -0,0 +1,11 @@ +.sidebarWrapper { + display: 'block'; + min-width: 40rem; + max-width: 50rem; + padding: 1rem; +} + +.section { + padding-bottom: 2rem; + padding-top: 1rem; +} \ No newline at end of file diff --git a/.internal/wui/src/features/BuildSidebar/index.jsx b/.internal/wui/src/features/BuildSidebar/index.jsx new file mode 100644 index 000000000..31e857fcd --- /dev/null +++ b/.internal/wui/src/features/BuildSidebar/index.jsx @@ -0,0 +1,340 @@ +import React, { Fragment, useState, useEffect } from 'react'; +import { useDispatch, useSelector } from "react-redux"; +import Box from '@material-ui/core/Box'; +// import Tooltip from '@material-ui/core/Tooltip'; +import Button from '@material-ui/core/Button'; +// import ErrorOutlineOutlinedIcon from '@material-ui/icons/ErrorOutlineOutlined'; +import Divider from '@material-ui/core/Divider'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import Checkbox from '@material-ui/core/Checkbox'; +// import { makeStyles, useTheme } from '@material-ui/core/styles'; +import styles from './build-sidebar.module.css'; +import { + addTagToHideListAction, + removeTagFromHideListAction +} from '../../actions/updateFilterTags.action'; +import { + createAndBuildStackAction +} from '../../actions/buildStack.action'; +import { + getScriptFromTemplateAction +} from '../../actions/getScript.action'; +import { + downloadBuildFile +} from '../../actions/downloadBuild.action'; +import BuildCompletedModal from '../buildCompletedModal'; +import { API_STATUS } from '../../constants'; +import { getBuildOptions } from '../../utils/buildOptionSync'; + +// const useStyles = makeStyles({ +// serviceCard: { +// "&:hover": { +// borderColor: ({ theme }) => theme.palette.text.primary +// } +// } +// }); + +const getUniqueTagsFromTemplates = ({ serviceTemplateListPayload, metadataList }) => { + const tagList = []; + if (Array.isArray(serviceTemplateListPayload)) { + serviceTemplateListPayload.forEach((service) => { + if (Array.isArray(metadataList?.[service]?.serviceTypeTags ?? false)) { + metadataList[service].serviceTypeTags.forEach((tag) => { + if (!tagList.includes(tag)) { + tagList.push(tag); + } + }); + } + }); + } + tagList.sort(); + return tagList; +}; + +const buildIssueListItem = (name, issueType, issueText) => { + return ( + {name} [{issueType}] - {issueText} + ); +}; + +const buildIssuesRender = (issues) => { + const unknownError = !( + issues.payload + && issues.payload.issueList + && Array.isArray(issues.payload.issueList.services) + && Array.isArray(issues.payload.issueList.networks) + && Array.isArray(issues.payload.issueList.other) + ); + + const noIssues = ( + !unknownError + && issues.payload.issueList.services.length === 0 + && issues.payload.issueList.networks.length === 0 + && issues.payload.issueList.other.length === 0 + ); + + return ( + + Build Issues: + + {issues.status === API_STATUS.SUCCESS + && ( + + {Array.isArray(issues?.payload?.issueList?.services) + && (issues?.payload?.issueList?.services?.length ?? 0) > 0 + && ( + + + Services: + +
    + {issues.payload.issueList.services.map((issue) => { + return ( +
  • + {buildIssueListItem(issue.name, issue.issueType, issue.message)} +
  • + ) + })} +
+
+
+
+ )} + {Array.isArray(issues?.payload?.issueList?.networks) + && (issues?.payload?.issueList?.networks?.length ?? 0) > 0 + && ( + + + Networks: + +
    + {(issues?.payload?.issueList?.networks ?? []).map((issue) => { + return ( +
  • + {buildIssueListItem(issue.name, issue.issueType, issue.message)} +
  • + ) + })} +
+
+
+
+ )} + {Array.isArray(issues?.payload?.issueList?.other) + && (issues?.payload?.issueList?.other?.length ?? 0) > 0 + && ( + + + Other Issues: + +
    + {issues.payload.issueList.other.map((issue) => { + return ( +
  • + {buildIssueListItem(issue.name, issue.issueType, issue.message)} +
  • + ) + })} +
+
+
+
+ )} + {noIssues + && ( + + + No build issues + + + )} + {!noIssues + && ( + + + You can still attempt to build when issues are reported. + + + )} + {unknownError + && ( + + + An unknown error occured retrieving build issues + + + )} +
+ )} + {issues.status === API_STATUS.PENDING + && ( + Loading... + )} + {issues.status === API_STATUS.FAILURE + && ( + Failed to get build issues from API + )} + {issues.status === API_STATUS.UNINIT + && ( + No changes detected + )} +
+
+ ); +}; + +const buildList = (selectedServices) => { + return ( + + Building Services: + + {selectedServices.join(', ')} + + + ); +}; + +const buildServices = (dispatchBuildStack, buildStack) => { + return ( + + Build: + + + + {(buildStack?.status === API_STATUS.FAILURE ?? false) + && ( + + Error occured when building. Please check API logs. + {buildStack?.error?.message ?? 'Unknown error'} + + )} + + ); +}; + +const Sidebar = (props) => { + // const theme = useTheme(); + // const classes = useStyles({ props, theme }); + + const mapStateToProps = (selector) => { + return { + configServiceMetadata: selector(state => state.configServiceMetadata), + hideServiceTags: selector(state => state.hideServiceTags), + selectedServices: selector(state => state.selectedServices), + buildIssues: selector(state => state.buildIssues), + buildStack: selector(state => state.buildStack), + scriptTemplates: selector(state => state.scriptTemplates), + allServicesMetadataReducer: selector(state => state.allServicesMetadataReducer) + }; + }; + const mapDispatchToProps = (dispatch) => { + return { + dispatchAddTagToHideList: (tag) => dispatch(addTagToHideListAction(tag)), + dispatchRemoveTagFromHideList: (tag) => dispatch(removeTagFromHideListAction(tag)), + dispatchBuildStack: (selectedServices, configurations) => dispatch(createAndBuildStackAction(selectedServices, configurations)), + dispatchGetScriptTemplates: ({ scriptName, options, linkRef }) => dispatch(getScriptFromTemplateAction({ scriptName, options, linkRef })), + dispatchDownloadBuildFile: ({ build, type, linkRef }) => dispatch(downloadBuildFile({ build, type, linkRef })) + }; + }; + + props = { + ...props, + ...mapStateToProps(useSelector), + ...mapDispatchToProps(useDispatch()), + }; + + const [modalOpen, setModalOpen] = useState(false); + useEffect(() => { + if (props.buildStack.status === API_STATUS.SUCCESS) { + setModalOpen(true); + } + }, [ + props.buildStack + ]); + + const { + serviceTemplateList, + allServicesMetadataReducer, + selectedServices, + buildIssues, + hideServiceTags, + dispatchRemoveTagFromHideList, + dispatchAddTagToHideList, + dispatchBuildStack, + dispatchGetScriptTemplates, + dispatchDownloadBuildFile, + buildStack, + scriptTemplates + } = props; + + const downloadLinkRef = React.useRef(null); + + const handleBuildSelectChange = (evt, tagName) => { + if (evt.target.checked) { + return dispatchAddTagToHideList(tagName); + } + return dispatchRemoveTagFromHideList(tagName); + }; + + const serviceFilter = (serviceTemplateListPayload, servicesMetadata) => { + return ( + + Hide by tag: + + { + getUniqueTagsFromTemplates({ serviceTemplateListPayload, metadataList: servicesMetadata }).map((tag) => { + return ( + -1} + onChange={(evt) => handleBuildSelectChange(evt, tag)} + name="checkedB" + color="primary" + /> + } + label={tag} + /> + ); + }) + } + + + ); + }; + + return ( + + + setModalOpen(false)} + buildStack={buildStack} + scriptTemplates={scriptTemplates} + dispatchGetScriptTemplates={dispatchGetScriptTemplates} + dispatchDownloadBuildFile={dispatchDownloadBuildFile} + downloadLinkRef={downloadLinkRef} + /> + + {serviceFilter(serviceTemplateList.payload, allServicesMetadataReducer.payload)} + + {buildIssuesRender(buildIssues)} + + {buildList(selectedServices.selectedServices)} + + {buildServices(() => { + if (Array.isArray(selectedServices.selectedServices) && selectedServices.selectedServices.length > 0) { + dispatchBuildStack(selectedServices.selectedServices, getBuildOptions()); + } + }, buildStack)} + + + ); +}; + +export default Sidebar; diff --git a/.internal/wui/src/features/Sidebar/index.js b/.internal/wui/src/features/Sidebar/index.js new file mode 100644 index 000000000..00fd410d5 --- /dev/null +++ b/.internal/wui/src/features/Sidebar/index.js @@ -0,0 +1,165 @@ +import React, { Fragment } from 'react'; +import clsx from 'clsx'; +import { makeStyles } from '@material-ui/core/styles'; +import Drawer from '@material-ui/core/Drawer'; +import Box from '@material-ui/core/Box'; +import List from '@material-ui/core/List'; +import CssBaseline from '@material-ui/core/CssBaseline'; +import Divider from '@material-ui/core/Divider'; +import IconButton from '@material-ui/core/IconButton'; +import MenuIcon from '@material-ui/icons/Menu'; +import BuildIcon from '@material-ui/icons/Build'; +import QueryBuilderIcon from '@material-ui/icons/QueryBuilder'; +import ChevronLeftIcon from '@material-ui/icons/ChevronLeft'; +import AttachMoneyIcon from '@material-ui/icons/AttachMoney'; +import { Link } from "react-router-dom"; +import HelpOutlineIcon from '@material-ui/icons/HelpOutline'; +import ListItem from '@material-ui/core/ListItem'; +import ListItemIcon from '@material-ui/core/ListItemIcon'; +import ListItemText from '@material-ui/core/ListItemText'; + +const drawerWidth = 240; + +const useStyles = makeStyles((theme) => ({ + root: { + display: 'flex', + }, + appBar: { + zIndex: theme.zIndex.drawer + 1, + transition: theme.transitions.create(['width', 'margin'], { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen, + }), + }, + appBarShift: { + marginLeft: drawerWidth, + width: `calc(100% - ${drawerWidth}px)`, + transition: theme.transitions.create(['width', 'margin'], { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.enteringScreen, + }), + }, + menuButton: { + marginRight: 10, + }, + hide: { + display: 'none', + }, + drawer: { + width: drawerWidth, + flexShrink: 0, + whiteSpace: 'nowrap', + }, + drawerOpen: { + width: drawerWidth, + transition: theme.transitions.create('width', { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.enteringScreen, + }), + }, + drawerClose: { + transition: theme.transitions.create('width', { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen, + }), + overflowX: 'hidden', + width: theme.spacing(7) + 1, + [theme.breakpoints.up('sm')]: { + width: theme.spacing(9) + 1, + }, + }, + toolbar: { + display: 'flex', + alignItems: 'center', + justifyContent: 'flex-end', + padding: theme.spacing(0, 1) + }, + content: { + flexGrow: 1, + padding: theme.spacing(3), + }, +})); + +export default function MiniDrawer() { + const classes = useStyles(); + // const theme = useTheme(); + const [menuOpen, setOpen] = React.useState(false); + + const handleDrawerOpen = () => { + setOpen(true); + }; + + const handleDrawerClose = () => { + setOpen(false); + }; + + return ( +
+ + + + {menuOpen + && ( + + + + + + )} + {!menuOpen + && ( + + + + + + )} + + + + + + + + + + + + + + + + + + + + + + + + +
+ ); +} \ No newline at end of file diff --git a/.internal/wui/src/features/buildCompletedModal/index.jsx b/.internal/wui/src/features/buildCompletedModal/index.jsx new file mode 100644 index 000000000..ac5e82c67 --- /dev/null +++ b/.internal/wui/src/features/buildCompletedModal/index.jsx @@ -0,0 +1,166 @@ +import React, { Fragment, useState, useEffect } from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import { useDispatch, useSelector } from "react-redux"; +import Modal from '@material-ui/core/Modal'; +// import Box from '@material-ui/core/Box'; +// import Tooltip from '@material-ui/core/Tooltip'; +import Grid from '@material-ui/core/Grid'; +import Button from '@material-ui/core/Button'; +import ScriptViewerModal from '../scriptViewerModal'; + +import { getBuildFileAction } from '../../actions/getBuildFile.action'; + +const getModalStyle = () => { + const top = 15; + const left = 50; + + return { + top: `${top}%`, + left: `${left}%`, + transform: `translate(-50%, -${left}%)`, + }; +}; + +const useStyles = makeStyles((theme) => ({ + paper: { + position: 'absolute', + width: '50%', + backgroundColor: theme.palette.background.paper, + border: '2px solid #000', + boxShadow: theme.shadows[5], + padding: theme.spacing(2, 4, 3), + }, +})); + +const mapDispatchToProps = (dispatch) => { + return { + dispatchGetBuildFile: ({ build, type, label }) => dispatch(getBuildFileAction({ build, type, label })) + }; +}; + +const mapStateToProps = (selector) => { + return { + buildFiles: selector(state => state.buildFiles) + }; +}; + +const BuildCompletedModal = (props) => { + props = { + ...props, + ...mapDispatchToProps(useDispatch()), + ...mapStateToProps(useSelector) + }; + + const { + isOpen, + handleClose, + buildStack, + scriptTemplates, + dispatchGetScriptTemplates, + dispatchGetBuildFile, + dispatchDownloadBuildFile, + downloadLinkRef, + buildFiles + } = props; + + const [scriptViewerModalOpen, setScriptViewerModalOpen] = useState(false); + const [displayScript, setDisplayScript] = useState('Loading...'); + const [showBootstrapScript, setShowBootstrapScript] = useState(false); + + // const { build, files, issues } = buildStack?.payload ?? {}; + const { build } = buildStack?.payload ?? {}; + + useEffect(() => { + const downloadedScript = buildFiles?.files?.completed?.[build]?.payload; + if (downloadedScript) { + setDisplayScript(downloadedScript); + setScriptViewerModalOpen(true); + } + }, [buildFiles?.files?.completed?.[build]?.status]); + + const bootstrapBody = () => { + if ((typeof scriptTemplates?.scripts?.completed?.bootstrap?.payload ?? null) === 'string') { + return scriptTemplates.scripts.completed.bootstrap?.payload; + } + return ''; + }; + + const closeModal = (event) => { + setShowBootstrapScript(false); + if (typeof handleClose === 'function') { + handleClose(event); + } + } + + // const linkRef = React.createRef(); + const classes = useStyles(); + const [modalStyle] = React.useState(getModalStyle); + const body = ( +
+

Build {build} Complete

+

+ Choose from the following options: +

+ + + + + + + + + + + + + {showBootstrapScript + && ( + +
{bootstrapBody()}
+
+ )} +
+ +
+ ); + + return ( + + setScriptViewerModalOpen(false)} + displayScript={displayScript} + scriptTitle={`docker-compose.yml for build: '${build}'`} + showActionButton={false} + actionButtonText={'Load build'} + actionButtonFunction={() => {}} + /> + + {body} + + + ); +}; + +export default BuildCompletedModal; \ No newline at end of file diff --git a/.internal/wui/src/features/buildHistoryGridItem/build-history-grid-item.module.css b/.internal/wui/src/features/buildHistoryGridItem/build-history-grid-item.module.css new file mode 100644 index 000000000..4546ab93b --- /dev/null +++ b/.internal/wui/src/features/buildHistoryGridItem/build-history-grid-item.module.css @@ -0,0 +1,33 @@ +.docsLink { + background: linear-gradient(45deg, #2196F3 30%, #04add3 90%); + box-shadow: 0 3px 5px 2px rgba(33, 203, 243, .3); + color: 'white'; + padding: 0.5rem; +} + +.selectedForBuild { + background: linear-gradient(45deg, #76c01644 30%, #04d35a44 90%); +} + +.buildIssue { + background: linear-gradient(45deg, #ff7b008c 30%, #ffee00a8 90%); +} + +.serviceError { + background: linear-gradient(45deg, #c016167f 30%, #d304047f 90%); +} + +.serviceCard { + width: 24rem; + height: 24rem; + /* border-color: "primary.main"; */ +} + +.serviceIconContainer { + height: 6rem +} + +.serviceIcon { + height: 6rem; + width: auto; +} diff --git a/.internal/wui/src/features/buildHistoryGridItem/index.jsx b/.internal/wui/src/features/buildHistoryGridItem/index.jsx new file mode 100644 index 000000000..5eec2ed78 --- /dev/null +++ b/.internal/wui/src/features/buildHistoryGridItem/index.jsx @@ -0,0 +1,232 @@ +import React, { Fragment, useState, useEffect } from 'react'; +// import { useDispatch, useSelector } from "react-redux"; +import Box from '@material-ui/core/Box'; +// import Tooltip from '@material-ui/core/Tooltip'; +import Skeleton from '@material-ui/lab/Skeleton'; +// import FormControlLabel from '@material-ui/core/FormControlLabel'; +// import Checkbox from '@material-ui/core/Checkbox'; +// import Button from '@material-ui/core/Button'; +import Link from '@material-ui/core/Link'; +import Button from '@material-ui/core/Button'; +import { useDispatch, useSelector } from "react-redux"; +// import ErrorOutlineOutlinedIcon from '@material-ui/icons/ErrorOutlineOutlined'; +import { makeStyles } from '@material-ui/core/styles'; +import { useTheme } from '@material-ui/core/styles'; +import ScriptViewerModal from '../scriptViewerModal'; +import { getBuildFileAction } from '../../actions/getBuildFile.action'; +import { deleteBuildAction } from '../../actions/deleteBuild.action'; +import { + addSelectedService, + clearAllSelectedServicesAction +} from '../../actions/updateSelectedServices.action'; +import { setSelectedItems_services, setBuildOptions } from '../../utils/buildOptionSync'; +import styles from './build-history-grid-item.module.css'; + +const useStyles = makeStyles({ + serviceCard: { + "&:hover": { + borderColor: ({ theme }) => theme.palette.text.primary + } + } +}); + +const mapDispatchToProps = (dispatch) => { + return { + dispatchGetBuildFile: ({ build, type, label }) => dispatch(getBuildFileAction({ build, type, label })), + dispatchAddSelectedService: (serviceName) => dispatch(addSelectedService(serviceName)), + dispatchClearAllSelectedServices: () => dispatch(clearAllSelectedServicesAction()), + dispatchDeleteBuild: ({ build }) => dispatch(deleteBuildAction({ build })) + }; +}; + +const mapStateToProps = (selector) => { + return { + buildFiles: selector(state => state.buildFiles) + }; +}; + +const ServiceItem = (props) => { + const theme = useTheme(); + const classes = useStyles({ props, theme }); + + props = { + ...props, + ...mapDispatchToProps(useDispatch()), + ...mapStateToProps(useSelector) + }; + const { + dispatchAddSelectedService, + dispatchClearAllSelectedServices, + dispatchGetBuildFile, + dispatchDeleteBuild, + buildTime, + dispatchDownloadBuildFile, + downloadLinkRef, + buildFiles + } = props; + + const [scriptViewerModalOpen, setScriptViewerModalOpen] = useState(false); + const [loadableScriptOptions, setLoadableScriptOptions] = useState(false); + const [displayScript, setDisplayScript] = useState('Loading...'); + + useEffect(() => { + const downloadedScript = buildFiles?.files?.completed?.[buildTime]?.payload; + if (downloadedScript) { + setDisplayScript(downloadedScript); + setScriptViewerModalOpen(true); + } + }, [buildFiles?.files?.completed?.[buildTime]?.status]); + + let isLoading = false; + let buildHistoryError = { + hasError: false + }; + + const loadBuild = (modalProps) => { + const buildConfig = JSON.parse(modalProps.displayScript); + + setSelectedItems_services(buildConfig.selectedServices); + setBuildOptions(buildConfig.configurations); + dispatchClearAllSelectedServices(); + buildConfig?.selectedServices?.map((service) => { + dispatchAddSelectedService(service); + }); + + setScriptViewerModalOpen(false); + }; + + const buildHistoryComponent = () => { + return ( + + {buildTime} + + { + return dispatchDownloadBuildFile({ build: buildTime, type: 'zip', linkRef: downloadLinkRef }); + }} + rel="noopener" + target="_blank" + className={styles.docsLink} + color="inherit" + > + Download Zip + + + + + + + + + + + + + ) + }; + + const errorComponent = () => { + return ( + +
Error loading: {buildTime}
+
Try refreshing, and ensuring the API server is running correctly.
+
+ ) + }; + + const loadingComponent = () => { + return ( + + Loading '{buildTime}' details... + + + + + + + + + + + + + + ) + }; + + return ( + + setScriptViewerModalOpen(false)} + displayScript={displayScript} + scriptTitle={`docker-compose.yml for build: '${buildTime}'`} + showActionButton={loadableScriptOptions} + actionButtonText={'Load build'} + actionButtonFunction={loadBuild} + /> + + {isLoading + && (loadingComponent())} + {!isLoading + && buildTime + && ( + buildHistoryComponent() + )} + {!isLoading + && buildHistoryError.hasError === true + && ( + errorComponent() + )} + + + ); +}; + +export default ServiceItem; diff --git a/.internal/wui/src/features/counter/Counter.js b/.internal/wui/src/features/counter/Counter.js new file mode 100644 index 000000000..7bdeb324d --- /dev/null +++ b/.internal/wui/src/features/counter/Counter.js @@ -0,0 +1,60 @@ +import React, { useState } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { + decrement, + increment, + incrementByAmount, + incrementAsync, + selectCount, +} from '../../reducers/counter'; +import styles from './Counter.module.css'; + +export function Counter() { + const count = useSelector(selectCount); + const dispatch = useDispatch(); + const [incrementAmount, setIncrementAmount] = useState('2'); + + return ( +
+
+ + {count} + +
+
+ setIncrementAmount(e.target.value)} + /> + + +
+
+ ); +} diff --git a/.internal/wui/src/features/counter/Counter.module.css b/.internal/wui/src/features/counter/Counter.module.css new file mode 100644 index 000000000..6c2b58f48 --- /dev/null +++ b/.internal/wui/src/features/counter/Counter.module.css @@ -0,0 +1,74 @@ +.row { + display: flex; + align-items: center; + justify-content: center; +} + +.row:not(:last-child) { + margin-bottom: 16px; +} + +.value { + font-size: 78px; + padding-left: 16px; + padding-right: 16px; + margin-top: 2px; + font-family: 'Courier New', Courier, monospace; +} + +.button { + appearance: none; + background: none; + font-size: 32px; + padding-left: 12px; + padding-right: 12px; + outline: none; + border: 2px solid transparent; + color: rgb(112, 76, 182); + padding-bottom: 4px; + cursor: pointer; + background-color: rgba(112, 76, 182, 0.1); + border-radius: 2px; + transition: all 0.15s; +} + +.textbox { + font-size: 32px; + padding: 2px; + width: 64px; + text-align: center; + margin-right: 8px; +} + +.button:hover, .button:focus { + border: 2px solid rgba(112, 76, 182, 0.4); +} + +.button:active { + background-color: rgba(112, 76, 182, 0.2); +} + +.asyncButton { + composes: button; + position: relative; + margin-left: 8px; +} + +.asyncButton:after { + content: ""; + background-color: rgba(112, 76, 182, 0.15); + display: block; + position: absolute; + width: 100%; + height: 100%; + left: 0; + top: 0; + opacity: 0; + transition: width 1s linear, opacity 0.5s ease 1s; +} + +.asyncButton:active:after { + width: 0%; + opacity: 1; + transition: 0s +} diff --git a/.internal/wui/src/features/helpAndDocsModal/help-and-docs.module.css b/.internal/wui/src/features/helpAndDocsModal/help-and-docs.module.css new file mode 100644 index 000000000..8bb4365f4 --- /dev/null +++ b/.internal/wui/src/features/helpAndDocsModal/help-and-docs.module.css @@ -0,0 +1,10 @@ +.docsLink { + background: linear-gradient(45deg, #242424 30%, #282828 90%); + box-shadow: 0 4px 6px 3px rgba(0, 0, 0, 0.3); + color: 'white'; +} +.docsInner { + background: linear-gradient(45deg, #242424 30%, #282828 90%); + box-shadow: 0 4px 6px 3px rgba(0, 0, 0, 0.3); + padding: 1rem; +} diff --git a/.internal/wui/src/features/helpAndDocsModal/index.jsx b/.internal/wui/src/features/helpAndDocsModal/index.jsx new file mode 100644 index 000000000..18e9f5e48 --- /dev/null +++ b/.internal/wui/src/features/helpAndDocsModal/index.jsx @@ -0,0 +1,106 @@ +import React, { Fragment, useState, useEffect } from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import { useDispatch, useSelector } from "react-redux"; +import Modal from '@material-ui/core/Modal'; +import Box from '@material-ui/core/Box'; +import Grid from '@material-ui/core/Grid'; +import Button from '@material-ui/core/Button'; +import Link from '@material-ui/core/Link'; + +import { byName } from '../../utils/interpolate'; + +import styles from './help-and-docs.module.css'; + +const getModalStyle = () => { + const top = 35; + const left = 50; + + return { + top: `${top}%`, + left: `${left}%`, + transform: `translate(-50%, -${left}%)`, + }; +}; + +const useStyles = makeStyles((theme) => ({ + paper: { + position: 'absolute', + width: '50%', + backgroundColor: theme.palette.background.paper, + border: '2px solid #000', + boxShadow: theme.shadows[5], + padding: theme.spacing(2, 4, 3), + }, +})); + +const HelpAndDocsModal = (props) => { + const { + isOpen, + handleClose, + serviceDocs, + serviceName, + displayName + } = props; + + const closeModal = (event) => { + if (typeof handleClose === 'function') { + handleClose(event); + } + } + + const classes = useStyles(); + const [modalStyle] = React.useState(getModalStyle); + const body = ( +
+

{displayName} - ({serviceName}) Help and Docs

+ + {serviceDocs + && typeof serviceDocs === 'object' + && Object.keys(serviceDocs?.links ?? []).map((key, i) => { + if (serviceDocs?.links[key]) { + return ( + + + + {byName(key, { displayName, serviceName })} + + + + ) + } + })} + + + + + + + + + +
+ ); + + return ( + + + {body} + + + ); +}; + +export default HelpAndDocsModal; \ No newline at end of file diff --git a/.internal/wui/src/features/scriptViewerModal/index.jsx b/.internal/wui/src/features/scriptViewerModal/index.jsx new file mode 100644 index 000000000..ea249e6e5 --- /dev/null +++ b/.internal/wui/src/features/scriptViewerModal/index.jsx @@ -0,0 +1,123 @@ +import React, { Fragment, useState, useEffect } from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import Modal from '@material-ui/core/Modal'; +import Box from '@material-ui/core/Box'; +import TextField from '@material-ui/core/TextField'; +import Grid from '@material-ui/core/Grid'; +// import Box from '@material-ui/core/Box'; +// import Tooltip from '@material-ui/core/Tooltip'; +import Button from '@material-ui/core/Button'; + +const getModalStyle = () => { + const top = 15; + const left = 50; + + return { + top: `${top}%`, + left: `${left}%`, + maxHeight: '75%', + overflow: 'hidden', + overflowY: 'scroll', + transform: `translate(-50%, 0%)`, + }; +}; + +const useStyles = makeStyles((theme) => ({ + paper: { + position: 'absolute', + width: '70%', + backgroundColor: theme.palette.background.paper, + border: '2px solid #000', + boxShadow: theme.shadows[5], + padding: theme.spacing(2, 4, 3), + }, +})); + +const ScriptViewerModal = (props) => { + const { + isOpen, + handleClose, + displayScript, + scriptTitle, + readOnly, + showActionButton, + actionButtonText, + actionButtonFunction + } = props; + const [renderedScript, setRenderedScript] = useState('Failed to load'); + + useEffect(() => { + setRenderedScript(displayScript); + }, [displayScript]); + + const closeModal = (event) => { + if (typeof handleClose === 'function') { + handleClose(event); + } + } + + const classes = useStyles(); + const [modalStyle] = useState(getModalStyle); + const body = ( +
+

{scriptTitle ?? 'Viewing Script'}

+ + + + + + + + + + + { + showActionButton + && actionButtonText + && typeof(actionButtonFunction) === 'function' + && ( + + + + ) + } + + +
+ ); + + return ( + + {body} + + ); +}; + +export default ScriptViewerModal; \ No newline at end of file diff --git a/.internal/wui/src/features/serviceConfigModal/index.jsx b/.internal/wui/src/features/serviceConfigModal/index.jsx new file mode 100644 index 000000000..03a5996ef --- /dev/null +++ b/.internal/wui/src/features/serviceConfigModal/index.jsx @@ -0,0 +1,138 @@ +import React, { Fragment, useState, useEffect } from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import Modal from '@material-ui/core/Modal'; +import Grid from '@material-ui/core/Grid'; +import Button from "@material-ui/core/Button"; +import Box from '@material-ui/core/Box'; +import getConfigComponents from '../../utils/configOptionLoader'; +import { + deleteTemporaryBuildOptions +} from '../../utils/buildOptionSync'; + +const getModalStyle = () => { + const top = 10; + const left = 50; + + return { + top: `${top}%`, + left: `${left}%`, + maxHeight: '75%', + overflow: 'hidden', + overflowY: 'scroll', + transform: `translate(-50%, 0%)`, + }; +}; + +const useStyles = makeStyles((theme) => ({ + paper: { + position: 'absolute', + width: '50%', + backgroundColor: theme.palette.background.paper, + border: '2px solid #000', + boxShadow: theme.shadows[5], + padding: theme.spacing(2, 4, 3), + }, +})); + +const ServiceConfigModal = (props) => { + const { + isOpen, + handleClose, + serviceName, + networkTemplateList, + serviceMetadata, + serviceConfigOptions, + buildOptions, + serviceTemplates, + setBuildOptions, + getBuildOptions, + buildOptionsInit, + setServiceOptions, + setTemporaryBuildOptions, + getTemporaryBuildOptions, + setTemporaryServiceOptions, + setupTemporaryBuildOptions, + saveTemporaryBuildOptions + } = props; + + const closeModal = (event) => { + deleteTemporaryBuildOptions(); + if (typeof handleClose === 'function') { + handleClose(event); + } + } + + const resetDefaults = (evt) => { + const currentBuildOptions = getBuildOptions(); + delete currentBuildOptions?.services[serviceName]; + setBuildOptions(currentBuildOptions); + closeModal(evt); + }; + + const classes = useStyles(); + const [modalStyle] = React.useState(getModalStyle); + const body = ( +
+

{serviceMetadata ? serviceMetadata.displayName : ''} ({serviceName}) Configuration

+ + {getConfigComponents(serviceConfigOptions ?? []).map((ConfigComponent, index) => { + return ( + + + + ); + })} + + + + + + + + + {/* + + */} + + + + +
+ ); + + return ( + + {body} + + ); +}; + +export default ServiceConfigModal; \ No newline at end of file diff --git a/.internal/wui/src/features/serviceUiControls/custom/deconz-devices.js b/.internal/wui/src/features/serviceUiControls/custom/deconz-devices.js new file mode 100644 index 000000000..6654649f3 --- /dev/null +++ b/.internal/wui/src/features/serviceUiControls/custom/deconz-devices.js @@ -0,0 +1,104 @@ +import React, { Fragment, useState, useEffect } from 'react'; +// import Box from '@material-ui/core/Box'; +import Grid from '@material-ui/core/Grid'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import Checkbox from '@material-ui/core/Checkbox'; +import Box from '@material-ui/core/Box'; +import MenuItem from '@material-ui/core/MenuItem'; +import Select from '@material-ui/core/Select'; + +const deconzDeviceList = [ // TODO: look into moving this to the API. + { + key: '/dev/ttyUSB0', + value: '/dev/ttyUSB0' + }, + { + key: '/dev/ttyACM0', + value: '/dev/ttyACM0' + }, + { + key: '/dev/ttyAMA0', + value: '/dev/ttyAMA0' + }, + { + key: '/dev/ttyS0', + value: '/dev/ttyS0' + }, + { + key: 'None', + value: 'none' + } +]; + +const DeconzDevices = (props) => { + const { + // serviceConfigOptions, + serviceName, + // setBuildOptions, + getBuildOptions, + // buildOptionsInit, + // setServiceOptions, + networkTemplateList, + // setTemporaryBuildOptions, + getTemporaryBuildOptions, + setTemporaryServiceOptions, + // setupTemporaryBuildOptions, + // saveTemporaryBuildOptions, + serviceTemplates, + onChange + } = props; + + // const tempBuildOptions = getTemporaryBuildOptions(); + const [selectedDevice, setSelectedDevice] = useState('none'); + + useEffect(() => { + const savedSelectedDevice = getBuildOptions()?.services?.[serviceName]?.selectedDevice ?? 'none'; + setSelectedDevice(savedSelectedDevice); + }, []); + + useEffect(() => { + setTemporaryServiceOptions(serviceName, { + ...getTemporaryBuildOptions()?.services?.[serviceName] ?? {}, + selectedDevice: selectedDevice + }); + }, [ + selectedDevice + ]); + + const onChangeCb = (event) => { + const newDevice = event.target.value; + setSelectedDevice(newDevice); + if (typeof(onChange) === 'function') { + onChange(event); + } + }; + + return ( + + + Deconz Hardware: + + + + + Select Device: + + + + + + ); +}; + +export default DeconzDevices; diff --git a/.internal/wui/src/features/serviceUiControls/custom/nodered-npm.js b/.internal/wui/src/features/serviceUiControls/custom/nodered-npm.js new file mode 100644 index 000000000..d5fbcf690 --- /dev/null +++ b/.internal/wui/src/features/serviceUiControls/custom/nodered-npm.js @@ -0,0 +1,171 @@ +import React, { Fragment, useState, useEffect } from 'react'; +// import Box from '@material-ui/core/Box'; +import Grid from '@material-ui/core/Grid'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import Checkbox from '@material-ui/core/Checkbox'; +import Box from '@material-ui/core/Box'; +import Divider from '@material-ui/core/Divider'; +import Button from '@material-ui/core/Button'; +import Link from '@material-ui/core/Link'; +import MenuItem from '@material-ui/core/MenuItem'; +import Select from '@material-ui/core/Select'; + +const NodeRedNpm = (props) => { + const { + serviceConfigOptions, + serviceName, + // setBuildOptions, + getBuildOptions, + // buildOptionsInit, + // setServiceOptions, + networkTemplateList, + // setTemporaryBuildOptions, + getTemporaryBuildOptions, + setTemporaryServiceOptions, + // setupTemporaryBuildOptions, + // saveTemporaryBuildOptions, + serviceTemplates, + onChange + } = props; + + const [selectedModules, setSelectedModules] = useState([]); + const [showNonEssential, setShowNonEssential] = useState(false); + const addons = serviceConfigOptions?.nodered_npmSelection ?? {}; + + useEffect(() => { + const savedSelectedModules = getBuildOptions()?.services?.[serviceName]?.addonsList ?? false; + const defaultSelectedModules = addons?.defaultOn ?? []; + const useSelectedModules = Array.isArray(savedSelectedModules) ? savedSelectedModules : defaultSelectedModules; + setSelectedModules(useSelectedModules); + }, []); + + useEffect(() => { + setTemporaryServiceOptions(serviceName, { + ...getTemporaryBuildOptions()?.services?.[serviceName] ?? {}, + addonsList: selectedModules + }); + }, [ + selectedModules + ]); + + const onChangeCb = (event, moduleName) => { + const indexOfModule = selectedModules.indexOf(moduleName); + const newModulesList = [...selectedModules]; + + if (indexOfModule > -1) { + newModulesList.splice(indexOfModule, 1); + } else { + newModulesList.push(moduleName); + } + + setSelectedModules(newModulesList); + if (typeof(onChange) === 'function') { + onChange(event, moduleName); + } + }; + + const sortAddons = ({ defaultOn, defaultOff, essentials }) => { + const combinedList = [ + ...defaultOn, + ...defaultOff + ]; + combinedList.sort(); + + const essentialsList = []; + const nonEssentialList = []; + + combinedList.forEach((moduleName) => { + if (essentials.includes(moduleName)) { + essentialsList.push(moduleName); + } else { + nonEssentialList.push(moduleName); + } + }); + return { combinedList, essentialsList, nonEssentialList }; + }; + + return ( + + + Initial NodeRed Plugins: + + + Note: After starting NodeRed, you must make any modules changes with NodeRed's Pallete Menu. + + + + + {sortAddons({ ...addons }).essentialsList.map((npmName) => { + return ( + + + onChangeCb(evt, npmName) } + name={npmName} + color="primary" + /> + } + label={npmName} + /> + + + ) + })} + + {showNonEssential + && ( + + + + + + {sortAddons({ ...addons }).nonEssentialList.map((npmName) => { + return ( + + + onChangeCb(evt, npmName) } + name={npmName} + color="primary" + /> + } + label={npmName} + /> + + + ) + })} + + + )} + + + + + + ); +}; + +export default NodeRedNpm; diff --git a/.internal/wui/src/features/serviceUiControls/general/devicesConfig.jsx b/.internal/wui/src/features/serviceUiControls/general/devicesConfig.jsx new file mode 100644 index 000000000..7fad0f3d8 --- /dev/null +++ b/.internal/wui/src/features/serviceUiControls/general/devicesConfig.jsx @@ -0,0 +1,99 @@ +import React, { Fragment, useState, useEffect } from 'react'; +// import Box from '@material-ui/core/Box'; +import Grid from '@material-ui/core/Grid'; +import TextField from '@material-ui/core/TextField'; + +const DevicessConfig = (props) => { + + const { + // serviceConfigOptions, + serviceName, + // setBuildOptions, + getBuildOptions, + // buildOptionsInit, + // setServiceOptions, + // setTemporaryBuildOptions, + getTemporaryBuildOptions, + setTemporaryServiceOptions, + // setupTemporaryBuildOptions, + // saveTemporaryBuildOptions, + serviceTemplates, + onChange + } = props; + + const tempBuildOptions = getTemporaryBuildOptions(); + const yamlDevicesSettings = serviceTemplates?.[serviceName]?.devices || []; + + const [devicesSettings, setDevicesSettings] = useState(yamlDevicesSettings); + const [loaded, setLoaded] = useState(false); + useEffect(() => { + if ((getBuildOptions().services?.[serviceName]?.devices?.length ?? 0) < 1 ) { + setTemporaryServiceOptions(serviceName, { + ...getBuildOptions().services?.[serviceName] ?? {}, + devices: yamlDevicesSettings + }); + } else { + setDevicesSettings(getBuildOptions().services?.[serviceName]?.devices); + } + setLoaded(true); + }, []); + + useEffect(() => { + if (loaded) { + setTemporaryServiceOptions(serviceName, { + ...tempBuildOptions?.services?.[serviceName] ?? {}, + devices: devicesSettings + }); + } + }, [ + devicesSettings + ]); + + const onChangeCb = (oldDevice, event) => { + const newDevice = event.target.value; + const temporaryDevices = [...devicesSettings]; + + const devicesIndex = temporaryDevices.findIndex((device) => { + return oldDevice === device; + }); + + if (devicesIndex > -1) { + temporaryDevices[devicesIndex] = newDevice; + } + + setDevicesSettings(temporaryDevices); + if (typeof(onChange) === 'function') { + onChange(oldDevice, newDevice); + } + }; + + return ( + + + {devicesSettings.map((device, index) => { + + return ( + + { onChangeCb(device, event) }} + value={device} + style={{ width: '100%' }} + /> + + ); + })} + + + ); +}; + +export default DevicessConfig; diff --git a/.internal/wui/src/features/serviceUiControls/general/environmentConfig.jsx b/.internal/wui/src/features/serviceUiControls/general/environmentConfig.jsx new file mode 100644 index 000000000..e904fe225 --- /dev/null +++ b/.internal/wui/src/features/serviceUiControls/general/environmentConfig.jsx @@ -0,0 +1,121 @@ +import React, { Fragment, useState, useEffect } from 'react'; +// import Box from '@material-ui/core/Box'; +import Grid from '@material-ui/core/Grid'; +import TextField from '@material-ui/core/TextField'; +import { + getEnvironmentKey, + getEnvironmentValue +} from '../../../utils/parsers'; + +const VolumesConfig = (props) => { + + const { + serviceConfigOptions, + serviceName, + // setBuildOptions, + getBuildOptions, + // buildOptionsInit, + // setServiceOptions, + // setTemporaryBuildOptions, + getTemporaryBuildOptions, + setTemporaryServiceOptions, + // setupTemporaryBuildOptions, + // saveTemporaryBuildOptions, + serviceTemplates, + onChange + } = props; + + const tempBuildOptions = getTemporaryBuildOptions(); + const yamlEnvironmentSettings = serviceTemplates?.[serviceName]?.environment || []; + + const [environmentSettings, setEnvironmentSettings] = useState(yamlEnvironmentSettings); + const [loaded, setLoaded] = useState(false); + useEffect(() => { + // Check if there's a default value set in configs, and use it if so. + const envBuildDefaultOptions = serviceConfigOptions?.modifyableEnvironment ?? []; + const defaultEnvSettings = yamlEnvironmentSettings.map((envKV) => { + const envKey = getEnvironmentKey(envKV); + const defaultKV = envBuildDefaultOptions.find((modObj) => { + return modObj.key === envKey; + }); + + if (defaultKV) { + return `${envKey}=${defaultKV.value}`; + } + return envKV; + }); + + // Load options from persistant state + if ((getBuildOptions().services?.[serviceName]?.environment?.length ?? 0) < 1 ) { + setTemporaryServiceOptions(serviceName, { + ...getBuildOptions().services?.[serviceName] ?? {}, + environment: defaultEnvSettings + }); + setEnvironmentSettings(defaultEnvSettings); + } else { + setEnvironmentSettings(getBuildOptions().services?.[serviceName]?.environment); + } + setLoaded(true); + }, []); + + useEffect(() => { + if (loaded) { + setTemporaryServiceOptions(serviceName, { + ...tempBuildOptions?.services?.[serviceName] ?? {}, + environment: environmentSettings + }); + } + }, [ + environmentSettings + ]); + + const onChangeCb = (environmentKey, event) => { + const newEnvironmentValue = event.target.value; + const temporaryEnvironment = [...environmentSettings]; + + const environmentIndex = temporaryEnvironment.findIndex((index) => { + return getEnvironmentKey(index) === environmentKey; + }); + + if (environmentIndex > -1) { + temporaryEnvironment[environmentIndex] = `${environmentKey}=${newEnvironmentValue}` + } + + setEnvironmentSettings(temporaryEnvironment); + if (typeof(onChange) === 'function') { + onChange(environmentKey, newEnvironmentValue); + } + }; + + return ( + + + {Array.isArray(environmentSettings) && environmentSettings.map((environmentKeyValue) => { + const environmentKey = getEnvironmentKey(environmentKeyValue); + const environmentValue = getEnvironmentValue(environmentKeyValue); + + return ( + + { onChangeCb(environmentKey, event) }} + value={environmentValue} + style={{ width: '100%' }} + /> + + ); + })} + + + ); +}; + +export default VolumesConfig; diff --git a/.internal/wui/src/features/serviceUiControls/general/logging.jsx b/.internal/wui/src/features/serviceUiControls/general/logging.jsx new file mode 100644 index 000000000..bb142f640 --- /dev/null +++ b/.internal/wui/src/features/serviceUiControls/general/logging.jsx @@ -0,0 +1,64 @@ +import React, { Fragment, useState, useEffect } from 'react'; +// import Box from '@material-ui/core/Box'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import Checkbox from '@material-ui/core/Checkbox'; +import Box from '@material-ui/core/Box'; + +const PortConfig = (props) => { + + const { + // serviceConfigOptions, + serviceName, + // setBuildOptions, + getBuildOptions, + // buildOptionsInit, + // setServiceOptions, + // setTemporaryBuildOptions, + getTemporaryBuildOptions, + setTemporaryServiceOptions, + // setupTemporaryBuildOptions, + // saveTemporaryBuildOptions, + // serviceTemplates, + onChange + } = props; + + // const tempBuildOptions = getTemporaryBuildOptions(); + + const [loggingEnabled, setLoggingEnabled] = useState(getBuildOptions()?.services?.[serviceName]?.loggingEnabled ?? true); + useEffect(() => { + setTemporaryServiceOptions(serviceName, { + ...getTemporaryBuildOptions()?.services?.[serviceName] ?? {}, + loggingEnabled + }); + }, [ + loggingEnabled + ]); + + const onChangeCb = (event) => { + const newSetting = event.target.checked; + setLoggingEnabled(newSetting); + if (typeof(onChange) === 'function') { + onChange(newSetting); + } + }; + + return ( + + + onChangeCb(evt) } + name={"logging"} + color="primary" + /> + } + label={`Enable Logging for ${serviceName}`} + /> + + + ); +}; + +export default PortConfig; diff --git a/.internal/wui/src/features/serviceUiControls/general/networkConfig.jsx b/.internal/wui/src/features/serviceUiControls/general/networkConfig.jsx new file mode 100644 index 000000000..1b7a11b63 --- /dev/null +++ b/.internal/wui/src/features/serviceUiControls/general/networkConfig.jsx @@ -0,0 +1,148 @@ +import React, { Fragment, useState, useEffect } from 'react'; +// import Box from '@material-ui/core/Box'; +import Grid from '@material-ui/core/Grid'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import Checkbox from '@material-ui/core/Checkbox'; +import Box from '@material-ui/core/Box'; +import MenuItem from '@material-ui/core/MenuItem'; +import Select from '@material-ui/core/Select'; + +const NetworkConfig = (props) => { + const { + // serviceConfigOptions, + serviceName, + // setBuildOptions, + getBuildOptions, + // buildOptionsInit, + // setServiceOptions, + networkTemplateList, + // setTemporaryBuildOptions, + getTemporaryBuildOptions, + setTemporaryServiceOptions, + // setupTemporaryBuildOptions, + // saveTemporaryBuildOptions, + serviceTemplates, + onChange + } = props; + + // const tempBuildOptions = getTemporaryBuildOptions(); + const [networkMode, setNetworkMode] = useState(getBuildOptions()?.services?.[serviceName]?.networkMode ?? 'unchanged'); + + const [modifiedNetworkList, setModifiedNetworkList] = useState({}); + useEffect(() => { + const defaultOnNetworks = { ...getBuildOptions()?.services?.[serviceName]?.networks ?? {} }; + serviceTemplates[serviceName]?.networks?.forEach((networkName) => { + defaultOnNetworks[networkName] = true; + }); + setModifiedNetworkList({ + ...defaultOnNetworks + }); + + const toNetworkMode = getBuildOptions()?.services?.[serviceName]?.networkMode ?? 'unchanged'; + setNetworkMode(toNetworkMode); + }, []); + + useEffect(() => { + setTemporaryServiceOptions(serviceName, { + ...getTemporaryBuildOptions()?.services?.[serviceName] ?? {}, + networks: modifiedNetworkList + }); + }, [ + modifiedNetworkList + ]); + + useEffect(() => { + setTemporaryServiceOptions(serviceName, { + ...getTemporaryBuildOptions()?.services?.[serviceName] ?? {}, + networkMode: networkMode + }); + }, [ + networkMode + ]); + + const onChangeCb = (event, changeType, networkName) => { + if (changeType === 'mode') { + const newMode = event.target.value; + setNetworkMode(newMode); + if (typeof(onChange) === 'function') { + onChange(event, changeType); + } + } else if (changeType === 'network') { + const networkSelected = event.target.checked; + setModifiedNetworkList({ + ...modifiedNetworkList, + [networkName]: networkSelected + }); + if (typeof(onChange) === 'function') { + onChange(networkName, networkSelected, changeType); + } + } else { + + if (typeof(onChange) === 'function') { + onChange(event, changeType, networkName); + } + } + }; + + const defaultValue = (networkName) => { + return (serviceTemplates[serviceName]?.networks ?? []).includes(networkName); + }; + + return ( + + + + IOTstack Networks: + + + + Mode: + + + + + + {(networkTemplateList?.payload ?? []).map((networkName) => { + return ( + + onChangeCb(evt, 'network', networkName) } + name={networkName} + color="primary" + /> + } + label={networkName} + /> + + ); + }).filter((ele) => { + return ele !== null; + })} + + + ); +}; + +export default NetworkConfig; diff --git a/.internal/wui/src/features/serviceUiControls/general/portConfig.jsx b/.internal/wui/src/features/serviceUiControls/general/portConfig.jsx new file mode 100644 index 000000000..c79509e27 --- /dev/null +++ b/.internal/wui/src/features/serviceUiControls/general/portConfig.jsx @@ -0,0 +1,91 @@ +import React, { Fragment, useState, useEffect } from 'react'; +// import Box from '@material-ui/core/Box'; +import Grid from '@material-ui/core/Grid'; +import TextField from '@material-ui/core/TextField'; +import { + getExternalPort, + replaceExternalPort, + getInternalPort +} from '../../../utils/parsers'; + +const PortConfig = (props) => { + + const { + serviceConfigOptions, + serviceName, + // setBuildOptions, + getBuildOptions, + // buildOptionsInit, + // setServiceOptions, + // setTemporaryBuildOptions, + getTemporaryBuildOptions, + setTemporaryServiceOptions, + // setupTemporaryBuildOptions, + // saveTemporaryBuildOptions, + serviceTemplates, + onChange + } = props; + + const tempBuildOptions = getTemporaryBuildOptions(); + + const [portSettings, setPortSettings] = useState(getBuildOptions()?.services?.[serviceName]?.ports || {}); + useEffect(() => { + setTemporaryServiceOptions(serviceName, { + ...getTemporaryBuildOptions()?.services?.[serviceName] ?? {}, + ports: portSettings + }); + }, [ + portSettings + ]); + + const onChangeCb = (portKey, portLabelValue, event) => { + const newPort = event.target.value; + // const defaultTemplatePort = defaultValue(portKey, portKey); + setPortSettings({ + ...portSettings, + [portKey]: replaceExternalPort((portSettings[portKey] || portKey), newPort) + }); + if (typeof(onChange) === 'function') { + onChange(portKey, portLabelValue, newPort); + } + }; + + const defaultValue = (portValueKey, defaultValue) => { + const servicePorts = tempBuildOptions?.services?.[serviceName] ?? {}; + return servicePorts?.ports?.[portValueKey] ?? defaultValue; + }; + + return ( + + + {serviceConfigOptions && Object.keys(serviceConfigOptions.labeledPorts).map((portValueKey) => { + const currentPortSetting = portSettings[portValueKey] || defaultValue(portValueKey, portValueKey); + if ((serviceTemplates[serviceName]?.ports?.[portValueKey] ?? []).indexOf(portValueKey)) { // Only show ports that exist in the YAML template + return ( + + { onChangeCb(portValueKey, serviceConfigOptions.labeledPorts[portValueKey], event) }} + value={getExternalPort(currentPortSetting)} + /> + + ); + } + + return null; + }).filter((ele) => { + return ele !== null; + })} + + + ); +}; + +export default PortConfig; diff --git a/.internal/wui/src/features/serviceUiControls/general/volumesConfig.jsx b/.internal/wui/src/features/serviceUiControls/general/volumesConfig.jsx new file mode 100644 index 000000000..4aa8515c6 --- /dev/null +++ b/.internal/wui/src/features/serviceUiControls/general/volumesConfig.jsx @@ -0,0 +1,106 @@ +import React, { Fragment, useState, useEffect } from 'react'; +// import Box from '@material-ui/core/Box'; +import Grid from '@material-ui/core/Grid'; +import TextField from '@material-ui/core/TextField'; +import { + getExternalVolume, + getInternalVolume, + replaceExternalVolume +} from '../../../utils/parsers'; + +const VolumesConfig = (props) => { + + const { + // serviceConfigOptions, + serviceName, + // setBuildOptions, + getBuildOptions, + // buildOptionsInit, + // setServiceOptions, + // setTemporaryBuildOptions, + getTemporaryBuildOptions, + setTemporaryServiceOptions, + // setupTemporaryBuildOptions, + // saveTemporaryBuildOptions, + serviceTemplates, + onChange + } = props; + + const tempBuildOptions = getTemporaryBuildOptions(); + const yamlVolumeSettings = serviceTemplates?.[serviceName]?.volumes || []; + + const [volumeSettings, setVolumeSettings] = useState(yamlVolumeSettings); + const [loaded, setLoaded] = useState(false); + useEffect(() => { + if ((getBuildOptions().services?.[serviceName]?.volumes?.length ?? 0) < 1 ) { + setTemporaryServiceOptions(serviceName, { + ...getBuildOptions().services?.[serviceName] ?? {}, + volumes: yamlVolumeSettings + }); + } else { + setVolumeSettings(getBuildOptions().services?.[serviceName]?.volumes); + } + setLoaded(true); + }, []); + + useEffect(() => { + if (loaded) { + setTemporaryServiceOptions(serviceName, { + ...tempBuildOptions?.services?.[serviceName] ?? {}, + volumes: volumeSettings + }); + } + }, [ + volumeSettings + ]); + + const onChangeCb = (internalVolume, event) => { + const newExternalVolumePath = event.target.value; + const temporaryVolumes = [...volumeSettings]; + + const volumeIndex = temporaryVolumes.findIndex((index) => { + return getInternalVolume(index) === internalVolume; + }); + + if (volumeIndex > -1) { + temporaryVolumes[volumeIndex] = replaceExternalVolume(temporaryVolumes[volumeIndex], newExternalVolumePath); + } + + setVolumeSettings(temporaryVolumes); + if (typeof(onChange) === 'function') { + onChange(internalVolume, newExternalVolumePath); + } + }; + + return ( + + + {volumeSettings.map((volume) => { + const internalVolume = getInternalVolume(volume); + const currentExternalVolume = getExternalVolume(volume); + + return ( + + { onChangeCb(internalVolume, event) }} + value={currentExternalVolume} + style={{ width: '100%' }} + /> + + ); + })} + + + ); +}; + +export default VolumesConfig; diff --git a/.internal/wui/src/features/serviceUiControls/index.js b/.internal/wui/src/features/serviceUiControls/index.js new file mode 100644 index 000000000..0276c19ea --- /dev/null +++ b/.internal/wui/src/features/serviceUiControls/index.js @@ -0,0 +1,19 @@ +import PortConfig from './general/portConfig'; +import NetworkConfig from './general/networkConfig'; +import Logging from './general/logging'; +import Volumes from './general/volumesConfig'; +import Devices from './general/devicesConfig'; +import Environment from './general/environmentConfig'; +import DeconzDevices from './custom/deconz-devices'; +import NodeRedNpm from './custom/nodered-npm'; + +export default { + PortConfig, + NetworkConfig, + Logging, + Volumes, + Devices, + Environment, + DeconzDevices, + NodeRedNpm +}; diff --git a/.internal/wui/src/features/servicesGridItem/index.jsx b/.internal/wui/src/features/servicesGridItem/index.jsx new file mode 100644 index 000000000..854978e01 --- /dev/null +++ b/.internal/wui/src/features/servicesGridItem/index.jsx @@ -0,0 +1,370 @@ +import React, { Fragment, useState, useEffect } from 'react'; +import { useDispatch, useSelector } from "react-redux"; +import Box from '@material-ui/core/Box'; +import Tooltip from '@material-ui/core/Tooltip'; +import Skeleton from '@material-ui/lab/Skeleton'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import Checkbox from '@material-ui/core/Checkbox'; +import Button from '@material-ui/core/Button'; +import Link from '@material-ui/core/Link'; +import ErrorOutlineOutlinedIcon from '@material-ui/icons/ErrorOutlineOutlined'; +import { makeStyles } from '@material-ui/core/styles'; +import ServiceConfigModal from '../serviceConfigModal'; +import HelpAndDocsModal from '../helpAndDocsModal'; +import { useTheme } from '@material-ui/core/styles'; +import { API_STATUS } from '../../constants' +import { + getBuildIssuesAction +} from '../../actions/checkBuildIssues.action'; +import { + getServiceMetadataAction +} from '../../actions/getServiceMetadata.action'; +import { + getServiceConfigOptionsAction +} from '../../actions/getServiceConfigOptions.action'; +import { + addSelectedService, + removeSelectedService +} from '../../actions/updateSelectedServices.action'; +import { + getSelectedItems_services, + setSelectedItems_services +} from '../../utils/buildOptionSync'; +import styles from './services-grid-item.module.css'; + +const mapDispatchToProps = (dispatch) => { + return { + dispatchGetServiceMetadata: (serviceName) => dispatch(getServiceMetadataAction(serviceName)), + dispatchGetServiceConfigOptions: (serviceName) => dispatch(getServiceConfigOptionsAction(serviceName)), + dispatchGetBuildIssues: (selectedServices, configurations) => dispatch(getBuildIssuesAction(selectedServices, configurations)), + dispatchAddSelectedService: (serviceName) => dispatch(addSelectedService(serviceName)), + dispatchRemoveSelectedService: (serviceName) => dispatch(removeSelectedService(serviceName)) + }; +}; + +const mapStateToProps = (selector) => { + return { + templateList: selector(state => state.templateList), + hideServiceTags: selector(state => state.hideServiceTags), + selectedServices: selector(state => state.selectedServices), + buildIssues: selector(state => state.buildIssues) + }; +}; + +const useStyles = makeStyles({ + serviceCard: { + "&:hover": { + borderColor: ({ theme }) => theme.palette.text.primary + } + } +}); + +const ServiceItem = (props) => { + const theme = useTheme(); + const classes = useStyles({ props, theme }); + // console.log('theme.palette', theme.palette) + props = { + ...props, + ...mapDispatchToProps(useDispatch()), + ...mapStateToProps(useSelector) + }; + + const { + serviceName, + networkTemplateList, + serviceTemplates, + dispatchAddSelectedService, + dispatchRemoveSelectedService, + dispatchGetBuildIssues, + selectedServices, + hideServiceTags, + buildIssues, + buildOptions, + setBuildOptions, + getBuildOptions, + buildOptionsInit, + setServiceOptions, + setTemporaryBuildOptions, + getTemporaryBuildOptions, + setTemporaryServiceOptions, + setupTemporaryBuildOptions, + saveTemporaryBuildOptions, + allServicesMetadataReducer, + allServicesConfigOptionsReducer, + allServicesConfigHelpReducer + } = props; + + const [isLoading, setIsLoading] = useState(false); + const [serviceMetadata, setServiceMetadata] = useState({}); + const [serviceConfigOptions, setServiceConfigOptions] = useState({}); + const [serviceLoadError, setServiceLoadError] = useState({}); + useEffect(() => { + // const savedSelectedServices = getSelectedItems_services(); + setSelectedItems_services(selectedServices.selectedServices); + }, [selectedServices]); + + useEffect(() => { + if ( + allServicesMetadataReducer.status === API_STATUS.SUCCESS + && allServicesConfigOptionsReducer.status === API_STATUS.SUCCESS + ) { + if ( + !( + (allServicesMetadataReducer?.payload?.[serviceName] ?? false) + && (allServicesConfigOptionsReducer?.payload?.[serviceName] ?? false) + ) + ) { + setServiceLoadError(true); + setIsLoading(false); + return null; + } + setServiceMetadata(allServicesMetadataReducer?.payload?.[serviceName]); + setServiceConfigOptions(allServicesConfigOptionsReducer?.payload?.[serviceName]); + setIsLoading(false); + return null; + } + + setIsLoading(false); + + if ( + allServicesMetadataReducer.status === API_STATUS.FAILURE + && allServicesConfigOptionsReducer.status === API_STATUS.FAILURE + ) { + setIsLoading(false); + setServiceLoadError(true); + return null; + } + }, [ + allServicesMetadataReducer, + allServicesConfigOptionsReducer + ]); + + const [updated, setIsUpdated] = useState(false); + useEffect(() => { + if (updated) { + dispatchGetBuildIssues(selectedServices.selectedServices, getBuildOptions()); + } + setIsUpdated(false); + }, [ + updated, + serviceName, + selectedServices.selectedServices, + dispatchGetBuildIssues + ]); + + const [hasIssue, setHasIssue] = useState(false); + useEffect(() => { + if (!selectedServices.selectedServices.includes(serviceName)) { + return void setHasIssue(false); + } + const issueList = buildIssues?.payload?.issueList ?? {}; + if (Array.isArray(issueList.services)) { + let issueFound = false; + issueList.services.forEach((service) => { + if (service.name === serviceName) { + issueFound = true; + } + }); + return void setHasIssue(issueFound); + } + }, [buildIssues, selectedServices.selectedServices, serviceName]); + + const handleBuildSelectChange = (evt) => { + setIsUpdated(true); + if (evt.target.checked) { + return dispatchAddSelectedService(serviceName); + } + return dispatchRemoveSelectedService(serviceName); + } + + const [modalOpen, setModalOpen] = useState(false); + const [modalhelpAndDocsOpen, setModalhelpAndDocsOpen] = useState(false); + + const serviceComponent = () => { + return ( + + { + setModalhelpAndDocsOpen(false); + }} + /> + { + setModalOpen(false); + dispatchGetBuildIssues(selectedServices.selectedServices, getBuildOptions()); + }} + serviceMetadata={serviceMetadata} + serviceConfigOptions={serviceConfigOptions} + serviceName={serviceName} + buildOptions={buildOptions} + setBuildOptions={setBuildOptions} + setServiceOptions={setServiceOptions} + getBuildOptions={getBuildOptions} + buildOptionsInit={buildOptionsInit} + setTemporaryBuildOptions={setTemporaryBuildOptions} + getTemporaryBuildOptions={getTemporaryBuildOptions} + setTemporaryServiceOptions={setTemporaryServiceOptions} + setupTemporaryBuildOptions={setupTemporaryBuildOptions} + saveTemporaryBuildOptions={saveTemporaryBuildOptions} + networkTemplateList={networkTemplateList} + serviceTemplates={serviceTemplates} + /> + + {serviceMetadata.displayName} + + + {!serviceMetadata.iconUri + && ( + + + + )} + {serviceMetadata.iconUri + && ( + +
+ {`${serviceMetadata.displayName} +
+
+ )} +
+ + + + + + } + label={`Add ${serviceMetadata.displayName} to build`} + /> + + + {allServicesConfigHelpReducer.status === API_STATUS.SUCCESS && ( + + + + )} + +
+ ) + }; + + const errorComponent = () => { + return ( + +
Error loading: {serviceName}
+
Try refreshing, and ensuring the API server is running correctly.
+
+ ) + }; + + const loadingComponent = () => { + return ( + + Loading '{serviceName}' metadata... + + + + + + + + + + + + + + ) + }; + + const highlightClass = () => { + if (selectedServices.selectedServices.includes(serviceName)) { + if (hasIssue) { + return styles.serviceError; + } else { + return styles.selectedForBuild; + } + } + + return ''; + }; + + const tagIsHidden = (hiddenTags, serviceTags) => { + let hide = false; + + hiddenTags.forEach((hiddenTag) => { + serviceTags.forEach((serviceTag) => { + if (hiddenTag === serviceTag) { + hide = true; + } + }); + }); + + return hide; + } + + if (!isLoading && tagIsHidden(hideServiceTags.hideServiceTags, serviceMetadata.serviceTypeTags)) { + return null; + } + + return ( + + + {isLoading + && (loadingComponent())} + {!isLoading + && serviceMetadata.displayName + && ( + serviceComponent() + )} + {!isLoading + && serviceLoadError === true + && ( + errorComponent() + )} + + + ); +}; + +export default ServiceItem; diff --git a/.internal/wui/src/features/servicesGridItem/services-grid-item.module.css b/.internal/wui/src/features/servicesGridItem/services-grid-item.module.css new file mode 100644 index 000000000..0c8384dbd --- /dev/null +++ b/.internal/wui/src/features/servicesGridItem/services-grid-item.module.css @@ -0,0 +1,44 @@ +.docsLink { + background: linear-gradient(45deg, #2196F3 30%, #04add3 90%); + box-shadow: 0 3px 5px 2px rgba(33, 203, 243, .3); + color: 'white'; + padding: 0.5rem; +} + +.selectedForBuild { + background: linear-gradient(45deg, #76c01644 30%, #04d35a44 90%); +} + +.buildIssue { + background: linear-gradient(45deg, #ff7b008c 30%, #ffee00a8 90%); +} + +.serviceError { + background: linear-gradient(45deg, #c016167f 30%, #d304047f 90%); +} + +.serviceCard { + width: 24rem; + height: 24rem; + /* border-color: "primary.main"; */ +} + +.serviceIconContainer { + height: 6rem +} + +.serviceIcon { + height: 6rem; + width: auto; +} + +.serviceName { + white-space: nowrap; + overflow: hidden; + font-size: 2rem; +} + +.configButton { + white-space: nowrap; + overflow: hidden; +} diff --git a/.internal/wui/src/index.css b/.internal/wui/src/index.css new file mode 100644 index 000000000..bd5bd6d57 --- /dev/null +++ b/.internal/wui/src/index.css @@ -0,0 +1,13 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} diff --git a/.internal/wui/src/index.js b/.internal/wui/src/index.js new file mode 100644 index 000000000..41c478d64 --- /dev/null +++ b/.internal/wui/src/index.js @@ -0,0 +1,15 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import './index.css'; +import App from './App'; +import store from './reducers'; +import { Provider } from 'react-redux'; + +ReactDOM.render( + + + + + , + document.getElementById('root') +); diff --git a/.internal/wui/src/logo.svg b/.internal/wui/src/logo.svg new file mode 100644 index 000000000..9e9633482 --- /dev/null +++ b/.internal/wui/src/logo.svg @@ -0,0 +1 @@ + diff --git a/.internal/wui/src/pages/buildHistory/index.jsx b/.internal/wui/src/pages/buildHistory/index.jsx new file mode 100644 index 000000000..70bbe1f41 --- /dev/null +++ b/.internal/wui/src/pages/buildHistory/index.jsx @@ -0,0 +1,88 @@ +// import React, { Fragment, useState, useEffect } from 'react'; +import React, { Fragment, useEffect } from 'react'; +import { useDispatch, useSelector } from "react-redux"; +import Grid from '@material-ui/core/Grid'; +import BuildHistoryGridItem from '../../features/buildHistoryGridItem' +import { + getBuildHistoryListAction +} from '../../actions/getBuildHistoryList.action'; + +import { + clearBuildStateAction +} from '../../actions/buildStack.action'; + +import { + downloadBuildFile +} from '../../actions/downloadBuild.action'; + +const mapDispatchToProps = (dispatch) => { + return { + dispatchGetBuildHistoryList: () => dispatch(getBuildHistoryListAction()), + dispatchClearBuildState: () => dispatch(clearBuildStateAction()), + dispatchDownloadBuildFile: ({ build, type, linkRef }) => dispatch(downloadBuildFile({ build, type, linkRef })) + }; +}; + +const mapStateToProps = (selector) => { + return { + buildHistory: selector(state => state.buildHistory), + deleteBuild: selector(state => state.deleteBuild) + }; +}; + +const Main = (props) => { + const downloadLinkRef = React.useRef(null); + props = { + ...props, + ...mapDispatchToProps(useDispatch()), + ...mapStateToProps(useSelector) + }; + + const { + dispatchGetBuildHistoryList, + dispatchClearBuildState, + dispatchDownloadBuildFile, + buildHistory, + deleteBuild + } = props; + + useEffect(() => { + dispatchGetBuildHistoryList(); + dispatchClearBuildState(); + }, []); + + useEffect(() => { + dispatchGetBuildHistoryList(); + }, [deleteBuild]); + + return ( + +
+
+ + {typeof buildHistory.payload !== 'undefined' && Object.keys(buildHistory.payload.buildsList).map((buildDetailsTime) => { + return ( + + + + ); + })} + +
+ + ); +} + +export default Main; diff --git a/.internal/wui/src/pages/help/index.jsx b/.internal/wui/src/pages/help/index.jsx new file mode 100644 index 000000000..38474d37d --- /dev/null +++ b/.internal/wui/src/pages/help/index.jsx @@ -0,0 +1,13 @@ +import React, { Fragment } from 'react'; + +const Help = () => { + return ( + +
+ Not completed - Help +
+
+ ); +} + +export default Help; diff --git a/.internal/wui/src/pages/mainBuild/index.jsx b/.internal/wui/src/pages/mainBuild/index.jsx new file mode 100644 index 000000000..91408219c --- /dev/null +++ b/.internal/wui/src/pages/mainBuild/index.jsx @@ -0,0 +1,157 @@ +// import React, { Fragment, useState, useEffect } from 'react'; +import React, { Fragment, useEffect, useState } from 'react'; +import { useDispatch, useSelector } from "react-redux"; +import Grid from '@material-ui/core/Grid'; +import Box from '@material-ui/core/Box'; +import ServiceGridItem from '../../features/servicesGridItem'; +import BuildSidebar from '../../features/BuildSidebar'; +import { getServiceTemplateListAction } from '../../actions/getServiceTemplateList.action'; +import { getServiceTemplatesAction } from '../../actions/getServiceTemplates.action'; +import { getNetworkTemplateListAction } from '../../actions/getNetworkTemplateList.action'; +import { getAllServicesConfigOptionsAction } from '../../actions/getAllServicesConfigOptions.action'; +import { getAllServicesConfigHelpAction } from '../../actions/getAllServicesConfigHelp.action'; +import { getAllServicesMetadataAction } from '../../actions/getAllServicesMetadata.action'; +import { + getBuildOptions, + setBuildOptions, + buildOptionsInit, + setServiceOptions, + setTemporaryBuildOptions, + getTemporaryBuildOptions, + setTemporaryServiceOptions, + setupTemporaryBuildOptions, + saveTemporaryBuildOptions, + getSelectedItems_services +} from '../../utils/buildOptionSync'; +import { + addSelectedService, + clearAllSelectedServicesAction +} from '../../actions/updateSelectedServices.action'; + +const mapDispatchToProps = (dispatch) => { + return { + dispatchGetServiceTemplatesList: () => dispatch(getServiceTemplateListAction()), + dispatchGetServiceTemplatesList: () => dispatch(getServiceTemplateListAction()), + dispatchGetNetworkTemplatesList: () => dispatch(getNetworkTemplateListAction()), + dispatchGetServiceTemplates: () => dispatch(getServiceTemplatesAction()), + dispatchGetAllServicesMetadata: () => dispatch(getAllServicesMetadataAction()), + dispatchAddSelectedService: (serviceName) => dispatch(addSelectedService(serviceName)), + dispatchGetAllServicesConfigOptions: () => dispatch(getAllServicesConfigOptionsAction()), + getAllServicesConfigHelpAction: () => dispatch(getAllServicesConfigHelpAction()), + dispatchClearAllSelectedServices: () => dispatch(clearAllSelectedServicesAction()) + }; +}; + +const mapStateToProps = (selector) => { + return { + serviceTemplateList: selector(state => state.serviceTemplateList), + networkTemplateList: selector(state => state.networkTemplateList), + serviceTemplates: selector(state => state.serviceTemplates), + allServicesConfigOptionsReducer: selector(state => state.allServicesConfigOptionsReducer), + allServicesConfigHelpReducer: selector(state => state.allServicesConfigHelpReducer), + selectedServices: selector(state => state.selectedServices), + allServicesMetadataReducer: selector(state => state.allServicesMetadataReducer) + }; +}; + +const Main = (props) => { + props = { + ...props, + ...mapDispatchToProps(useDispatch()), + ...mapStateToProps(useSelector) + }; + + const { + dispatchGetServiceTemplatesList, + dispatchAddSelectedService, + dispatchClearAllSelectedServices, + dispatchGetNetworkTemplatesList, + dispatchGetServiceTemplates, + dispatchGetAllServicesMetadata, + dispatchGetAllServicesConfigOptions, + getAllServicesConfigHelpAction, + allServicesConfigOptionsReducer, + allServicesConfigHelpReducer, + allServicesMetadataReducer, + serviceTemplateList, + networkTemplateList, + serviceTemplates, + selectedServices + } = props; + const [isLoading, setIsLoading] = useState(true); + const buildOptions = getBuildOptions(); + + useEffect(() => { + dispatchGetServiceTemplatesList(); + dispatchGetNetworkTemplatesList(); + dispatchGetServiceTemplates(); + dispatchGetAllServicesMetadata(); + dispatchGetAllServicesConfigOptions(); + getAllServicesConfigHelpAction(); + + dispatchClearAllSelectedServices(); + }, []); + + useEffect(() => { + if (isLoading) { + setIsLoading(false) + const savedSelectedServices = getSelectedItems_services(); + savedSelectedServices.map((service) => { + dispatchAddSelectedService(service); + }); + } + }, [selectedServices]); + + return ( + +
+ + + + {Array.isArray(serviceTemplateList.payload) && serviceTemplateList.payload.map((templateName) => { + return ( + + + + ); + })} + + + + + + + +
+
+ ); +}; + +export default Main; diff --git a/.internal/wui/src/pages/notFound/index.js b/.internal/wui/src/pages/notFound/index.js new file mode 100644 index 000000000..0d9f0174e --- /dev/null +++ b/.internal/wui/src/pages/notFound/index.js @@ -0,0 +1,13 @@ +import React, { Fragment } from 'react'; + +const NotFound = () => { + return ( + +
+ Page not found +
+
+ ); +} + +export default NotFound; diff --git a/.internal/wui/src/pages/scripts/index.jsx b/.internal/wui/src/pages/scripts/index.jsx new file mode 100644 index 000000000..8b86ea72f --- /dev/null +++ b/.internal/wui/src/pages/scripts/index.jsx @@ -0,0 +1,13 @@ +import React, { Fragment } from 'react'; + +const Scripts = () => { + return ( + +
+ Not completed - Scripts +
+
+ ); +} + +export default Scripts; diff --git a/.internal/wui/src/reducers/buildStackReducer.js b/.internal/wui/src/reducers/buildStackReducer.js new file mode 100644 index 000000000..4ee4153da --- /dev/null +++ b/.internal/wui/src/reducers/buildStackReducer.js @@ -0,0 +1,45 @@ +import { API_STATUS } from '../constants' +import { CREATE_AND_BUILD_STACK, RESET_STATE_CREATE_AND_BUILD_STACK } from '../actions/buildStack.action'; + +const defaultState = { + status: API_STATUS.UNINIT +}; + +const reducerHandler = (state = defaultState, action) => { + switch (action.type) { + case RESET_STATE_CREATE_AND_BUILD_STACK: + return { + ...state, + status: API_STATUS.UNINIT + } + + case `${CREATE_AND_BUILD_STACK}_${API_STATUS.PENDING}`: + return { + ...state, + status: API_STATUS.PENDING + } + + case `${CREATE_AND_BUILD_STACK}_${API_STATUS.SUCCESS}`: + return { + ...state, + status: API_STATUS.SUCCESS, + payload: action.res + } + + case `${CREATE_AND_BUILD_STACK}_${API_STATUS.FAILURE}`: + return { + ...state, + status: API_STATUS.FAILURE, + payload: undefined, + error: action.error + } + default: + return state + } +}; + +export default reducerHandler; + +export const getBuildStackSelector = (state) => { + return state.buildStack +}; diff --git a/.internal/wui/src/reducers/counter.js b/.internal/wui/src/reducers/counter.js new file mode 100644 index 000000000..01dcfe06b --- /dev/null +++ b/.internal/wui/src/reducers/counter.js @@ -0,0 +1,42 @@ +import { createSlice } from '@reduxjs/toolkit'; + +export const counterSlice = createSlice({ + name: 'counter', + initialState: { + value: 0, + }, + reducers: { + increment: state => { + // Redux Toolkit allows us to write "mutating" logic in reducers. It + // doesn't actually mutate the state because it uses the Immer library, + // which detects changes to a "draft state" and produces a brand new + // immutable state based off those changes + state.value += 1; + }, + decrement: state => { + state.value -= 1; + }, + incrementByAmount: (state, action) => { + state.value += action.payload; + }, + }, +}); + +export const { increment, decrement, incrementByAmount } = counterSlice.actions; + +// The function below is called a thunk and allows us to perform async logic. It +// can be dispatched like a regular action: `dispatch(incrementAsync(10))`. This +// will call the thunk with the `dispatch` function as the first argument. Async +// code can then be executed and other actions can be dispatched +export const incrementAsync = amount => dispatch => { + setTimeout(() => { + dispatch(incrementByAmount(amount)); + }, 1000); +}; + +// The function below is called a selector and allows us to select a value from +// the state. Selectors can also be defined inline where they're used instead of +// in the slice file. For example: `useSelector((state) => state.counter.value)` +export const selectCount = state => state.counter.value; + +export default counterSlice.reducer; diff --git a/.internal/wui/src/reducers/getAllServicesConfigHelpReducer.js b/.internal/wui/src/reducers/getAllServicesConfigHelpReducer.js new file mode 100644 index 000000000..139c37568 --- /dev/null +++ b/.internal/wui/src/reducers/getAllServicesConfigHelpReducer.js @@ -0,0 +1,39 @@ +import { API_STATUS } from '../constants' +import { GET_ALL_SERVICES_CONFIG_HELP_ACTION } from '../actions/getAllServicesConfigHelp.action'; + +const defaultState = { + status: API_STATUS.UNINIT +}; + +const reducerHandler = (state = defaultState, action) => { + switch (action.type) { + case `${GET_ALL_SERVICES_CONFIG_HELP_ACTION}_${API_STATUS.PENDING}`: + return { + ...state, + status: API_STATUS.PENDING + } + + case `${GET_ALL_SERVICES_CONFIG_HELP_ACTION}_${API_STATUS.SUCCESS}`: + return { + ...state, + status: API_STATUS.SUCCESS, + payload: action.res + } + + case `${GET_ALL_SERVICES_CONFIG_HELP_ACTION}_${API_STATUS.FAILURE}`: + return { + ...state, + status: API_STATUS.FAILURE, + payload: undefined, + error: action.error + } + default: + return state + } +}; + +export default reducerHandler; + +export const getAllServicesConfigHelpSelector = (state) => { + return state.allServicesConfigHelp +}; diff --git a/.internal/wui/src/reducers/getAllServicesConfigOptionsReducer.js b/.internal/wui/src/reducers/getAllServicesConfigOptionsReducer.js new file mode 100644 index 000000000..9c17248a6 --- /dev/null +++ b/.internal/wui/src/reducers/getAllServicesConfigOptionsReducer.js @@ -0,0 +1,39 @@ +import { API_STATUS } from '../constants' +import { GET_ALL_SERVICES_CONFIG_OPTIONS_ACTION } from '../actions/getAllServicesConfigOptions.action'; + +const defaultState = { + status: API_STATUS.UNINIT +}; + +const reducerHandler = (state = defaultState, action) => { + switch (action.type) { + case `${GET_ALL_SERVICES_CONFIG_OPTIONS_ACTION}_${API_STATUS.PENDING}`: + return { + ...state, + status: API_STATUS.PENDING + } + + case `${GET_ALL_SERVICES_CONFIG_OPTIONS_ACTION}_${API_STATUS.SUCCESS}`: + return { + ...state, + status: API_STATUS.SUCCESS, + payload: action.res + } + + case `${GET_ALL_SERVICES_CONFIG_OPTIONS_ACTION}_${API_STATUS.FAILURE}`: + return { + ...state, + status: API_STATUS.FAILURE, + payload: undefined, + error: action.error + } + default: + return state + } +}; + +export default reducerHandler; + +export const getAllServicesConfigOptionsSelector = (state) => { + return state.allServicesConfigOptions +}; diff --git a/.internal/wui/src/reducers/getAllServicesMetadataReducer.js b/.internal/wui/src/reducers/getAllServicesMetadataReducer.js new file mode 100644 index 000000000..11f2f16c8 --- /dev/null +++ b/.internal/wui/src/reducers/getAllServicesMetadataReducer.js @@ -0,0 +1,39 @@ +import { API_STATUS } from '../constants' +import { GET_ALL_SERVICES_METADATA_ACTION } from '../actions/getAllServicesMetadata.action'; + +const defaultState = { + status: API_STATUS.UNINIT +}; + +const reducerHandler = (state = defaultState, action) => { + switch (action.type) { + case `${GET_ALL_SERVICES_METADATA_ACTION}_${API_STATUS.PENDING}`: + return { + ...state, + status: API_STATUS.PENDING + }; + + case `${GET_ALL_SERVICES_METADATA_ACTION}_${API_STATUS.SUCCESS}`: + return { + ...state, + status: API_STATUS.SUCCESS, + payload: action.res + }; + + case `${GET_ALL_SERVICES_METADATA_ACTION}_${API_STATUS.FAILURE}`: + return { + ...state, + status: API_STATUS.FAILURE, + payload: undefined, + error: action.error + }; + default: + return state; + } +}; + +export default reducerHandler; + +export const getAllServicesMetadataSelector = (state) => { + return state.allServicesMetadata +}; diff --git a/.internal/wui/src/reducers/getBuildFileReducer.js b/.internal/wui/src/reducers/getBuildFileReducer.js new file mode 100644 index 000000000..f28a7aa74 --- /dev/null +++ b/.internal/wui/src/reducers/getBuildFileReducer.js @@ -0,0 +1,85 @@ +import { API_STATUS } from '../constants' +import { GET_BUILD_FILE_ACTION } from '../actions/getBuildFile.action'; + +const defaultState = { + files: { + pending: [], + failed: {}, + completed: {} + }, + status: API_STATUS.UNINIT, + payload: null +}; + +const reducerHandler = (state = defaultState, action) => { + const newState = JSON.parse(JSON.stringify(state)); + + switch (action.type) { + case `${GET_BUILD_FILE_ACTION}_${API_STATUS.PENDING}`: + if (action?.label) { + if (state.files.pending.indexOf(action.label) > -1) { + console.warn(`getBuildFile: '${action.label}' already dispatched`, action); + } + newState.files.pending.push(action.label); + delete newState.files.completed[action.label]; + delete newState.files.failed[action.label]; + return { + ...state, + ...newState + }; + } + return { + ...state, + status: API_STATUS.PENDING + }; + + case `${GET_BUILD_FILE_ACTION}_${API_STATUS.SUCCESS}`: + if (action?.label) { + newState.files.completed[action.label] = { + status: API_STATUS.SUCCESS, + payload: action.res + }; + delete newState.files.failed[action.label]; + newState.files.pending.splice(newState.files.pending.indexOf(action.label), 1); + return { + ...state, + ...newState + }; + } + return { + ...state, + status: API_STATUS.SUCCESS, + payload: action.res + }; + + case `${GET_BUILD_FILE_ACTION}_${API_STATUS.FAILURE}`: + if (action?.label) { + newState.files.failed[action.label] = { + status: API_STATUS.FAILURE, + payload: undefined, + error: JSON.stringify(action.error, Object.getOwnPropertyNames(action.error)) + }; + delete newState.files.completed[action.label]; + newState.files.pending.splice(newState.files.pending.indexOf(action.label), 1); + return { + ...state, + ...newState + } + } + return { + ...state, + status: API_STATUS.FAILURE, + payload: undefined, + error: action.error + }; + + default: + return state + } +}; + +export default reducerHandler; + +export const getBuildFileSelector = (state) => { + return state.buildFileSelector +}; diff --git a/.internal/wui/src/reducers/getBuildHistoryListReducer.js b/.internal/wui/src/reducers/getBuildHistoryListReducer.js new file mode 100644 index 000000000..fcf264100 --- /dev/null +++ b/.internal/wui/src/reducers/getBuildHistoryListReducer.js @@ -0,0 +1,39 @@ +import { API_STATUS } from '../constants' +import { GET_BUILD_HISTORY_LIST_ACTION } from '../actions/getBuildHistoryList.action'; + +const defaultState = { + status: API_STATUS.UNINIT +}; + +const reducerHandler = (state = defaultState, action) => { + switch (action.type) { + case `${GET_BUILD_HISTORY_LIST_ACTION}_${API_STATUS.PENDING}`: + return { + ...state, + status: API_STATUS.PENDING + } + + case `${GET_BUILD_HISTORY_LIST_ACTION}_${API_STATUS.SUCCESS}`: + return { + ...state, + status: API_STATUS.SUCCESS, + payload: action.res + } + + case `${GET_BUILD_HISTORY_LIST_ACTION}_${API_STATUS.FAILURE}`: + return { + ...state, + status: API_STATUS.FAILURE, + payload: undefined, + error: action.error + } + default: + return state + } +}; + +export default reducerHandler; + +export const getBuildHistoryListSelector = (state) => { + return state.buildHistory +}; diff --git a/.internal/wui/src/reducers/getBuildIssuesReducer.js b/.internal/wui/src/reducers/getBuildIssuesReducer.js new file mode 100644 index 000000000..44cb600bf --- /dev/null +++ b/.internal/wui/src/reducers/getBuildIssuesReducer.js @@ -0,0 +1,39 @@ +import { API_STATUS } from '../constants' +import { CHECK_BUILD_ISSUES } from '../actions/checkBuildIssues.action'; + +const defaultState = { + status: API_STATUS.UNINIT +}; + +const reducerHandler = (state = defaultState, action) => { + switch (action.type) { + case `${CHECK_BUILD_ISSUES}_${API_STATUS.PENDING}`: + return { + ...state, + status: API_STATUS.PENDING + } + + case `${CHECK_BUILD_ISSUES}_${API_STATUS.SUCCESS}`: + return { + ...state, + status: API_STATUS.SUCCESS, + payload: action.res + } + + case `${CHECK_BUILD_ISSUES}_${API_STATUS.FAILURE}`: + return { + ...state, + status: API_STATUS.FAILURE, + payload: undefined, + error: action.error + } + default: + return state + } +}; + +export default reducerHandler; + +export const getBuildIssuesSelector = (state) => { + return state.buildIssues +}; diff --git a/.internal/wui/src/reducers/getDeleteBuildReducer.js b/.internal/wui/src/reducers/getDeleteBuildReducer.js new file mode 100644 index 000000000..e1ef6c608 --- /dev/null +++ b/.internal/wui/src/reducers/getDeleteBuildReducer.js @@ -0,0 +1,45 @@ +import { API_STATUS } from '../constants' +import { DELETE_BUILD, RESET_STATE_DELETE_BUILD } from '../actions/deleteBuild.action'; + +const defaultState = { + status: API_STATUS.UNINIT +}; + +const reducerHandler = (state = defaultState, action) => { + switch (action.type) { + case RESET_STATE_DELETE_BUILD: + return { + ...state, + status: API_STATUS.UNINIT + } + + case `${DELETE_BUILD}_${API_STATUS.PENDING}`: + return { + ...state, + status: API_STATUS.PENDING + } + + case `${DELETE_BUILD}_${API_STATUS.SUCCESS}`: + return { + ...state, + status: API_STATUS.SUCCESS, + payload: action.res + } + + case `${DELETE_BUILD}_${API_STATUS.FAILURE}`: + return { + ...state, + status: API_STATUS.FAILURE, + payload: undefined, + error: action.error + } + default: + return state + } +}; + +export default reducerHandler; + +export const getDeleteBuildSelector = (state) => { + return state.deleteBuild +}; diff --git a/.internal/wui/src/reducers/getNetworkTemplateListReducer.js b/.internal/wui/src/reducers/getNetworkTemplateListReducer.js new file mode 100644 index 000000000..0fc9cb38f --- /dev/null +++ b/.internal/wui/src/reducers/getNetworkTemplateListReducer.js @@ -0,0 +1,39 @@ +import { API_STATUS } from '../constants' +import { GET_NETWORK_TEMPLATE_LIST_ACTION } from '../actions/getNetworkTemplateList.action'; + +const defaultState = { + status: API_STATUS.UNINIT +}; + +const reducerHandler = (state = defaultState, action) => { + switch (action.type) { + case `${GET_NETWORK_TEMPLATE_LIST_ACTION}_${API_STATUS.PENDING}`: + return { + ...state, + status: API_STATUS.PENDING + } + + case `${GET_NETWORK_TEMPLATE_LIST_ACTION}_${API_STATUS.SUCCESS}`: + return { + ...state, + status: API_STATUS.SUCCESS, + payload: action.res + } + + case `${GET_NETWORK_TEMPLATE_LIST_ACTION}_${API_STATUS.FAILURE}`: + return { + ...state, + status: API_STATUS.FAILURE, + payload: undefined, + error: action.error + } + default: + return state + } +}; + +export default reducerHandler; + +export const getNetworkTemplateListSelector = (state) => { + return state.networkTemplateList +}; diff --git a/.internal/wui/src/reducers/getScriptTemplatesReducer.js b/.internal/wui/src/reducers/getScriptTemplatesReducer.js new file mode 100644 index 000000000..669ed5202 --- /dev/null +++ b/.internal/wui/src/reducers/getScriptTemplatesReducer.js @@ -0,0 +1,57 @@ +import { API_STATUS } from '../constants' +import { GET_SCRIPT_TEMPLATE } from '../actions/getScript.action'; + +const defaultState = { + scripts: { + pending: [], + failed: {}, + completed: {} + } +}; + +const reducerHandler = (state = defaultState, action) => { + const newState = JSON.parse(JSON.stringify(state)); + switch (action.type) { + case `${GET_SCRIPT_TEMPLATE}_${API_STATUS.PENDING}`: + if (state.scripts.pending.indexOf(action.label) > -1) { + console.warn(`getScriptTemplatesReducer: '${action.label}' already dispatched`, action); + } + newState.scripts.pending.push(action.label); + delete newState.scripts.completed[action.label]; + delete newState.scripts.failed[action.label]; + return { + ...state, + ...newState + } + + case `${GET_SCRIPT_TEMPLATE}_${API_STATUS.SUCCESS}`: + newState.scripts.completed[action.label] = { + status: API_STATUS.SUCCESS, + payload: action.res + }; + delete newState.scripts.failed[action.label]; + newState.scripts.pending.splice(newState.scripts.pending.indexOf(action.label), 1); + return { + ...state, + ...newState + } + + case `${GET_SCRIPT_TEMPLATE}_${API_STATUS.FAILURE}`: + newState.scripts.failed[action.label] = { + status: API_STATUS.FAILURE, + payload: undefined, + error: JSON.stringify(action.error, Object.getOwnPropertyNames(action.error)) + }; + delete newState.scripts.completed[action.label]; + newState.scripts.pending.splice(newState.scripts.pending.indexOf(action.label), 1); + return { + ...state, + ...newState + } + + default: + return state + } +}; + +export default reducerHandler; diff --git a/.internal/wui/src/reducers/getServiceConfigOptionsReducer.js b/.internal/wui/src/reducers/getServiceConfigOptionsReducer.js new file mode 100644 index 000000000..795bbcf34 --- /dev/null +++ b/.internal/wui/src/reducers/getServiceConfigOptionsReducer.js @@ -0,0 +1,61 @@ +import { API_STATUS } from '../constants' +import { GET_CONFIG_SERVICE_CONFIG_OPTIONS } from '../actions/getServiceConfigOptions.action'; + +const defaultState = { + services: { + pending: [], + failed: {}, + completed: {} + } +}; + +const reducerHandler = (state = defaultState, action) => { + const newState = JSON.parse(JSON.stringify(state)); + switch (action.type) { + case `${GET_CONFIG_SERVICE_CONFIG_OPTIONS}_${API_STATUS.PENDING}`: + if (state.services.pending.indexOf(action.label) > -1) { + console.warn(`getServiceConfigOptionsReducer: '${action.label}' already dispatched`, action); + } + newState.services.pending.push(action.label); + delete newState.services.completed[action.label]; + delete newState.services.failed[action.label]; + return { + ...state, + ...newState + } + + case `${GET_CONFIG_SERVICE_CONFIG_OPTIONS}_${API_STATUS.SUCCESS}`: + newState.services.completed[action.label] = { + status: API_STATUS.SUCCESS, + payload: action.res + }; + delete newState.services.failed[action.label]; + newState.services.pending.splice(newState.services.pending.indexOf(action.label), 1); + return { + ...state, + ...newState + } + + case `${GET_CONFIG_SERVICE_CONFIG_OPTIONS}_${API_STATUS.FAILURE}`: + newState.services.failed[action.label] = { + status: API_STATUS.FAILURE, + payload: undefined, + error: JSON.stringify(action.error, Object.getOwnPropertyNames(action.error)) + }; + delete newState.services.completed[action.label]; + newState.services.pending.splice(newState.services.pending.indexOf(action.label), 1); + return { + ...state, + ...newState + } + + default: + return state + } +}; + +export default reducerHandler; + +export const getConfigServiceConfigOptionsSelector = (state) => { + return state.configServiceConfigOptionsSelector +}; diff --git a/.internal/wui/src/reducers/getServiceMetadataReducer.js b/.internal/wui/src/reducers/getServiceMetadataReducer.js new file mode 100644 index 000000000..4132f2725 --- /dev/null +++ b/.internal/wui/src/reducers/getServiceMetadataReducer.js @@ -0,0 +1,61 @@ +import { API_STATUS } from '../constants' +import { GET_CONFIG_SERVICE_METADATA } from '../actions/getServiceMetadata.action'; + +const defaultState = { + services: { + pending: [], + failed: {}, + completed: {} + } +}; + +const reducerHandler = (state = defaultState, action) => { + const newState = JSON.parse(JSON.stringify(state)); + switch (action.type) { + case `${GET_CONFIG_SERVICE_METADATA}_${API_STATUS.PENDING}`: + if (state.services.pending.indexOf(action.label) > -1) { + console.warn(`getServiceMetadataReducer: '${action.label}' already dispatched`, action); + } + newState.services.pending.push(action.label); + delete newState.services.completed[action.label]; + delete newState.services.failed[action.label]; + return { + ...state, + ...newState + } + + case `${GET_CONFIG_SERVICE_METADATA}_${API_STATUS.SUCCESS}`: + newState.services.completed[action.label] = { + status: API_STATUS.SUCCESS, + payload: action.res + }; + delete newState.services.failed[action.label]; + newState.services.pending.splice(newState.services.pending.indexOf(action.label), 1); + return { + ...state, + ...newState + } + + case `${GET_CONFIG_SERVICE_METADATA}_${API_STATUS.FAILURE}`: + newState.services.failed[action.label] = { + status: API_STATUS.FAILURE, + payload: undefined, + error: JSON.stringify(action.error, Object.getOwnPropertyNames(action.error)) + }; + delete newState.services.completed[action.label]; + newState.services.pending.splice(newState.services.pending.indexOf(action.label), 1); + return { + ...state, + ...newState + } + + default: + return state + } +}; + +export default reducerHandler; + +export const getConfigServiceMetadataSelector = (state) => { + return state.configServiceMetadataSelector +}; diff --git a/.internal/wui/src/reducers/getServiceTemplateListReducer.js b/.internal/wui/src/reducers/getServiceTemplateListReducer.js new file mode 100644 index 000000000..2f9311863 --- /dev/null +++ b/.internal/wui/src/reducers/getServiceTemplateListReducer.js @@ -0,0 +1,39 @@ +import { API_STATUS } from '../constants' +import { GET_SERVICE_TEMPLATE_LIST_ACTION } from '../actions/getServiceTemplateList.action'; + +const defaultState = { + status: API_STATUS.UNINIT +}; + +const reducerHandler = (state = defaultState, action) => { + switch (action.type) { + case `${GET_SERVICE_TEMPLATE_LIST_ACTION}_${API_STATUS.PENDING}`: + return { + ...state, + status: API_STATUS.PENDING + } + + case `${GET_SERVICE_TEMPLATE_LIST_ACTION}_${API_STATUS.SUCCESS}`: + return { + ...state, + status: API_STATUS.SUCCESS, + payload: action.res + } + + case `${GET_SERVICE_TEMPLATE_LIST_ACTION}_${API_STATUS.FAILURE}`: + return { + ...state, + status: API_STATUS.FAILURE, + payload: undefined, + error: action.error + } + default: + return state + } +}; + +export default reducerHandler; + +export const getServiceTemplateListSelector = (state) => { + return state.serviceTemplateList +}; diff --git a/.internal/wui/src/reducers/getServiceTemplatesReducer.js b/.internal/wui/src/reducers/getServiceTemplatesReducer.js new file mode 100644 index 000000000..402e7f69c --- /dev/null +++ b/.internal/wui/src/reducers/getServiceTemplatesReducer.js @@ -0,0 +1,40 @@ +import { API_STATUS } from '../constants' +import { GET_SERVICE_TEMPLATES_ACTION } from '../actions/getServiceTemplates.action'; + +const defaultState = { + status: API_STATUS.UNINIT, + payload: {} +}; + +const reducerHandler = (state = defaultState, action) => { + switch (action.type) { + case `${GET_SERVICE_TEMPLATES_ACTION}_${API_STATUS.PENDING}`: + return { + ...state, + status: API_STATUS.PENDING + } + + case `${GET_SERVICE_TEMPLATES_ACTION}_${API_STATUS.SUCCESS}`: + return { + ...state, + status: API_STATUS.SUCCESS, + payload: action.res + } + + case `${GET_SERVICE_TEMPLATES_ACTION}_${API_STATUS.FAILURE}`: + return { + ...state, + status: API_STATUS.FAILURE, + payload: undefined, + error: action.error + } + default: + return state + } +}; + +export default reducerHandler; + +export const getServiceTemplatesSelector = (state) => { + return state.serviceTemplates +}; diff --git a/.internal/wui/src/reducers/index.js b/.internal/wui/src/reducers/index.js new file mode 100644 index 000000000..5251d59ae --- /dev/null +++ b/.internal/wui/src/reducers/index.js @@ -0,0 +1,47 @@ +import { configureStore } from '@reduxjs/toolkit'; +import thunk from 'redux-thunk'; +import promiseMiddleware from './middlewares/promiseMiddleware'; +import asyncDispatchMiddleware from './middlewares/asyncDispatchMiddleware'; +import counterReducer from './counter'; +import serviceTemplateList from './getServiceTemplateListReducer'; +import serviceTemplates from './getServiceTemplatesReducer'; +import networkTemplateList from './getNetworkTemplateListReducer'; +import configServiceMetadata from './getServiceMetadataReducer'; +import allServicesMetadataReducer from './getAllServicesMetadataReducer'; +import configServiceConfigOptions from './getServiceConfigOptionsReducer'; +import allServicesConfigOptionsReducer from './getAllServicesConfigOptionsReducer'; +import allServicesConfigHelpReducer from './getAllServicesConfigHelpReducer'; +import selectedServices from './updateSelectedServicesReducer'; +import hideServiceTags from './updateSelectedFilterTagsReducer'; +import buildIssues from './getBuildIssuesReducer'; +import buildStack from './buildStackReducer'; +import buildHistory from './getBuildHistoryListReducer'; +import scriptTemplates from './getScriptTemplatesReducer'; +import buildFiles from './getBuildFileReducer'; +import deleteBuild from './getDeleteBuildReducer'; + + +const middlewares = [thunk, promiseMiddleware, asyncDispatchMiddleware]; + +export default configureStore({ + reducer: { + counter: counterReducer, + serviceTemplateList: serviceTemplateList, + networkTemplateList: networkTemplateList, + serviceTemplates: serviceTemplates, + configServiceMetadata: configServiceMetadata, + allServicesMetadataReducer: allServicesMetadataReducer, + configServiceConfigOptions: configServiceConfigOptions, + allServicesConfigOptionsReducer: allServicesConfigOptionsReducer, + allServicesConfigHelpReducer: allServicesConfigHelpReducer, + selectedServices: selectedServices, + buildHistory: buildHistory, + hideServiceTags: hideServiceTags, + buildIssues: buildIssues, + buildStack: buildStack, + scriptTemplates: scriptTemplates, + buildFiles: buildFiles, + deleteBuild: deleteBuild + }, + middleware: middlewares +}); diff --git a/.internal/wui/src/reducers/middlewares/asyncDispatchMiddleware.js b/.internal/wui/src/reducers/middlewares/asyncDispatchMiddleware.js new file mode 100644 index 000000000..a1d3a2c47 --- /dev/null +++ b/.internal/wui/src/reducers/middlewares/asyncDispatchMiddleware.js @@ -0,0 +1,25 @@ +const asyncDispatchMiddleware = store => next => action => { + let syncActivityFinished = false; + let actionQueue = []; + + const flushQueue = () => { + actionQueue.forEach(a => store.dispatch(a)); // flush queue + actionQueue = []; + } + + const asyncDispatch = (asyncAction) => { + actionQueue = actionQueue.concat([asyncAction]); + + if (syncActivityFinished) { + flushQueue(); + } + } + + const actionWithAsyncDispatch = Object.assign({}, action, { asyncDispatch }); + + next(actionWithAsyncDispatch); + syncActivityFinished = true; + flushQueue(); +}; + +export default asyncDispatchMiddleware; diff --git a/.internal/wui/src/reducers/middlewares/promiseMiddleware.js b/.internal/wui/src/reducers/middlewares/promiseMiddleware.js new file mode 100644 index 000000000..d63ca1fae --- /dev/null +++ b/.internal/wui/src/reducers/middlewares/promiseMiddleware.js @@ -0,0 +1,29 @@ +import { API_STATUS } from '../../constants'; + +const promiseMiddleware = () => { + return next => action => { + const { promise, type, label, ...rest } = action; + + if (!promise) { + return next(action); + } + + const SUCCESS = type + `_${API_STATUS.SUCCESS}`; + const PENDING = type + `_${API_STATUS.PENDING}`; + const FAILURE = type + `_${API_STATUS.FAILURE}`; + + next({ ...rest, type: PENDING, label }); + + return promise.then(res => { + next({ ...rest, res, type: SUCCESS, label }); + + return true; + }).catch(error => { + next({ ...rest, error, type: FAILURE, label }); + + return false; + }); + }; +} + +export default promiseMiddleware; diff --git a/.internal/wui/src/reducers/updateSelectedFilterTagsReducer.js b/.internal/wui/src/reducers/updateSelectedFilterTagsReducer.js new file mode 100644 index 000000000..33cafaf4e --- /dev/null +++ b/.internal/wui/src/reducers/updateSelectedFilterTagsReducer.js @@ -0,0 +1,43 @@ +import { + REMOVE_FROM_FILTER_HIDE_LIST, + ADD_TO_FILTER_HIDE_LIST +} from '../actions/updateFilterTags.action'; + +const defaultState = { + hideServiceTags: [] +}; + +const reducerHandler = (state = defaultState, action) => { + const newState = JSON.parse(JSON.stringify(state)); + switch (action.type) { + case `${REMOVE_FROM_FILTER_HIDE_LIST}`: + newState.hideServiceTags.splice(newState.hideServiceTags.indexOf(action.data.filterTag), 1); + return { + ...state, + ...newState + }; + + case `${ADD_TO_FILTER_HIDE_LIST}`: + if (newState.hideServiceTags.indexOf(action.data.filterTag) < 0) { + newState.hideServiceTags.push(action.data.filterTag); + } else { + console.warn(`Tag '${action.data.filterTag}' is already added!`); + return { + ...state + } + } + return { + ...state, + ...newState + }; + + default: + return state; + } +}; + +export default reducerHandler; + +export const updateHideServiceTagsSelector = (state) => { + return state.hideServiceTags +}; diff --git a/.internal/wui/src/reducers/updateSelectedServicesReducer.js b/.internal/wui/src/reducers/updateSelectedServicesReducer.js new file mode 100644 index 000000000..35d0ef4d5 --- /dev/null +++ b/.internal/wui/src/reducers/updateSelectedServicesReducer.js @@ -0,0 +1,51 @@ +import { + REMOVE_FROM_SELECTED_SERVICES, + ADD_TO_SELECTED_SERVICES, + CLEAR_ALL_SELECTED_SERVICES +} from '../actions/updateSelectedServices.action'; + +const defaultState = { + selectedServices: [] +}; + +const reducerHandler = (state = defaultState, action) => { + const newState = JSON.parse(JSON.stringify(state)); + switch (action.type) { + case `${REMOVE_FROM_SELECTED_SERVICES}`: + newState.selectedServices.splice(newState.selectedServices.indexOf(action.data.serviceName), 1); + return { + ...state, + ...newState + } + + case `${ADD_TO_SELECTED_SERVICES}`: + if (newState.selectedServices.indexOf(action.data.serviceName) < 0) { + newState.selectedServices.push(action.data.serviceName); + } else { + console.warn(`Service '${action.data.serviceName}' is already added!`); + return { + ...state + } + } + return { + ...state, + ...newState + } + + case `${CLEAR_ALL_SELECTED_SERVICES}`: + newState.selectedServices = []; + return { + ...state, + ...newState + } + + default: + return state + } +}; + +export default reducerHandler; + +export const updateSelectedServicesSelector = (state) => { + return state.selectedServices +}; diff --git a/.internal/wui/src/router.jsx b/.internal/wui/src/router.jsx new file mode 100644 index 000000000..d4be1df2e --- /dev/null +++ b/.internal/wui/src/router.jsx @@ -0,0 +1,48 @@ +import React, { Fragment } from "react"; +import { + BrowserRouter as Router, + Switch, + Redirect, + Route +} from "react-router-dom"; +import Build from './pages/mainBuild' +import NotFound from './pages/notFound' +import BuildHistory from './pages/buildHistory' +import Scripts from './pages/scripts' +import Box from '@material-ui/core/Box'; +import Help from './pages/help' +import Sidebar from './features/Sidebar' + +export default function RouteWrapper() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/.internal/wui/src/services/builds.js b/.internal/wui/src/services/builds.js new file mode 100644 index 000000000..9cbdd507c --- /dev/null +++ b/.internal/wui/src/services/builds.js @@ -0,0 +1,228 @@ +import config from '../config'; + +const getBuildHistoryList = () => { + return new Promise((resolve, reject) => { + try { + return fetch(`${config.apiProtocol}${config.apiUrl}:${config.apiPort}/build/list`).then((response) => { + return response.json().then((data) => { + return resolve(data); + }).catch((err) => { + console.error('getBuildHistoryList: error parsing JSON response:'); + console.error(response); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + }); + }).catch((err) => { + console.error('getBuildHistoryList: error communicating with API.'); + console.error(err); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + }); + } catch (err) { + console.error('getBuildHistoryList: an unhandled error occured'); + console.error(err); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + } + }); +}; + +const getBuildIssues = ({ selectedServices, configurations }) => { + return new Promise((resolve, reject) => { + try { + if (!Array.isArray(selectedServices)) { + console.error('getBuildIssues: selectedServices is not an array'); + return reject('getBuildIssues: selectedServices is not an array'); + } + const bodyObject = { + buildOptions: { + selectedServices, + configurations + } + } + + return fetch( + `${config.apiProtocol}${config.apiUrl}:${config.apiPort}/build/dryrun`, + { + method: 'POST', + cache: 'no-cache', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(bodyObject) + }).then((response) => { + return response.json().then((data) => { + return resolve(data); + }).catch((err) => { + console.error('getBuildIssues: error parsing JSON response:'); + console.error(response); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + }); + }).catch((err) => { + console.error('getBuildIssues: error communicating with API.'); + console.error(err); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + }); + } catch (err) { + console.error('getBuildIssues: an unhandled error occured'); + console.error(err); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + } + }); +}; + +const createAndBuildStack = ({ selectedServices, configurations }) => { + return new Promise((resolve, reject) => { + try { + if (!Array.isArray(selectedServices)) { + console.error('createAndBuildStack: selectedServices is not an array'); + return reject('createAndBuildStack: selectedServices is not an array'); + } + const bodyObject = { + buildOptions: { + selectedServices, + configurations + } + } + + return fetch( + `${config.apiProtocol}${config.apiUrl}:${config.apiPort}/build/save`, + { + method: 'POST', + cache: 'no-cache', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(bodyObject) + }).then((response) => { + return response.json().then((data) => { + if (response.status > 399) { + return reject(data); + } + return resolve(data); + }).catch((err) => { + console.error('createAndBuildStack: error parsing JSON response:'); + console.error(response); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + }); + }).catch((err) => { + console.error('createAndBuildStack: error communicating with API.'); + console.error(err); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + }); + } catch (err) { + console.error('createAndBuildStack: an unhandled error occured'); + console.error(err); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + } + }); +}; + +const deleteBuild = ({ build }) => { + return new Promise((resolve, reject) => { + try { + if (build.length > 20) { + console.error('deleteBuild: Invalid build name. Ensure length is less than 20.'); + return reject('deleteBuild: Invalid build name. Ensure length is less than 20.'); + } + return fetch( + `${config.apiProtocol}${config.apiUrl}:${config.apiPort}/build/delete/${build}`, + { + method: 'POST', + cache: 'no-cache', + headers: { + 'Content-Type': 'application/json' + } + }).then((response) => { + return response.json().then((data) => { + if (response.status > 399) { + return reject(data); + } + return resolve(data); + }).catch((err) => { + console.error('deleteBuild: error parsing JSON response:'); + console.error(response); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + }); + }).catch((err) => { + console.error('deleteBuild: error communicating with API.'); + console.error(err); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + }); + } catch (err) { + console.error('deleteBuild: an unhandled error occured'); + console.error(err); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + } + }); +}; + +const getBuildFile = ({ build, type }) => { + return new Promise((resolve, reject) => { + try { + return fetch(`${config.apiProtocol}${config.apiUrl}:${config.apiPort}/build/get/${build}/${type}`).then((response) => { + return response.text().then((data) => { + return resolve(data); + }).catch((err) => { + console.error('getBuildDockerComposeFile: error getting text response:'); + console.error(response); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + }); + }).catch((err) => { + console.error('getBuildDockerComposeFile: error communicating with API.'); + console.error(err); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + }); + } catch (err) { + console.error('getBuildDockerComposeFile: an unhandled error occured'); + console.error(err); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + } + }); +}; + +const downloadBuild = ({ build, type, linkRef }) => { + return new Promise((resolve, reject) => { + try { + return fetch( + `${config.apiProtocol}${config.apiUrl}:${config.apiPort}/build/get/${build}/${type}`, + { + cache: 'no-cache' + }).then((response) => { + return response.blob().then((data) => { + try { + const href = window.URL.createObjectURL(data); + const a = linkRef.current; + a.download = `${build}.${type}`; + a.href = href; + a.click(); + a.href = ''; + } catch (err) { + console.error('downloadBuild: error creating link for blob download:'); + console.error(err) + } + + return resolve(data); + }).catch((err) => { + console.error('downloadBuild: error parsing text response:'); + console.error(response); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + }); + }).catch((err) => { + console.error('downloadBuild: error communicating with API.'); + console.error(err); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + }); + } catch (err) { + console.error('downloadBuild: an unhandled error occured'); + console.error(err); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + } + }); +}; + +export { + downloadBuild, + getBuildHistoryList, + getBuildIssues, + createAndBuildStack, + getBuildFile, + deleteBuild +}; diff --git a/.internal/wui/src/services/configs.js b/.internal/wui/src/services/configs.js new file mode 100644 index 000000000..6eb811b23 --- /dev/null +++ b/.internal/wui/src/services/configs.js @@ -0,0 +1,141 @@ +import config from '../config'; + +const getServiceMetadata = (serviceName) => { + return new Promise((resolve, reject) => { + try { + if (typeof serviceName !== 'string' || !serviceName) { + console.error('getServiceMetadata: invalid serviceName: ', serviceName); + console.trace(); + return reject('getServiceMetadata: invalid serviceName: ', serviceName); + } + + return fetch(`${config.apiProtocol}${config.apiUrl}:${config.apiPort}/config/${serviceName}/meta`).then((response) => { + return response.json().then((data) => { + return resolve(data); + }).catch((err) => { + console.error('getServiceMetadata: error parsing JSON response:'); + console.error(response); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + }); + }).catch((err) => { + console.error('getServiceMetadata: error communicating with API.'); + console.error(err); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + }); + } catch (err) { + console.error('getServiceMetadata: an unhandled error occured'); + console.error(err); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + } + }); +}; + +const getAllServicesMetadata = () => { + return new Promise((resolve, reject) => { + try { + return fetch(`${config.apiProtocol}${config.apiUrl}:${config.apiPort}/config/meta`).then((response) => { + return response.json().then((data) => { + return resolve(data); + }).catch((err) => { + console.error('getAllServicesMetadata: error parsing JSON response:'); + console.error(response); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + }); + }).catch((err) => { + console.error('getAllServicesMetadata: error communicating with API.'); + console.error(err); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + }); + } catch (err) { + console.error('getAllServicesMetadata: an unhandled error occured'); + console.error(err); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + } + }); +}; + +const getServiceConfigOptions = (serviceName) => { + return new Promise((resolve, reject) => { + try { + if (typeof serviceName !== 'string' || !serviceName) { + console.error('getServiceConfigOptions: invalid serviceName: ', serviceName); + console.trace(); + return reject('getServiceConfigOptions: invalid serviceName: ', serviceName); + } + + return fetch(`${config.apiProtocol}${config.apiUrl}:${config.apiPort}/config/${serviceName}/options`).then((response) => { + return response.json().then((data) => { + return resolve(data); + }).catch((err) => { + console.error('getServiceConfigOptions: error parsing JSON response:'); + console.error(response); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + }); + }).catch((err) => { + console.error('getServiceConfigOptions: error communicating with API.'); + console.error(err); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + }); + } catch (err) { + console.error('getServiceConfigOptions: an unhandled error occured'); + console.error(err); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + } + }); +}; + +const getAllServicesConfigOptions = (serviceName) => { + return new Promise((resolve, reject) => { + try { + return fetch(`${config.apiProtocol}${config.apiUrl}:${config.apiPort}/config/options`).then((response) => { + return response.json().then((data) => { + return resolve(data); + }).catch((err) => { + console.error('getAllServicesConfigOptions: error parsing JSON response:'); + console.error(response); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + }); + }).catch((err) => { + console.error('getAllServicesConfigOptions: error communicating with API.'); + console.error(err); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + }); + } catch (err) { + console.error('getAllServicesConfigOptions: an unhandled error occured'); + console.error(err); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + } + }); +}; + +const getAllServicesConfigHelp = (serviceName) => { + return new Promise((resolve, reject) => { + try { + return fetch(`${config.apiProtocol}${config.apiUrl}:${config.apiPort}/config/help`).then((response) => { + return response.json().then((data) => { + return resolve(data); + }).catch((err) => { + console.error('getAllServicesConfigHelp: error parsing JSON response:'); + console.error(response); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + }); + }).catch((err) => { + console.error('getAllServicesConfigHelp: error communicating with API.'); + console.error(err); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + }); + } catch (err) { + console.error('getAllServicesConfigHelp: an unhandled error occured'); + console.error(err); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + } + }); +}; + +export { + getServiceMetadata, + getServiceConfigOptions, + getAllServicesMetadata, + getAllServicesConfigOptions, + getAllServicesConfigHelp +}; diff --git a/.internal/wui/src/services/templates.js b/.internal/wui/src/services/templates.js new file mode 100644 index 000000000..ed6aa4a89 --- /dev/null +++ b/.internal/wui/src/services/templates.js @@ -0,0 +1,134 @@ +import config from '../config'; + +const getServiceTemplatesList = () => { + return new Promise((resolve, reject) => { + try { + return fetch(`${config.apiProtocol}${config.apiUrl}:${config.apiPort}/templates/services/list`).then((response) => { + return response.json().then((data) => { + return resolve(data); + }).catch((err) => { + console.error('getServiceTemplatesList: error parsing JSON response:'); + console.error(response); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + }); + }).catch((err) => { + console.error('getServiceTemplatesList: error communicating with API.'); + console.error(err); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + }); + } catch (err) { + console.error('getServiceTemplatesList: an unhandled error occured'); + console.error(err); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + } + }); +}; + +const getServiceTemplates = () => { + return new Promise((resolve, reject) => { + try { + return fetch(`${config.apiProtocol}${config.apiUrl}:${config.apiPort}/templates/services/json`).then((response) => { + return response.json().then((data) => { + return resolve(data); + }).catch((err) => { + console.error('getServiceTemplates: error parsing JSON response:'); + console.error(response); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + }); + }).catch((err) => { + console.error('getServiceTemplates: error communicating with API.'); + console.error(err); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + }); + } catch (err) { + console.error('getServiceTemplates: an unhandled error occured'); + console.error(err); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + } + }); +}; + +const getNetworkTemplatesList = () => { + return new Promise((resolve, reject) => { + try { + return fetch(`${config.apiProtocol}${config.apiUrl}:${config.apiPort}/templates/networks/list`).then((response) => { + return response.json().then((data) => { + return resolve(data); + }).catch((err) => { + console.error('getServiceTemplatesList: error parsing JSON response:'); + console.error(response); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + }); + }).catch((err) => { + console.error('getServiceTemplatesList: error communicating with API.'); + console.error(err); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + }); + } catch (err) { + console.error('getServiceTemplatesList: an unhandled error occured'); + console.error(err); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + } + }); +}; + +const getScriptTemplate = ({ scriptName, options, linkRef }) => { + return new Promise((resolve, reject) => { + try { + return fetch( + `${config.apiProtocol}${config.apiUrl}:${config.apiPort}/templates/scripts/${linkRef ? 'download/' : ''}${scriptName}`, + { + method: 'POST', + cache: 'no-cache', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ options }) + }).then((response) => { + if (linkRef) { + return response.blob().then((data) => { + try { + const href = window.URL.createObjectURL(data); + const a = linkRef.current; + a.download = `${scriptName}${options.build ? '_' + options.build : ''}.sh`; + a.href = href; + a.click(); + a.href = ''; + } catch (err) { + console.error('getScriptTemplate: error creating link for blob download:'); + console.error(err) + } + + return resolve(data); + }).catch((err) => { + console.error('getScriptTemplate: error parsing text response:'); + console.error(response); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + }); + } + return response.text().then((data) => { + return resolve(data); + }).catch((err) => { + console.error('getScriptTemplate: error parsing text response:'); + console.error(response); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + }); + }).catch((err) => { + console.error('getScriptTemplate: error communicating with API.'); + console.error(err); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + }); + } catch (err) { + console.error('getScriptTemplate: an unhandled error occured'); + console.error(err); + return reject(JSON.stringify(err, Object.getOwnPropertyNames(err))); + } + }); +}; + +export { + getScriptTemplate, + getServiceTemplates, + getNetworkTemplatesList, + getServiceTemplatesList +}; diff --git a/.internal/wui/src/utils/buildOptionSync.js b/.internal/wui/src/utils/buildOptionSync.js new file mode 100644 index 000000000..95a0da787 --- /dev/null +++ b/.internal/wui/src/utils/buildOptionSync.js @@ -0,0 +1,111 @@ +const setBuildOptions = (newOptions) => { + buildOptionsInit(); + localStorage.setItem('buildOptions', JSON.stringify(newOptions)); +}; + +const getBuildOptions = () => { + try { + return JSON.parse(localStorage.getItem('buildOptions')) || {}; + } catch (err) { + console.error('Error getting build options', err); + } + + return {}; +}; + +const setServiceOptions = (serviceName, options) => { + buildOptionsInit(); + const currentBuildOptions = getBuildOptions(); + + currentBuildOptions.services[serviceName] = options; + + localStorage.setItem('buildOptions', JSON.stringify(currentBuildOptions)); +}; + +const buildOptionsInit = () => { + const currentBuildOptions = getBuildOptions(); + + if (!currentBuildOptions.services || typeof(currentBuildOptions.services) !== 'object') { + currentBuildOptions.services = {}; + } + + if (!currentBuildOptions.networks || typeof(currentBuildOptions.networks) !== 'object') { + currentBuildOptions.networks = {}; + } + + if (!currentBuildOptions.meta || typeof(currentBuildOptions.meta) !== 'object') { + currentBuildOptions.meta = {}; + } + + localStorage.setItem('buildOptions', JSON.stringify(currentBuildOptions)); +}; + +const setTemporaryBuildOptions = (newOptions) => { + buildOptionsInit(); + sessionStorage.setItem('unsavedBuildOptions', JSON.stringify(newOptions)); +}; + +const deleteTemporaryBuildOptions = () => { + sessionStorage.removeItem('unsavedBuildOptions'); +}; + +const getTemporaryBuildOptions = () => { + try { + return JSON.parse(sessionStorage.getItem('unsavedBuildOptions')) || {}; + } catch (err) { + console.error('Error getting build options', err); + } + + return {}; +}; + +const setTemporaryServiceOptions = (serviceName, options) => { + buildOptionsInit(); + const currentBuildOptions = getBuildOptions(); + + currentBuildOptions.services[serviceName] = options; + + sessionStorage.setItem('unsavedBuildOptions', JSON.stringify(currentBuildOptions)); +}; + +const setupTemporaryBuildOptions = () => { + setTemporaryBuildOptions(getBuildOptions()); +}; + +const saveTemporaryBuildOptions = () => { + setBuildOptions(getTemporaryBuildOptions()); +}; + +const getSelectedItems_services = () => { + try { + return JSON.parse(localStorage.getItem('selectedItems'))?.services || []; + } catch (err) { + console.error('Error getting build options', err); + } + + return {}; +}; + +const setSelectedItems_services = (newItems) => { + const currentSelected = getSelectedItems_services()?.services ?? false; + if (!Array.isArray(currentSelected)) { + localStorage.setItem('selectedItems', JSON.stringify({ services: [] })); + } + + localStorage.setItem('selectedItems', JSON.stringify({ services: newItems })); +}; + +module.exports = { + getBuildOptions, + setBuildOptions, + buildOptionsInit, + setServiceOptions, + setTemporaryBuildOptions, + getTemporaryBuildOptions, + setTemporaryServiceOptions, + setupTemporaryBuildOptions, + saveTemporaryBuildOptions, + deleteTemporaryBuildOptions, + setSelectedItems_services, + getSelectedItems_services +}; diff --git a/.internal/wui/src/utils/configOptionLoader.jsx b/.internal/wui/src/utils/configOptionLoader.jsx new file mode 100644 index 000000000..3e6736df5 --- /dev/null +++ b/.internal/wui/src/utils/configOptionLoader.jsx @@ -0,0 +1,78 @@ +import { Fragment } from 'react'; +import ServiceUiControls from '../features/serviceUiControls'; + +const getConfigComponents = (configOptions) => { + if (configOptions && typeof(configOptions) === 'object') { + return Object.keys(configOptions).map((configName) => { + switch(configName) { + case "serviceName": + return null; + + case "labeledPorts": + return ServiceUiControls.PortConfig + + case "networks": + if (configOptions[configName] === true) { // Check if set to true + return ServiceUiControls.NetworkConfig + } else { + return null; + } + + case "logging": + if (configOptions[configName] === true) { // Check if set to true + return ServiceUiControls.Logging + } else { + return null; + } + + case "volumes": + if (configOptions[configName] === true) { // Check if set to true + return ServiceUiControls.Volumes + } else { + return null; + } + + case "devices": + if (configOptions[configName] === true) { // Check if set to true + return ServiceUiControls.Devices + } else { + return null; + } + + case "modifyableEnvironment": + if (configOptions[configName].length > 0) { + return ServiceUiControls.Environment + } else { + return null; + } + + case "deconzSelectedDevice": + if (configOptions[configName] === true) { + return ServiceUiControls.DeconzDevices + } else { + return null; + } + + case "nodered_npmSelection": + if (Array.isArray(configOptions[configName]?.defaultOn) && Array.isArray(configOptions[configName]?.defaultOff)) { + return ServiceUiControls.NodeRedNpm + } else { + return null; + } + + default: + if (configOptions[configName]) { // Check if set + return () => (
Unknown Option {configName}
); + } else { + return null; + } + } + }).filter((ele) => { + return ele !== null; + }); + } + + return [Fragment]; +}; + +export default getConfigComponents; diff --git a/.internal/wui/src/utils/interpolate.js b/.internal/wui/src/utils/interpolate.js new file mode 100644 index 000000000..dba353c7d --- /dev/null +++ b/.internal/wui/src/utils/interpolate.js @@ -0,0 +1,6 @@ +const byName = (formatString, replacements) => Object.keys(replacements).reduce( + (result, name) => result.replace(`{$${name}}`, replacements[name]), + formatString || '' +); + +module.exports = { byName }; diff --git a/.internal/wui/src/utils/parsers.js b/.internal/wui/src/utils/parsers.js new file mode 100644 index 000000000..e4984170d --- /dev/null +++ b/.internal/wui/src/utils/parsers.js @@ -0,0 +1,122 @@ +const getExternalPort = (intExtStr) => { + if (typeof(intExtStr) === 'string') { + const splitted = intExtStr.split(':'); + if (splitted.length === 2) { + return splitted[0]; + } + } + + return intExtStr; +}; + +const getInternalPort = (intExtStr) => { + if (typeof(intExtStr) === 'string') { + const portSection = intExtStr.split('/'); // So that /TCP or /UDP is not returned. + const splitted = portSection[0].split(':'); + if (splitted.length === 2) { + return splitted[1]; + } + } + + return intExtStr; +}; + +const getPortProtocol = (intExtProtStr) => { + if (typeof(intExtProtStr) === 'string') { + const splitted = intExtProtStr.split('/'); + if (splitted.length === 2) { + return splitted[1]; + } + } + + return intExtProtStr; +}; + +const replaceExternalPort = (intExtStr, newExtPort) => { + if (typeof(intExtStr) === 'string' && (typeof(newExtPort) === 'string' || typeof(newExtPort) === 'number')) { + const intAndProtLoc = intExtStr.indexOf(':'); + if (intAndProtLoc > 0 && intAndProtLoc < 6) { + const portsWithoutExt = intExtStr.substring(intAndProtLoc, intExtStr.length); + return `${newExtPort}${portsWithoutExt}`; + } + } + + return intExtStr; +}; + +const getExternalVolume = (intExtStr) => { + if (typeof(intExtStr) === 'string') { + const splitted = intExtStr.split(':'); + if (splitted.length === 2) { + return splitted[0]; + } + } + + return intExtStr; +}; + +const getInternalVolume = (intExtStr) => { + if (typeof(intExtStr) === 'string') { + const splitted = intExtStr.split(':'); + if (splitted.length === 2) { + return splitted[1]; + } + } + + return intExtStr; +}; + +const replaceExternalVolume = (intExtStr, newExtVolume) => { + if (typeof(intExtStr) === 'string' && (typeof(newExtVolume) === 'string')) { + const intLoc = intExtStr.indexOf(':'); + const volumesWithoutExt = intExtStr.substring(intLoc, intExtStr.length); + return `${newExtVolume}${volumesWithoutExt}`; + } + + return intExtStr; +}; + +const getEnvironmentKey = (EnvKVStr) => { + if (typeof(EnvKVStr) === 'string') { + const splitted = EnvKVStr.split('='); + if (splitted.length === 2) { + return splitted[0]; + } + } + + return EnvKVStr; +}; + +const getEnvironmentValue = (EnvKVStr) => { + if (typeof(EnvKVStr) === 'string') { + const splitted = EnvKVStr.split('='); + if (splitted.length === 2) { + return splitted[1]; + } + } + + return EnvKVStr; +}; + +const replaceEnvironmentValue = (EnvKVStr, newEnvValue) => { + if (typeof(EnvKVStr) === 'string' && (typeof(newEnvValue) === 'string')) { + const intLoc = EnvKVStr.indexOf('='); + const EnvWithoutValue = EnvKVStr.substring(intLoc, EnvKVStr.length); + return `${EnvWithoutValue}${newEnvValue}`; + } + + return EnvKVStr; +}; + +module.exports = { + getExternalPort, + getInternalPort, + replaceExternalPort, + getPortProtocol, + getExternalVolume, + getInternalVolume, + replaceExternalVolume, + getEnvironmentKey, + getEnvironmentValue, + replaceEnvironmentValue +}; \ No newline at end of file diff --git a/.internal/wui/wui.dev.Dockerfile b/.internal/wui/wui.dev.Dockerfile new file mode 100644 index 000000000..b43925ffd --- /dev/null +++ b/.internal/wui/wui.dev.Dockerfile @@ -0,0 +1,10 @@ +FROM node:14 + +WORKDIR /usr/iotstack_wui + +# node_modules is ignored with this copy, as specified in .dockerignore +COPY ./wui ./ +RUN npm install + +EXPOSE 32777 +CMD [ "npm", "start" ] diff --git a/.native/hassio_supervisor.sh b/.native/hassio_supervisor.sh deleted file mode 100755 index 692c42beb..000000000 --- a/.native/hassio_supervisor.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/bin/bash - -echo " " -echo "Ensure that you have read the documentation on installing Hass.io before continuing." -echo "Not following the installation instructions may render you system to be unable to connect to the internet." -echo "Hass.io Documentation: " -echo " https://sensorsiot.github.io/IOTstack/Containers/Home-Assistant/" - -echo " " -sleep 1 - -read -r -n 1 -p "Press Y to continue, any other key to cancel " response; - -if [[ $response == "y" || $response == "Y" ]]; then - echo "Install requirements for Hass.io" - sudo apt install -y bash jq curl avahi-daemon dbus - hassio_machine=$(whiptail --title "Machine type" --menu \ - "Please select you device type" 20 78 12 -- \ - "raspberrypi4-64" " " \ - "raspberrypi4" " " \ - "raspberrypi3-64" " " \ - "raspberrypi3" " " \ - "raspberrypi2" " " \ - "qemux86" " " \ - "qemux86-64" " " \ - "qemuarm" " " \ - "qemuarm-64" " " \ - "orangepi-prime" " " \ - "odroid-xu" " " \ - "odroid-c2" " " \ - "intel-nuc" " " \ - "tinker" " " \ - 3>&1 1>&2 2>&3) - - if [ -n "$hassio_machine" ]; then - sudo systemctl disable ModemManager - sudo systemctl stop ModemManager - curl -sL "https://raw.githubusercontent.com/Kanga-Who/home-assistant/master/supervised-installer.sh" | sudo bash -s -- -m $hassio_machine - clear - exit 0 - else - clear - echo "No selection" - exit 4 - fi - clear - exit 3 -else - clear - exit 5 -fi \ No newline at end of file diff --git a/.templates/adguardhome/service.yml b/.templates/adguardhome/service.yml deleted file mode 100644 index 7f6f151a0..000000000 --- a/.templates/adguardhome/service.yml +++ /dev/null @@ -1,35 +0,0 @@ -adguardhome: - container_name: adguardhome - image: adguard/adguardhome - restart: unless-stopped - environment: - - TZ=Etc/UTC - # enable host mode to activate DHCP server on ports 67/udp & 68/tcp+udp - # note that you must also disable all other ports if you enable host mode - # network_mode: host - ports: - # regular DNS - - "53:53/tcp" - - "53:53/udp" - # administration port (http) - # note: external and internal ports MUST be the same - # not active until defined via setup port - - "8089:8089/tcp" - # HTTPS/DNS-over-HTTPS - # - "443:443/tcp" - # DNS-over-QUIC - # - "784:784/udp" - # DNS-over-TLS - # - "853:853/tcp" - # setup (http) - # note: only active until port 8089 becomes active - - "3001:3000/tcp" - # DNSCrypt - # - "5443:5443/tcp" - # - "5443:5443/udp" - volumes: - - ./volumes/adguardhome/workdir:/opt/adguardhome/work - - ./volumes/adguardhome/confdir:/opt/adguardhome/conf - networks: - - iotstack_nw - - vpn_nw diff --git a/.templates/adminer/build.py b/.templates/adminer/build.py deleted file mode 100755 index 5a5fb79ad..000000000 --- a/.templates/adminer/build.py +++ /dev/null @@ -1,315 +0,0 @@ -#!/usr/bin/env python3 - -issues = {} # Returned issues dict -buildHooks = {} # Options, and others hooks -haltOnErrors = True - -# Main wrapper function. Required to make local vars work correctly -def main(): - import os - import time - import ruamel.yaml - import signal - import sys - from blessed import Terminal - - from deps.chars import specialChars, commonTopBorder, commonBottomBorder, commonEmptyLine, padText - from deps.consts import servicesDirectory, templatesDirectory - from deps.common_functions import getExternalPorts, getInternalPorts, checkPortConflicts, enterPortNumberWithWhiptail - - yaml = ruamel.yaml.YAML() - yaml.preserve_quotes = True - - global dockerComposeServicesYaml # The loaded memory YAML of all checked services - global toRun # Switch for which function to run when executed - global buildHooks # Where to place the options menu result - global currentServiceName # Name of the current service - global issues # Returned issues dict - global haltOnErrors # Turn on to allow erroring - global hideHelpText # Showing and hiding the help controls text - - try: # If not already set, then set it. - hideHelpText = hideHelpText - except: - hideHelpText = False - - documentationHint = 'https://sensorsiot.github.io/IOTstack/Containers/Adminer' - - # runtime vars - portConflicts = [] - - # This lets the menu know whether to put " >> Options " or not - # This function is REQUIRED. - def checkForOptionsHook(): - try: - buildHooks["options"] = callable(runOptionsMenu) - except: - buildHooks["options"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPreBuildHook(): - try: - buildHooks["preBuildHook"] = callable(preBuild) - except: - buildHooks["preBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPostBuildHook(): - try: - buildHooks["postBuildHook"] = callable(postBuild) - except: - buildHooks["postBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForRunChecksHook(): - try: - buildHooks["runChecksHook"] = callable(runChecks) - except: - buildHooks["runChecksHook"] = False - return buildHooks - return buildHooks - - # This service will not check anything unless this is set - # This function is optional, and will run each time the menu is rendered - def runChecks(): - checkForIssues() - return [] - - # This function is optional, and will run after the docker-compose.yml file is written to disk. - def postBuild(): - return True - - # This function is optional, and will run just before the build docker-compose.yml code. - def preBuild(): - return True - - # ##################################### - # Supporting functions below - # ##################################### - - def checkForIssues(): - for (index, serviceName) in enumerate(dockerComposeServicesYaml): - if not currentServiceName == serviceName: # Skip self - currentServicePorts = getExternalPorts(currentServiceName, dockerComposeServicesYaml) - portConflicts = checkPortConflicts(serviceName, currentServicePorts, dockerComposeServicesYaml) - if (len(portConflicts) > 0): - issues["portConflicts"] = portConflicts - - # ##################################### - # End Supporting functions - # ##################################### - - ############################ - # Menu Logic - ############################ - - global currentMenuItemIndex - global selectionInProgress - global menuNavigateDirection - global needsRender - - selectionInProgress = True - currentMenuItemIndex = 0 - menuNavigateDirection = 0 - needsRender = 1 - term = Terminal() - hotzoneLocation = [((term.height // 16) + 6), 0] - - def goBack(): - global selectionInProgress - global needsRender - selectionInProgress = False - needsRender = 1 - return True - - def enterPortNumberExec(): - # global term - global needsRender - global dockerComposeServicesYaml - externalPort = getExternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - internalPort = getInternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - newPortNumber = enterPortNumberWithWhiptail(term, dockerComposeServicesYaml, currentServiceName, hotzoneLocation, externalPort) - - if newPortNumber > 0: - dockerComposeServicesYaml[currentServiceName]["ports"][0] = "{newExtPort}:{oldIntPort}".format( - newExtPort = newPortNumber, - oldIntPort = internalPort - ) - createMenu() - needsRender = 1 - - def onResize(sig, action): - global adminerBuildOptions - global currentMenuItemIndex - mainRender(1, adminerBuildOptions, currentMenuItemIndex) - - adminerBuildOptions = [] - - def createMenu(): - global adminerBuildOptions - try: - adminerBuildOptions = [] - portNumber = getExternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - adminerBuildOptions.append([ - "Change external WUI Port Number from: {port}".format(port=portNumber), - enterPortNumberExec - ]) - except: # Error getting port - pass - adminerBuildOptions.append(["Go back", goBack]) - - def runOptionsMenu(): - createMenu() - menuEntryPoint() - return True - - def renderHotZone(term, menu, selection, hotzoneLocation): - lineLengthAtTextStart = 71 - print(term.move(hotzoneLocation[0], hotzoneLocation[1])) - for (index, menuItem) in enumerate(menu): - toPrint = "" - if index == selection: - toPrint += ('{bv} -> {t.blue_on_green} {title} {t.normal} <-'.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) - else: - toPrint += ('{bv} {t.normal} {title} '.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) - - for i in range(lineLengthAtTextStart - len(menuItem[0])): - toPrint += " " - - toPrint += "{bv}".format(bv=specialChars[renderMode]["borderVertical"]) - - toPrint = term.center(toPrint) - - print(toPrint) - - def mainRender(needsRender, menu, selection): - term = Terminal() - - if needsRender == 1: - print(term.clear()) - print(term.move_y(term.height // 16)) - print(term.black_on_cornsilk4(term.center('IOTstack Adminer Options'))) - print("") - print(term.center(commonTopBorder(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Select Option to configure {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - - if needsRender >= 1: - renderHotZone(term, menu, selection, hotzoneLocation) - - if needsRender == 1: - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - if not hideHelpText: - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Enter] to run command or save input {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Escape] to go back to build stack menu {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - if len(documentationHint) > 1: - if len(documentationHint) > 56: - documentationAndPadding = padText(documentationHint, 71) - print(term.center("{bv} Documentation: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} {dap} {bv}".format(bv=specialChars[renderMode]["borderVertical"], dap=documentationAndPadding))) - else: - documentationAndPadding = padText(documentationHint, 56) - print(term.center("{bv} Documentation: {dap} {bv}".format(bv=specialChars[renderMode]["borderVertical"], dap=documentationAndPadding))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonBottomBorder(renderMode))) - - def runSelection(selection): - import types - global adminerBuildOptions - if len(adminerBuildOptions[selection]) > 1 and isinstance(adminerBuildOptions[selection][1], types.FunctionType): - adminerBuildOptions[selection][1]() - else: - print(term.green_reverse('IOTstack Error: No function assigned to menu item: "{}"'.format(nodeRedBuildOptions[selection][0]))) - - def isMenuItemSelectable(menu, index): - if len(menu) > index: - if len(menu[index]) > 2: - if menu[index][2]["skip"] == True: - return False - return True - - def menuEntryPoint(): - # These need to be reglobalised due to eval() - global currentMenuItemIndex - global selectionInProgress - global menuNavigateDirection - global needsRender - global hideHelpText - global adminerBuildOptions - term = Terminal() - with term.fullscreen(): - menuNavigateDirection = 0 - mainRender(needsRender, adminerBuildOptions, currentMenuItemIndex) - selectionInProgress = True - with term.cbreak(): - while selectionInProgress: - menuNavigateDirection = 0 - - if needsRender: # Only rerender when changed to prevent flickering - mainRender(needsRender, adminerBuildOptions, currentMenuItemIndex) - needsRender = 0 - key = term.inkey(esc_delay=0.05) - if key.is_sequence: - if key.name == 'KEY_TAB': - menuNavigateDirection += 1 - if key.name == 'KEY_DOWN': - menuNavigateDirection += 1 - if key.name == 'KEY_UP': - menuNavigateDirection -= 1 - if key.name == 'KEY_LEFT': - goBack() - if key.name == 'KEY_ENTER': - runSelection(currentMenuItemIndex) - if key.name == 'KEY_ESCAPE': - return True - elif key: - if key == 'h': # H pressed - if hideHelpText: - hideHelpText = False - else: - hideHelpText = True - mainRender(1, adminerBuildOptions, currentMenuItemIndex) - - if menuNavigateDirection != 0: # If a direction was pressed, find next selectable item - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(adminerBuildOptions) - needsRender = 2 - - while not isMenuItemSelectable(adminerBuildOptions, currentMenuItemIndex): - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(adminerBuildOptions) - return True - - #################### - # End menu section - #################### - - - if haltOnErrors: - eval(toRun)() - else: - try: - eval(toRun)() - except: - pass - -# This check isn't required, but placed here for debugging purposes -global currentServiceName # Name of the current service -if currentServiceName == 'adminer': - main() -else: - print("Error. '{}' Tried to run 'adminer' config".format(currentServiceName)) diff --git a/.templates/blynk_server/Dockerfile b/.templates/blynk_server/Dockerfile deleted file mode 100644 index bc9b76058..000000000 --- a/.templates/blynk_server/Dockerfile +++ /dev/null @@ -1,25 +0,0 @@ -FROM adoptopenjdk/openjdk14 -MAINTAINER 877dev <877dev@gmail.com> - -#RUN apt-get update -#RUN apt-get install -y apt-utils -#RUN apt-get install -y default-jdk curl - -ENV BLYNK_SERVER_VERSION 0.41.16 -RUN mkdir /blynk -RUN curl -L https://github.com/blynkkk/blynk-server/releases/download/v${BLYNK_SERVER_VERSION}/server-${BLYNK_SERVER_VERSION}.jar > /blynk/server.jar - -# Create data folder. To persist data, map a volume to /data -RUN mkdir /data - -# Create configuration folder. To persist data, map a file to /config/server.properties -RUN mkdir /config && touch /config/server.properties -VOLUME ["/config", "/data/backup"] - -# IP port listing: -# 8080: Hardware without ssl/tls support -# 9443: Blynk app, https, web sockets, admin port -EXPOSE 8080 9443 - -WORKDIR /data -ENTRYPOINT ["java", "-jar", "/blynk/server.jar", "-dataFolder", "/data", "-serverConfig", "/config/server.properties", "-mailConfig", "/config/mail.properties"] diff --git a/.templates/blynk_server/build.py b/.templates/blynk_server/build.py deleted file mode 100755 index 0583686e7..000000000 --- a/.templates/blynk_server/build.py +++ /dev/null @@ -1,328 +0,0 @@ -#!/usr/bin/env python3 - -issues = {} # Returned issues dict -buildHooks = {} # Options, and others hooks -haltOnErrors = True - -# Main wrapper function. Required to make local vars work correctly -def main(): - import os - import time - import ruamel.yaml - import signal - import shutil - import sys - from blessed import Terminal - - from deps.chars import specialChars, commonTopBorder, commonBottomBorder, commonEmptyLine, padText - from deps.consts import servicesDirectory, templatesDirectory - from deps.common_functions import getExternalPorts, getInternalPorts, checkPortConflicts, enterPortNumberWithWhiptail - - yaml = ruamel.yaml.YAML() - yaml.preserve_quotes = True - - global dockerComposeServicesYaml # The loaded memory YAML of all checked services - global toRun # Switch for which function to run when executed - global buildHooks # Where to place the options menu result - global currentServiceName # Name of the current service - global issues # Returned issues dict - global haltOnErrors # Turn on to allow erroring - global hideHelpText # Showing and hiding the help controls text - global serviceService - - serviceService = servicesDirectory + currentServiceName - serviceTemplate = templatesDirectory + currentServiceName - - try: # If not already set, then set it. - hideHelpText = hideHelpText - except: - hideHelpText = False - - documentationHint = 'https://sensorsiot.github.io/IOTstack/Containers/Blynk_server' - - # runtime vars - portConflicts = [] - - # This lets the menu know whether to put " >> Options " or not - # This function is REQUIRED. - def checkForOptionsHook(): - try: - buildHooks["options"] = callable(runOptionsMenu) - except: - buildHooks["options"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPreBuildHook(): - try: - buildHooks["preBuildHook"] = callable(preBuild) - except: - buildHooks["preBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPostBuildHook(): - try: - buildHooks["postBuildHook"] = callable(postBuild) - except: - buildHooks["postBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForRunChecksHook(): - try: - buildHooks["runChecksHook"] = callable(runChecks) - except: - buildHooks["runChecksHook"] = False - return buildHooks - return buildHooks - - # This service will not check anything unless this is set - # This function is optional, and will run each time the menu is rendered - def runChecks(): - checkForIssues() - return [] - - # This function is optional, and will run after the docker-compose.yml file is written to disk. - def postBuild(): - return True - - # This function is optional, and will run just before the build docker-compose.yml code. - def preBuild(): - # Setup service directory - if not os.path.exists(serviceService): - os.makedirs(serviceService, exist_ok=True) - - # Files copy - shutil.copy(r'%s/Dockerfile' % serviceTemplate, r'%s/Dockerfile' % serviceService) - return True - - - # ##################################### - # Supporting functions below - # ##################################### - - def checkForIssues(): - for (index, serviceName) in enumerate(dockerComposeServicesYaml): - if not currentServiceName == serviceName: # Skip self - currentServicePorts = getExternalPorts(currentServiceName, dockerComposeServicesYaml) - portConflicts = checkPortConflicts(serviceName, currentServicePorts, dockerComposeServicesYaml) - if (len(portConflicts) > 0): - issues["portConflicts"] = portConflicts - - # ##################################### - # End Supporting functions - # ##################################### - - ############################ - # Menu Logic - ############################ - - global currentMenuItemIndex - global selectionInProgress - global menuNavigateDirection - global needsRender - - selectionInProgress = True - currentMenuItemIndex = 0 - menuNavigateDirection = 0 - needsRender = 1 - term = Terminal() - hotzoneLocation = [((term.height // 16) + 6), 0] - - def goBack(): - global selectionInProgress - global needsRender - selectionInProgress = False - needsRender = 1 - return True - - def enterPortNumberExec(): - # global term - global needsRender - global dockerComposeServicesYaml - externalPort = getExternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - internalPort = getInternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - newPortNumber = enterPortNumberWithWhiptail(term, dockerComposeServicesYaml, currentServiceName, hotzoneLocation, externalPort) - - if newPortNumber > 0: - dockerComposeServicesYaml[currentServiceName]["ports"][0] = "{newExtPort}:{oldIntPort}".format( - newExtPort = newPortNumber, - oldIntPort = internalPort - ) - createMenu() - needsRender = 1 - - def onResize(sig, action): - global blynkServerBuildOptions - global currentMenuItemIndex - mainRender(1, blynkServerBuildOptions, currentMenuItemIndex) - - blynkServerBuildOptions = [] - - def createMenu(): - global blynkServerBuildOptions - try: - blynkServerBuildOptions = [] - portNumber = getExternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - blynkServerBuildOptions.append([ - "Change external WUI Port Number from: {port}".format(port=portNumber), - enterPortNumberExec - ]) - except: # Error getting port - pass - blynkServerBuildOptions.append(["Go back", goBack]) - - def runOptionsMenu(): - createMenu() - menuEntryPoint() - return True - - def renderHotZone(term, menu, selection, hotzoneLocation): - lineLengthAtTextStart = 71 - print(term.move(hotzoneLocation[0], hotzoneLocation[1])) - for (index, menuItem) in enumerate(menu): - toPrint = "" - if index == selection: - toPrint += ('{bv} -> {t.blue_on_green} {title} {t.normal} <-'.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) - else: - toPrint += ('{bv} {t.normal} {title} '.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) - - for i in range(lineLengthAtTextStart - len(menuItem[0])): - toPrint += " " - - toPrint += "{bv}".format(bv=specialChars[renderMode]["borderVertical"]) - - toPrint = term.center(toPrint) - - print(toPrint) - - def mainRender(needsRender, menu, selection): - term = Terminal() - - if needsRender == 1: - print(term.clear()) - print(term.move_y(term.height // 16)) - print(term.black_on_cornsilk4(term.center('IOTstack Blynk Server Options'))) - print("") - print(term.center(commonTopBorder(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Select Option to configure {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - - if needsRender >= 1: - renderHotZone(term, menu, selection, hotzoneLocation) - - if needsRender == 1: - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - if not hideHelpText: - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Enter] to run command or save input {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Escape] to go back to build stack menu {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - if len(documentationHint) > 1: - if len(documentationHint) > 56: - documentationAndPadding = padText(documentationHint, 71) - print(term.center("{bv} Documentation: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} {dap} {bv}".format(bv=specialChars[renderMode]["borderVertical"], dap=documentationAndPadding))) - else: - documentationAndPadding = padText(documentationHint, 56) - print(term.center("{bv} Documentation: {dap} {bv}".format(bv=specialChars[renderMode]["borderVertical"], dap=documentationAndPadding))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonBottomBorder(renderMode))) - - def runSelection(selection): - import types - global blynkServerBuildOptions - if len(blynkServerBuildOptions[selection]) > 1 and isinstance(blynkServerBuildOptions[selection][1], types.FunctionType): - blynkServerBuildOptions[selection][1]() - else: - print(term.green_reverse('IOTstack Error: No function assigned to menu item: "{}"'.format(nodeRedBuildOptions[selection][0]))) - - def isMenuItemSelectable(menu, index): - if len(menu) > index: - if len(menu[index]) > 2: - if menu[index][2]["skip"] == True: - return False - return True - - def menuEntryPoint(): - # These need to be reglobalised due to eval() - global currentMenuItemIndex - global selectionInProgress - global menuNavigateDirection - global needsRender - global hideHelpText - global blynkServerBuildOptions - term = Terminal() - with term.fullscreen(): - menuNavigateDirection = 0 - mainRender(needsRender, blynkServerBuildOptions, currentMenuItemIndex) - selectionInProgress = True - with term.cbreak(): - while selectionInProgress: - menuNavigateDirection = 0 - - if needsRender: # Only rerender when changed to prevent flickering - mainRender(needsRender, blynkServerBuildOptions, currentMenuItemIndex) - needsRender = 0 - - key = term.inkey(esc_delay=0.05) - if key.is_sequence: - if key.name == 'KEY_TAB': - menuNavigateDirection += 1 - if key.name == 'KEY_DOWN': - menuNavigateDirection += 1 - if key.name == 'KEY_UP': - menuNavigateDirection -= 1 - if key.name == 'KEY_LEFT': - goBack() - if key.name == 'KEY_ENTER': - runSelection(currentMenuItemIndex) - if key.name == 'KEY_ESCAPE': - return True - elif key: - if key == 'h': # H pressed - if hideHelpText: - hideHelpText = False - else: - hideHelpText = True - mainRender(1, blynkServerBuildOptions, currentMenuItemIndex) - - if menuNavigateDirection != 0: # If a direction was pressed, find next selectable item - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(blynkServerBuildOptions) - needsRender = 2 - - while not isMenuItemSelectable(blynkServerBuildOptions, currentMenuItemIndex): - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(blynkServerBuildOptions) - return True - - #################### - # End menu section - #################### - - - if haltOnErrors: - eval(toRun)() - else: - try: - eval(toRun)() - except: - pass - -# This check isn't required, but placed here for debugging purposes -global currentServiceName # Name of the current service -if currentServiceName == 'blynk_server': - main() -else: - print("Error. '{}' Tried to run 'blynk_server' config".format(currentServiceName)) diff --git a/.templates/blynk_server/directoryfix.sh b/.templates/blynk_server/directoryfix.sh deleted file mode 100644 index ff3d40b19..000000000 --- a/.templates/blynk_server/directoryfix.sh +++ /dev/null @@ -1,90 +0,0 @@ -#!/bin/bash - -# Create config files for Blynk custom server - -#current user -u=$(whoami) - -#Check if the config directory already exists: -if [ ! -d ./volumes/blynk_server/data/config ]; then - #Create the config directory - sudo mkdir -p ./volumes/blynk_server/data/config - - #Create the properties files: - #cd ~/IOTstack/volumes/blynk_server/data/config - sudo touch ./volumes/blynk_server/data/config/server.properties - sudo touch ./volumes/blynk_server/data/config/mail.properties - - #Give permissions: - sudo chown -R $u:$u ./volumes/blynk_server/data/config - - #Populate the server.properties file: - sudo echo "hardware.mqtt.port=8440 - http.port=8080 - force.port.80.for.csv=false - force.port.80.for.redirect=true - https.port=9443 - data.folder=./data - logs.folder=./logs - log.level=info - user.devices.limit=10 - user.tags.limit=100 - user.dashboard.max.limit=100 - user.widget.max.size.limit=20 - user.message.quota.limit=100 - notifications.queue.limit=2000 - blocking.processor.thread.pool.limit=6 - notifications.frequency.user.quota.limit=5 - webhooks.frequency.user.quota.limit=1000 - webhooks.response.size.limit=96 - user.profile.max.size=128 - terminal.strings.pool.size=25 - map.strings.pool.size=25 - lcd.strings.pool.size=6 - table.rows.pool.size=100 - profile.save.worker.period=60000 - stats.print.worker.period=60000 - web.request.max.size=524288 - csv.export.data.points.max=43200 - hard.socket.idle.timeout=10 - enable.db=false - enable.raw.db.data.store=false - async.logger.ring.buffer.size=2048 - allow.reading.widget.without.active.app=false - allow.store.ip=true - initial.energy=1000000 - admin.rootPath=/admin - restore.host=blynk-cloud.com - product.name=Blynk - admin.email=admin@blynk.cc - admin.pass=admin - " > ./volumes/blynk_server/data/config/server.properties - - #Populate the mail.properties file: - sudo echo "mail.smtp.auth=true - mail.smtp.starttls.enable=true - mail.smtp.host=smtp.gmail.com - mail.smtp.port=587 - mail.smtp.username=YOUR_GMAIL@gmail.com - mail.smtp.password=YOUR_GMAIL_APP_PASSWORD - mail.smtp.connectiontimeout=30000 - mail.smtp.timeout=120000 - " > ./volumes/blynk_server/data/config/mail.properties - - #Information messages: - echo "Sample properties files created in ~/IOTstack/volumes/blynk_server/data/config" - echo "Make sure you edit the files with your details, and restart the container to take effect." - - - -fi - - - - - - - - - - diff --git a/.templates/deconz/build.py b/.templates/deconz/build.py deleted file mode 100755 index 57e3a53d0..000000000 --- a/.templates/deconz/build.py +++ /dev/null @@ -1,480 +0,0 @@ -#!/usr/bin/env python3 - -issues = {} # Returned issues dict -buildHooks = {} # Options, and others hooks -haltOnErrors = True - -# Main wrapper function. Required to make local vars work correctly -def main(): - import os - import time - import ruamel.yaml - import signal - import sys - from blessed import Terminal - - from deps.chars import specialChars, commonTopBorder, commonBottomBorder, commonEmptyLine - from deps.consts import servicesDirectory, templatesDirectory, buildSettingsFileName, buildCache, servicesFileName - from deps.common_functions import getExternalPorts, getInternalPorts, checkPortConflicts, enterPortNumberWithWhiptail, generateRandomString - - yaml = ruamel.yaml.YAML() - yaml.preserve_quotes = True - - global dockerComposeServicesYaml # The loaded memory YAML of all checked services - global toRun # Switch for which function to run when executed - global buildHooks # Where to place the options menu result - global currentServiceName # Name of the current service - global issues # Returned issues dict - global haltOnErrors # Turn on to allow erroring - global hideHelpText # Showing and hiding the help controls text - global serviceService - global serviceTemplate - global hasRebuiltHardwareSelection - - serviceService = servicesDirectory + currentServiceName - serviceTemplate = templatesDirectory + currentServiceName - buildSettings = serviceService + buildSettingsFileName - - hasRebuiltHardwareSelection = False - - try: # If not already set, then set it. - hideHelpText = hideHelpText - except: - hideHelpText = False - - # runtime vars - portConflicts = [] - - # This lets the menu know whether to put " >> Options " or not - # This function is REQUIRED. - def checkForOptionsHook(): - try: - buildHooks["options"] = callable(runOptionsMenu) - except: - buildHooks["options"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPreBuildHook(): - try: - buildHooks["preBuildHook"] = callable(preBuild) - except: - buildHooks["preBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPostBuildHook(): - try: - buildHooks["postBuildHook"] = callable(postBuild) - except: - buildHooks["postBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForRunChecksHook(): - try: - buildHooks["runChecksHook"] = callable(runChecks) - except: - buildHooks["runChecksHook"] = False - return buildHooks - return buildHooks - - # This service will not check anything unless this is set - # This function is optional, and will run each time the menu is rendered - def runChecks(): - checkForIssues() - return [] - - # This function is optional, and will run after the docker-compose.yml file is written to disk. - def postBuild(): - return True - - # This function is optional, and will run just before the build docker-compose.yml code. - def preBuild(): - global dockerComposeServicesYaml - global currentServiceName - with open("{serviceDir}{buildSettings}".format(serviceDir=serviceService, buildSettings=buildSettingsFileName)) as objHardwareListFile: - deconzYamlBuildOptions = yaml.load(objHardwareListFile) - # Password randomisation - # Multi-service: - with open((r'%s/' % serviceTemplate) + servicesFileName) as objServiceFile: - serviceYamlTemplate = yaml.load(objServiceFile) - - oldBuildCache = {} - try: - with open(r'%s' % buildCache) as objBuildCache: - oldBuildCache = yaml.load(objBuildCache) - except: - pass - - buildCacheServices = {} - if "services" in oldBuildCache: - buildCacheServices = oldBuildCache["services"] - - if not os.path.exists(serviceService): - os.makedirs(serviceService, exist_ok=True) - - if os.path.exists(buildSettings): - # Password randomisation - if "databasePasswordOption" in deconzYamlBuildOptions: - if ( - deconzYamlBuildOptions["databasePasswordOption"] == "Randomise database password for this build" - or deconzYamlBuildOptions["databasePasswordOption"] == "Randomise database password every build" - or deconzYamlBuildOptions["databasePasswordOption"] == "Use default password for this build" - ): - if deconzYamlBuildOptions["databasePasswordOption"] == "Use default password for this build": - newPassword = "IOtSt4ckDec0nZ" - else: - newPassword = generateRandomString() - for (index, serviceName) in enumerate(serviceYamlTemplate): - dockerComposeServicesYaml[serviceName] = serviceYamlTemplate[serviceName] - if "environment" in serviceYamlTemplate[serviceName]: - for (envIndex, envName) in enumerate(serviceYamlTemplate[serviceName]["environment"]): - envName = envName.replace("%randomPassword%", newPassword) - dockerComposeServicesYaml[serviceName]["environment"][envIndex] = envName - - # Ensure you update the "Do nothing" and other 2 strings used for password settings in 'passwords.py' - if (deconzYamlBuildOptions["databasePasswordOption"] == "Randomise database password for this build"): - deconzYamlBuildOptions["databasePasswordOption"] = "Do nothing" - with open(buildSettings, 'w') as outputFile: - yaml.dump(deconzYamlBuildOptions, outputFile) - else: # Do nothing - don't change password - for (index, serviceName) in enumerate(buildCacheServices): - if serviceName in buildCacheServices: # Load service from cache if exists (to maintain password) - dockerComposeServicesYaml[serviceName] = buildCacheServices[serviceName] - else: - dockerComposeServicesYaml[serviceName] = serviceYamlTemplate[serviceName] - else: - print("Deconz Warning: Build settings file not found, using default password") - time.sleep(1) - newPassword = "IOtSt4ckDec0nZ" - for (index, serviceName) in enumerate(serviceYamlTemplate): - dockerComposeServicesYaml[serviceName] = serviceYamlTemplate[serviceName] - if "environment" in serviceYamlTemplate[serviceName]: - for (envIndex, envName) in enumerate(serviceYamlTemplate[serviceName]["environment"]): - envName = envName.replace("%randomPassword%", newPassword) - dockerComposeServicesYaml[serviceName]["environment"][envIndex] = envName - - deconzYamlBuildOptions["databasePasswordOption"] = "Do nothing" - with open(buildSettings, 'w') as outputFile: - yaml.dump(deconzYamlBuildOptions, outputFile) - - else: - print("Deconz Warning: Build settings file not found, using default password") - time.sleep(1) - newPassword = "IOtSt4ckDec0nZ" - for (index, serviceName) in enumerate(serviceYamlTemplate): - dockerComposeServicesYaml[serviceName] = serviceYamlTemplate[serviceName] - if "environment" in serviceYamlTemplate[serviceName]: - for (envIndex, envName) in enumerate(serviceYamlTemplate[serviceName]["environment"]): - envName = envName.replace("%randomPassword%", newPassword) - dockerComposeServicesYaml[serviceName]["environment"][envIndex] = envName - deconzYamlBuildOptions = { - "version": "1", - "application": "IOTstack", - "service": "Deconz", - "comment": "Deconz Build Options" - } - - deconzYamlBuildOptions["databasePasswordOption"] = "Do nothing" - with open(buildSettings, 'w') as outputFile: - yaml.dump(deconzYamlBuildOptions, outputFile) - - try: - if currentServiceName in dockerComposeServicesYaml: - dockerComposeServicesYaml[currentServiceName]["devices"] = deconzYamlBuildOptions["hardware"] - except Exception as err: - print("Error setting deconz hardware: ", err) - return False - - return True - - # ##################################### - # Supporting functions below - # ##################################### - - def checkForIssues(): - fileIssues = checkFiles() - if (len(fileIssues) > 0): - issues["fileIssues"] = fileIssues - for (index, serviceName) in enumerate(dockerComposeServicesYaml): - if not currentServiceName == serviceName: # Skip self - currentServicePorts = getExternalPorts(currentServiceName, dockerComposeServicesYaml) - portConflicts = checkPortConflicts(serviceName, currentServicePorts, dockerComposeServicesYaml) - if (len(portConflicts) > 0): - issues["portConflicts"] = portConflicts - - def checkFiles(): - fileIssues = [] - if not os.path.exists("{serviceDir}{buildSettings}".format(serviceDir=serviceService, buildSettings=buildSettingsFileName)): - fileIssues.append(serviceService + '/build_settings.yml does not exist. Select hardware in options to fix.') - return fileIssues - - # ##################################### - # End Supporting functions - # ##################################### - - ############################ - # Menu Logic - ############################ - - global currentMenuItemIndex - global selectionInProgress - global menuNavigateDirection - global needsRender - - selectionInProgress = True - currentMenuItemIndex = 0 - menuNavigateDirection = 0 - needsRender = 1 - term = Terminal() - hotzoneLocation = [((term.height // 16) + 6), 0] - - def goBack(): - global selectionInProgress - global needsRender - selectionInProgress = False - needsRender = 1 - return True - - def selectDeconzHardware(): - global needsRender - global hasRebuiltHardwareSelection - deconzSelectHardwareFilePath = "./.templates/deconz/select_hw.py" - with open(deconzSelectHardwareFilePath, "rb") as pythonDynamicImportFile: - code = compile(pythonDynamicImportFile.read(), deconzSelectHardwareFilePath, "exec") - # execGlobals = globals() - # execLocals = locals() - execGlobals = { - "currentServiceName": currentServiceName, - "renderMode": renderMode - } - execLocals = {} - screenActive = False - exec(code, execGlobals, execLocals) - signal.signal(signal.SIGWINCH, onResize) - try: - hasRebuiltHardwareSelection = execGlobals["hasRebuiltHardwareSelection"] - except: - hasRebuiltHardwareSelection = False - screenActive = True - needsRender = 1 - - def enterPortNumberExec(): - # global term - global needsRender - global dockerComposeServicesYaml - externalPort = getExternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - internalPort = getInternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - newPortNumber = enterPortNumberWithWhiptail(term, dockerComposeServicesYaml, currentServiceName, hotzoneLocation, externalPort) - - if newPortNumber > 0: - dockerComposeServicesYaml[currentServiceName]["ports"][0] = "{newExtPort}:{oldIntPort}".format( - newExtPort = newPortNumber, - oldIntPort = internalPort - ) - createMenu() - needsRender = 1 - - def setPasswordOptions(): - global needsRender - global hasRebuiltAddons - passwordOptionsMenuFilePath = "./.templates/{currentService}/passwords.py".format(currentService=currentServiceName) - with open(passwordOptionsMenuFilePath, "rb") as pythonDynamicImportFile: - code = compile(pythonDynamicImportFile.read(), passwordOptionsMenuFilePath, "exec") - execGlobals = { - "currentServiceName": currentServiceName, - "renderMode": renderMode - } - execLocals = {} - screenActive = False - exec(code, execGlobals, execLocals) - signal.signal(signal.SIGWINCH, onResize) - screenActive = True - needsRender = 1 - - def onResize(sig, action): - global deconzBuildOptions - global currentMenuItemIndex - mainRender(1, deconzBuildOptions, currentMenuItemIndex) - - deconzBuildOptions = [] - - def createMenu(): - global deconzBuildOptions - global serviceService - try: - deconzBuildOptions = [] - portNumber = getExternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - deconzBuildOptions.append([ - "Change external WUI Port Number from: {port}".format(port=portNumber), - enterPortNumberExec - ]) - except: # Error getting port - pass - - - if os.path.exists("{buildSettings}".format(buildSettings=buildSettings)): - deconzBuildOptions.insert(0, ["Change selected hardware", selectDeconzHardware]) - else: - deconzBuildOptions.insert(0, ["Select hardware", selectDeconzHardware]) - deconzBuildOptions.append([ - "DeConz Password Options", - setPasswordOptions - ]) - - deconzBuildOptions.append(["Go back", goBack]) - - def runOptionsMenu(): - createMenu() - menuEntryPoint() - return True - - def renderHotZone(term, menu, selection, hotzoneLocation): - lineLengthAtTextStart = 71 - print(term.move(hotzoneLocation[0], hotzoneLocation[1])) - for (index, menuItem) in enumerate(menu): - toPrint = "" - if index == selection: - toPrint += ('{bv} -> {t.blue_on_green} {title} {t.normal} <-'.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) - else: - toPrint += ('{bv} {t.normal} {title} '.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) - - for i in range(lineLengthAtTextStart - len(menuItem[0])): - toPrint += " " - - toPrint += "{bv}".format(bv=specialChars[renderMode]["borderVertical"]) - - toPrint = term.center(toPrint) - - print(toPrint) - - def mainRender(needsRender, menu, selection): - global hasRebuiltHardwareSelection - term = Terminal() - - if needsRender == 1: - print(term.clear()) - print(term.move_y(term.height // 16)) - print(term.black_on_cornsilk4(term.center('IOTstack DeConz Options'))) - print("") - print(term.center(commonTopBorder(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Select Option to configure {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - - if needsRender >= 1: - renderHotZone(term, menu, selection, hotzoneLocation) - - if needsRender == 1: - if os.path.exists("{buildSettings}".format(buildSettings=buildSettings)): - if hasRebuiltHardwareSelection: - print(term.center(commonEmptyLine(renderMode))) - print(term.center('{bv} {t.grey_on_blue4} {text} {t.normal}{t.white_on_black}{t.normal} {bv}'.format(t=term, text="Hardware list has been rebuilt: build_settings.yml", bv=specialChars[renderMode]["borderVertical"]))) - else: - print(term.center(commonEmptyLine(renderMode))) - print(term.center('{bv} {t.grey_on_blue4} {text} {t.normal}{t.white_on_black}{t.normal} {bv}'.format(t=term, text="Using existing build_settings.yml for hardware installation", bv=specialChars[renderMode]["borderVertical"]))) - else: - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - if not hideHelpText: - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Enter] to run command or save input {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Escape] to go back to build stack menu {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonBottomBorder(renderMode))) - - def runSelection(selection): - import types - global deconzBuildOptions - if len(deconzBuildOptions[selection]) > 1 and isinstance(deconzBuildOptions[selection][1], types.FunctionType): - deconzBuildOptions[selection][1]() - else: - print(term.green_reverse('IOTstack Error: No function assigned to menu item: "{}"'.format(nodeRedBuildOptions[selection][0]))) - - def isMenuItemSelectable(menu, index): - if len(menu) > index: - if len(menu[index]) > 2: - if menu[index][2]["skip"] == True: - return False - return True - - def menuEntryPoint(): - # These need to be reglobalised due to eval() - global currentMenuItemIndex - global selectionInProgress - global menuNavigateDirection - global needsRender - global hideHelpText - global deconzBuildOptions - term = Terminal() - with term.fullscreen(): - menuNavigateDirection = 0 - mainRender(needsRender, deconzBuildOptions, currentMenuItemIndex) - selectionInProgress = True - with term.cbreak(): - while selectionInProgress: - menuNavigateDirection = 0 - - if needsRender: # Only rerender when changed to prevent flickering - mainRender(needsRender, deconzBuildOptions, currentMenuItemIndex) - needsRender = 0 - - key = term.inkey(esc_delay=0.05) - if key.is_sequence: - if key.name == 'KEY_TAB': - menuNavigateDirection += 1 - if key.name == 'KEY_DOWN': - menuNavigateDirection += 1 - if key.name == 'KEY_UP': - menuNavigateDirection -= 1 - if key.name == 'KEY_LEFT': - goBack() - if key.name == 'KEY_ENTER': - runSelection(currentMenuItemIndex) - if key.name == 'KEY_ESCAPE': - return True - elif key: - if key == 'h': # H pressed - if hideHelpText: - hideHelpText = False - else: - hideHelpText = True - mainRender(1, deconzBuildOptions, currentMenuItemIndex) - - if menuNavigateDirection != 0: # If a direction was pressed, find next selectable item - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(deconzBuildOptions) - needsRender = 2 - - while not isMenuItemSelectable(deconzBuildOptions, currentMenuItemIndex): - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(deconzBuildOptions) - return True - - #################### - # End menu section - #################### - - - if haltOnErrors: - eval(toRun)() - else: - try: - eval(toRun)() - except: - pass - -# This check isn't required, but placed here for debugging purposes -global currentServiceName # Name of the current service -if currentServiceName == 'deconz': - main() -else: - print("Error. '{}' Tried to run 'deconz' config".format(currentServiceName)) diff --git a/.templates/deconz/hardware_list.yml b/.templates/deconz/hardware_list.yml deleted file mode 100755 index 8fa02cd92..000000000 --- a/.templates/deconz/hardware_list.yml +++ /dev/null @@ -1,9 +0,0 @@ -version: 1 -application: "IOTstack" -service: "deconz" -comment: "Deconz hardware check list." -hardwarePaths: - - "/dev/ttyUSB0" - - "/dev/ttyACM0" - - "/dev/ttyAMA0" - - "/dev/ttyS0" diff --git a/.templates/deconz/passwords.py b/.templates/deconz/passwords.py deleted file mode 100755 index 4f4b6315c..000000000 --- a/.templates/deconz/passwords.py +++ /dev/null @@ -1,328 +0,0 @@ -#!/usr/bin/env python3 - -import signal - -def main(): - from blessed import Terminal - from deps.chars import specialChars, commonTopBorder, commonBottomBorder, commonEmptyLine - from deps.consts import servicesDirectory, templatesDirectory, buildSettingsFileName - import time - import subprocess - import ruamel.yaml - import os - - global signal - global currentServiceName - global menuSelectionInProgress - global mainMenuList - global currentMenuItemIndex - global renderMode - global paginationSize - global paginationStartIndex - global hideHelpText - - yaml = ruamel.yaml.YAML() - yaml.preserve_quotes = True - - try: # If not already set, then set it. - hideHelpText = hideHelpText - except: - hideHelpText = False - - term = Terminal() - hotzoneLocation = [((term.height // 16) + 6), 0] - paginationToggle = [10, term.height - 25] - paginationStartIndex = 0 - paginationSize = paginationToggle[0] - - serviceService = servicesDirectory + currentServiceName - serviceTemplate = templatesDirectory + currentServiceName - buildSettings = serviceService + buildSettingsFileName - - def goBack(): - global menuSelectionInProgress - global needsRender - menuSelectionInProgress = False - needsRender = 1 - return True - - mainMenuList = [] - - hotzoneLocation = [((term.height // 16) + 6), 0] - - menuSelectionInProgress = True - currentMenuItemIndex = 0 - menuNavigateDirection = 0 - - # Render Modes: - # 0 = No render needed - # 1 = Full render - # 2 = Hotzone only - needsRender = 1 - - def onResize(sig, action): - global mainMenuList - global currentMenuItemIndex - mainRender(1, mainMenuList, currentMenuItemIndex) - - def generateLineText(text, textLength=None, paddingBefore=0, lineLength=64): - result = "" - for i in range(paddingBefore): - result += " " - - textPrintableCharactersLength = textLength - - if (textPrintableCharactersLength) == None: - textPrintableCharactersLength = len(text) - - result += text - remainingSpace = lineLength - textPrintableCharactersLength - - for i in range(remainingSpace): - result += " " - - return result - - def renderHotZone(term, renderType, menu, selection, hotzoneLocation, paddingBefore = 4): - global paginationSize - selectedTextLength = len("-> ") - - print(term.move(hotzoneLocation[0], hotzoneLocation[1])) - - if paginationStartIndex >= 1: - print(term.center("{b} {uaf} {uaf}{uaf}{uaf} {ual} {b}".format( - b=specialChars[renderMode]["borderVertical"], - uaf=specialChars[renderMode]["upArrowFull"], - ual=specialChars[renderMode]["upArrowLine"] - ))) - else: - print(term.center(commonEmptyLine(renderMode))) - - for (index, menuItem) in enumerate(menu): # Menu loop - if index >= paginationStartIndex and index < paginationStartIndex + paginationSize: - lineText = generateLineText(menuItem[0], paddingBefore=paddingBefore) - - # Menu highlight logic - if index == selection: - formattedLineText = '-> {t.blue_on_green}{title}{t.normal} <-'.format(t=term, title=menuItem[0]) - paddedLineText = generateLineText(formattedLineText, textLength=len(menuItem[0]) + selectedTextLength, paddingBefore=paddingBefore - selectedTextLength) - toPrint = paddedLineText - else: - toPrint = '{title}{t.normal}'.format(t=term, title=lineText) - # ##### - - # Menu check render logic - if menuItem[1]["checked"]: - toPrint = " (X) " + toPrint - else: - toPrint = " ( ) " + toPrint - - toPrint = "{bv} {toPrint} {bv}".format(bv=specialChars[renderMode]["borderVertical"], toPrint=toPrint) # Generate border - toPrint = term.center(toPrint) # Center Text (All lines should have the same amount of printable characters) - # ##### - print(toPrint) - - if paginationStartIndex + paginationSize < len(menu): - print(term.center("{b} {daf} {daf}{daf}{daf} {dal} {b}".format( - b=specialChars[renderMode]["borderVertical"], - daf=specialChars[renderMode]["downArrowFull"], - dal=specialChars[renderMode]["downArrowLine"] - ))) - else: - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - - - def mainRender(needsRender, menu, selection): - global paginationStartIndex - global paginationSize - term = Terminal() - - if selection >= paginationStartIndex + paginationSize: - paginationStartIndex = selection - (paginationSize - 1) + 1 - needsRender = 1 - - if selection <= paginationStartIndex - 1: - paginationStartIndex = selection - needsRender = 1 - - if needsRender == 1: - print(term.clear()) - print(term.move_y(term.height // 16)) - print(term.black_on_cornsilk4(term.center('IOTstack DeConz Password Options'))) - print("") - print(term.center(commonTopBorder(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Select Password Option {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - - if needsRender >= 1: - renderHotZone(term, needsRender, menu, selection, hotzoneLocation) - - if needsRender == 1: - print(term.center(commonEmptyLine(renderMode))) - if not hideHelpText: - if term.height < 32: - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Not enough vertical room to render controls help text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - else: - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Space] to select option {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Enter] to build and save option {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Escape] to cancel changes {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonBottomBorder(renderMode))) - - def runSelection(selection): - import types - if len(mainMenuList[selection]) > 1 and isinstance(mainMenuList[selection][1], types.FunctionType): - mainMenuList[selection][1]() - else: - print(term.green_reverse('IOTstack Error: No function assigned to menu item: "{}"'.format(mainMenuList[selection][0]))) - - def isMenuItemSelectable(menu, index): - if len(menu) > index: - if len(menu[index]) > 1: - if "skip" in menu[index][1] and menu[index][1]["skip"] == True: - return False - return True - - def loadOptionsMenu(): - global mainMenuList - mainMenuList.append(["Use default password for this build", { "checked": True }]) - mainMenuList.append(["Randomise database password for this build", { "checked": False }]) - mainMenuList.append(["Randomise database password every build", { "checked": False }]) - mainMenuList.append(["Do nothing", { "checked": False }]) - - def checkMenuItem(selection): - global mainMenuList - for (index, menuItem) in enumerate(mainMenuList): - mainMenuList[index][1]["checked"] = False - - mainMenuList[selection][1]["checked"] = True - - def saveOptions(): - try: - if not os.path.exists(serviceService): - os.makedirs(serviceService, exist_ok=True) - - if os.path.exists(buildSettings): - with open(r'%s' % buildSettings) as objBuildSettingsFile: - deconzYamlBuildOptions = yaml.load(objBuildSettingsFile) - else: - deconzYamlBuildOptions = { - "version": "1", - "application": "IOTstack", - "service": "deconz", - "comment": "Build Settings", - } - - deconzYamlBuildOptions["databasePasswordOption"] = "" - - for (index, menuOption) in enumerate(mainMenuList): - if menuOption[1]["checked"]: - deconzYamlBuildOptions["databasePasswordOption"] = menuOption[0] - break - - with open(buildSettings, 'w') as outputFile: - yaml.dump(deconzYamlBuildOptions, outputFile) - - except Exception as err: - print("Error saving DeConz Password options", currentServiceName) - print(err) - return False - global hasRebuiltHardwareSelection - hasRebuiltHardwareSelection = True - return True - - def loadOptions(): - try: - if not os.path.exists(serviceService): - os.makedirs(serviceService, exist_ok=True) - - if os.path.exists(buildSettings): - with open(r'%s' % buildSettings) as objBuildSettingsFile: - deconzYamlBuildOptions = yaml.load(objBuildSettingsFile) - - for (index, menuOption) in enumerate(mainMenuList): - if menuOption[0] == deconzYamlBuildOptions["databasePasswordOption"]: - checkMenuItem(index) - break - - except Exception as err: - print("Error loading DeConz Password options", currentServiceName) - print(err) - return False - return True - - - if __name__ == 'builtins': - global signal - term = Terminal() - signal.signal(signal.SIGWINCH, onResize) - loadOptionsMenu() - loadOptions() - with term.fullscreen(): - menuNavigateDirection = 0 - mainRender(needsRender, mainMenuList, currentMenuItemIndex) - menuSelectionInProgress = True - with term.cbreak(): - while menuSelectionInProgress: - menuNavigateDirection = 0 - - if not needsRender == 0: # Only rerender when changed to prevent flickering - mainRender(needsRender, mainMenuList, currentMenuItemIndex) - needsRender = 0 - - key = term.inkey(esc_delay=0.05) - if key.is_sequence: - if key.name == 'KEY_TAB': - if paginationSize == paginationToggle[0]: - paginationSize = paginationToggle[1] - else: - paginationSize = paginationToggle[0] - mainRender(1, mainMenuList, currentMenuItemIndex) - if key.name == 'KEY_DOWN': - menuNavigateDirection += 1 - if key.name == 'KEY_UP': - menuNavigateDirection -= 1 - if key.name == 'KEY_ENTER': - if saveOptions(): - return True - else: - print("Something went wrong. Try saving the list again.") - if key.name == 'KEY_ESCAPE': - menuSelectionInProgress = False - return True - elif key: - if key == ' ': # Space pressed - checkMenuItem(currentMenuItemIndex) # Update checked list - needsRender = 2 - elif key == 'h': # H pressed - if hideHelpText: - hideHelpText = False - else: - hideHelpText = True - mainRender(1, mainMenuList, currentMenuItemIndex) - - if menuNavigateDirection != 0: # If a direction was pressed, find next selectable item - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(mainMenuList) - needsRender = 2 - - while not isMenuItemSelectable(mainMenuList, currentMenuItemIndex): - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(mainMenuList) - return True - - return True - -originalSignalHandler = signal.getsignal(signal.SIGINT) -main() -signal.signal(signal.SIGWINCH, originalSignalHandler) diff --git a/.templates/deconz/select_hw.py b/.templates/deconz/select_hw.py deleted file mode 100755 index 381aa31a5..000000000 --- a/.templates/deconz/select_hw.py +++ /dev/null @@ -1,330 +0,0 @@ -#!/usr/bin/env python3 - -import signal - -def main(): - from blessed import Terminal - from deps.chars import specialChars, commonTopBorder, commonBottomBorder, commonEmptyLine - from deps.consts import servicesDirectory, templatesDirectory, buildSettingsFileName - import time - import subprocess - import ruamel.yaml - import os - - global signal - global currentServiceName - global dockerCommandsSelectionInProgress - global mainMenuList - global currentMenuItemIndex - global renderMode - global paginationSize - global paginationStartIndex - global hardwareListFile - global hideHelpText - - global installCommand - - yaml = ruamel.yaml.YAML() - yaml.preserve_quotes = True - - try: # If not already set, then set it. - hideHelpText = hideHelpText - except: - hideHelpText = False - - term = Terminal() - hotzoneLocation = [((term.height // 16) + 6), 0] - paginationToggle = [10, term.height - 25] - paginationStartIndex = 0 - paginationSize = paginationToggle[0] - - serviceService = servicesDirectory + currentServiceName - serviceTemplate = templatesDirectory + currentServiceName - hardwareListFileSource = serviceTemplate + '/hardware_list.yml' - - def goBack(): - global dockerCommandsSelectionInProgress - global needsRender - dockerCommandsSelectionInProgress = False - needsRender = 1 - return True - - mainMenuList = [] - - hotzoneLocation = [((term.height // 16) + 6), 0] - - dockerCommandsSelectionInProgress = True - currentMenuItemIndex = 0 - menuNavigateDirection = 0 - - # Render Modes: - # 0 = No render needed - # 1 = Full render - # 2 = Hotzone only - needsRender = 1 - - def onResize(sig, action): - global mainMenuList - global currentMenuItemIndex - mainRender(1, mainMenuList, currentMenuItemIndex) - - def generateLineText(text, textLength=None, paddingBefore=0, lineLength=64): - result = "" - for i in range(paddingBefore): - result += " " - - textPrintableCharactersLength = textLength - - if (textPrintableCharactersLength) == None: - textPrintableCharactersLength = len(text) - - result += text - remainingSpace = lineLength - textPrintableCharactersLength - - for i in range(remainingSpace): - result += " " - - return result - - def renderHotZone(term, renderType, menu, selection, hotzoneLocation, paddingBefore = 4): - global paginationSize - selectedTextLength = len("-> ") - - print(term.move(hotzoneLocation[0], hotzoneLocation[1])) - - if paginationStartIndex >= 1: - print(term.center("{b} {uaf} {uaf}{uaf}{uaf} {ual} {b}".format( - b=specialChars[renderMode]["borderVertical"], - uaf=specialChars[renderMode]["upArrowFull"], - ual=specialChars[renderMode]["upArrowLine"] - ))) - else: - print(term.center(commonEmptyLine(renderMode))) - - for (index, menuItem) in enumerate(menu): # Menu loop - if index >= paginationStartIndex and index < paginationStartIndex + paginationSize: - lineText = generateLineText(menuItem[0], paddingBefore=paddingBefore) - - # Menu highlight logic - if index == selection: - formattedLineText = '-> {t.blue_on_green}{title}{t.normal} <-'.format(t=term, title=menuItem[0]) - paddedLineText = generateLineText(formattedLineText, textLength=len(menuItem[0]) + selectedTextLength, paddingBefore=paddingBefore - selectedTextLength) - toPrint = paddedLineText - else: - toPrint = '{title}{t.normal}'.format(t=term, title=lineText) - # ##### - - # Menu check render logic - if menuItem[1]["checked"]: - toPrint = " (X) " + toPrint - else: - toPrint = " ( ) " + toPrint - - toPrint = "{bv} {toPrint} {bv}".format(bv=specialChars[renderMode]["borderVertical"], toPrint=toPrint) # Generate border - toPrint = term.center(toPrint) # Center Text (All lines should have the same amount of printable characters) - # ##### - print(toPrint) - - if paginationStartIndex + paginationSize < len(menu): - print(term.center("{b} {daf} {daf}{daf}{daf} {dal} {b}".format( - b=specialChars[renderMode]["borderVertical"], - daf=specialChars[renderMode]["downArrowFull"], - dal=specialChars[renderMode]["downArrowLine"] - ))) - else: - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - - - def mainRender(needsRender, menu, selection): - global paginationStartIndex - global paginationSize - term = Terminal() - - if selection >= paginationStartIndex + paginationSize: - paginationStartIndex = selection - (paginationSize - 1) + 1 - needsRender = 1 - - if selection <= paginationStartIndex - 1: - paginationStartIndex = selection - needsRender = 1 - - if needsRender == 1: - print(term.clear()) - print(term.move_y(term.height // 16)) - print(term.black_on_cornsilk4(term.center('IOTstack DeConz Hardware'))) - print("") - print(term.center(commonTopBorder(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Select DeConz Hardware {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - - if needsRender >= 1: - renderHotZone(term, needsRender, menu, selection, hotzoneLocation) - - if needsRender == 1: - print(term.center(commonEmptyLine(renderMode))) - if not hideHelpText: - if term.height < 32: - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Not enough vertical room to render controls help text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - else: - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Space] to select or deselect hardware {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Enter] to build and save hardware list {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Escape] to cancel changes {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonBottomBorder(renderMode))) - - def runSelection(selection): - import types - if len(mainMenuList[selection]) > 1 and isinstance(mainMenuList[selection][1], types.FunctionType): - mainMenuList[selection][1]() - else: - print(term.green_reverse('IOTstack Error: No function assigned to menu item: "{}"'.format(mainMenuList[selection][0]))) - - def isMenuItemSelectable(menu, index): - if len(menu) > index: - if len(menu[index]) > 1: - if "skip" in menu[index][1] and menu[index][1]["skip"] == True: - return False - return True - - def loadAddonsMenu(): - global mainMenuList - if os.path.exists(hardwareListFileSource): - with open(r'%s' % hardwareListFileSource) as objHardwareListFile: - hardwareKnown = yaml.load(objHardwareListFile) - knownHardwareList = hardwareKnown["hardwarePaths"] - if os.path.exists("{serviceDir}{buildSettings}".format(serviceDir=serviceService, buildSettings=buildSettingsFileName)): - with open("{serviceDir}{buildSettings}".format(serviceDir=serviceService, buildSettings=buildSettingsFileName)) as objSavedHardwareListFile: - savedHardwareList = yaml.load(objSavedHardwareListFile) - savedHardware = [] - - try: - savedHardware = savedHardwareList["hardware"] - except: - print("Error: Loading saved hardware selection. Please resave your selection.") - input("Press Enter to continue...") - - for (index, hardwarePath) in enumerate(knownHardwareList): - if hardwarePath in savedHardware: - mainMenuList.append([hardwarePath, { "checked": True }]) - else: - mainMenuList.append([hardwarePath, { "checked": False }]) - - else: # No saved list - for (index, hardwarePath) in enumerate(knownHardwareList): - if os.path.exists(hardwarePath): - mainMenuList.append([hardwarePath, { "checked": True }]) - else: - mainMenuList.append([hardwarePath, { "checked": False }]) - - - else: - print("Error: '{hardwareListFile}' file doesn't exist.".format(hardwareListFile=hardwareListFileSource)) - input("Press Enter to continue...") - - def checkMenuItem(selection): - global mainMenuList - if mainMenuList[selection][1]["checked"] == True: - mainMenuList[selection][1]["checked"] = False - else: - mainMenuList[selection][1]["checked"] = True - - def saveAddonList(): - try: - if not os.path.exists(serviceService): - os.makedirs(serviceService, exist_ok=True) - deconzYamlHardwareList = { - "version": "1", - "application": "IOTstack", - "service": "deconz", - "comment": "Build Settings", - "hardware": [] - } - for (index, addon) in enumerate(mainMenuList): - if addon[1]["checked"]: - deconzYamlHardwareList["hardware"].append(addon[0]) - - with open("{serviceDir}{buildSettings}".format(serviceDir=serviceService, buildSettings=buildSettingsFileName), 'w') as outputFile: - yaml.dump(deconzYamlHardwareList, outputFile) - - except Exception as err: - print("Error saving DeConz Hardware list", currentServiceName) - print(err) - return False - global hasRebuiltHardwareSelection - hasRebuiltHardwareSelection = True - return True - - - if __name__ == 'builtins': - global signal - term = Terminal() - signal.signal(signal.SIGWINCH, onResize) - loadAddonsMenu() - with term.fullscreen(): - menuNavigateDirection = 0 - mainRender(needsRender, mainMenuList, currentMenuItemIndex) - dockerCommandsSelectionInProgress = True - with term.cbreak(): - while dockerCommandsSelectionInProgress: - menuNavigateDirection = 0 - - if not needsRender == 0: # Only rerender when changed to prevent flickering - mainRender(needsRender, mainMenuList, currentMenuItemIndex) - needsRender = 0 - - key = term.inkey(esc_delay=0.05) - if key.is_sequence: - if key.name == 'KEY_TAB': - if paginationSize == paginationToggle[0]: - paginationSize = paginationToggle[1] - else: - paginationSize = paginationToggle[0] - mainRender(1, mainMenuList, currentMenuItemIndex) - if key.name == 'KEY_DOWN': - menuNavigateDirection += 1 - if key.name == 'KEY_UP': - menuNavigateDirection -= 1 - if key.name == 'KEY_ENTER': - if saveAddonList(): - return True - else: - print("Something went wrong. Try saving the list again.") - if key.name == 'KEY_ESCAPE': - dockerCommandsSelectionInProgress = False - return True - elif key: - if key == ' ': # Space pressed - checkMenuItem(currentMenuItemIndex) # Update checked list - needsRender = 2 - elif key == 'h': # H pressed - if hideHelpText: - hideHelpText = False - else: - hideHelpText = True - mainRender(1, mainMenuList, currentMenuItemIndex) - - if menuNavigateDirection != 0: # If a direction was pressed, find next selectable item - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(mainMenuList) - needsRender = 2 - - while not isMenuItemSelectable(mainMenuList, currentMenuItemIndex): - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(mainMenuList) - return True - - return True - -originalSignalHandler = signal.getsignal(signal.SIGINT) -main() -signal.signal(signal.SIGWINCH, originalSignalHandler) diff --git a/.templates/diyhue/build.py b/.templates/diyhue/build.py deleted file mode 100755 index 7d0ff9b78..000000000 --- a/.templates/diyhue/build.py +++ /dev/null @@ -1,323 +0,0 @@ -#!/usr/bin/env python3 - -issues = {} # Returned issues dict -buildHooks = {} # Options, and others hooks -haltOnErrors = True - -# Main wrapper function. Required to make local vars work correctly -def main(): - import os - import time - import ruamel.yaml - import signal - import sys - from blessed import Terminal - - from deps.chars import specialChars, commonTopBorder, commonBottomBorder, commonEmptyLine - from deps.consts import servicesDirectory, templatesDirectory - from deps.common_functions import getExternalPorts, getInternalPorts, checkPortConflicts, enterPortNumberWithWhiptail, getNetworkDetails - - yaml = ruamel.yaml.YAML() - yaml.preserve_quotes = True - - global dockerComposeServicesYaml # The loaded memory YAML of all checked services - global toRun # Switch for which function to run when executed - global buildHooks # Where to place the options menu result - global currentServiceName # Name of the current service - global issues # Returned issues dict - global haltOnErrors # Turn on to allow erroring - global hideHelpText # Showing and hiding the help controls text - global serviceService - - serviceService = servicesDirectory + currentServiceName - serviceTemplate = templatesDirectory + currentServiceName - - try: # If not already set, then set it. - hideHelpText = hideHelpText - except: - hideHelpText = False - - # runtime vars - portConflicts = [] - - # This lets the menu know whether to put " >> Options " or not - # This function is REQUIRED. - def checkForOptionsHook(): - try: - buildHooks["options"] = callable(runOptionsMenu) - except: - buildHooks["options"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPreBuildHook(): - try: - buildHooks["preBuildHook"] = callable(preBuild) - except: - buildHooks["preBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPostBuildHook(): - try: - buildHooks["postBuildHook"] = callable(postBuild) - except: - buildHooks["postBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForRunChecksHook(): - try: - buildHooks["runChecksHook"] = callable(runChecks) - except: - buildHooks["runChecksHook"] = False - return buildHooks - return buildHooks - - # This service will not check anything unless this is set - # This function is optional, and will run each time the menu is rendered - def runChecks(): - checkForIssues() - return [] - - # This function is optional, and will run after the docker-compose.yml file is written to disk. - def postBuild(): - return True - - # This function is optional, and will run just before the build docker-compose.yml code. - def preBuild(): - global dockerComposeServicesYaml - try: - if "diyhue" in dockerComposeServicesYaml: - networkDetails = getNetworkDetails() - if "environment" in dockerComposeServicesYaml["diyhue"]: - for (envIndex, envName) in enumerate(dockerComposeServicesYaml["diyhue"]["environment"]): - ipAddressSet = envName.replace("%LAN_IP_Address%", networkDetails["ip"]) - macAddressSet = ipAddressSet.replace("%LAN_MAC_Address%", networkDetails["mac"]) - dockerComposeServicesYaml["diyhue"]["environment"][envIndex] = macAddressSet - except Exception as err: - print("Error setting diyhue network details: ", err) - input("Press any key to continue...") - return False - - return True - - # ##################################### - # Supporting functions below - # ##################################### - - def checkForIssues(): - for (index, serviceName) in enumerate(dockerComposeServicesYaml): - if not currentServiceName == serviceName: # Skip self - currentServicePorts = getExternalPorts(currentServiceName, dockerComposeServicesYaml) - portConflicts = checkPortConflicts(serviceName, currentServicePorts, dockerComposeServicesYaml) - if (len(portConflicts) > 0): - issues["portConflicts"] = portConflicts - - # ##################################### - # End Supporting functions - # ##################################### - - ############################ - # Menu Logic - ############################ - - global currentMenuItemIndex - global selectionInProgress - global menuNavigateDirection - global needsRender - - selectionInProgress = True - currentMenuItemIndex = 0 - menuNavigateDirection = 0 - needsRender = 1 - term = Terminal() - hotzoneLocation = [((term.height // 16) + 6), 0] - - def goBack(): - global selectionInProgress - global needsRender - selectionInProgress = False - needsRender = 1 - return True - - def enterPortNumberExec(): - # global term - global needsRender - global dockerComposeServicesYaml - externalPort = getExternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - internalPort = getInternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - newPortNumber = enterPortNumberWithWhiptail(term, dockerComposeServicesYaml, currentServiceName, hotzoneLocation, externalPort) - - if newPortNumber > 0: - dockerComposeServicesYaml[currentServiceName]["ports"][0] = "{newExtPort}:{oldIntPort}".format( - newExtPort = newPortNumber, - oldIntPort = internalPort - ) - createMenu() - needsRender = 1 - - def onResize(sig, action): - global diyhueBuildOptions - global currentMenuItemIndex - mainRender(1, diyhueBuildOptions, currentMenuItemIndex) - - diyhueBuildOptions = [] - - def createMenu(): - global diyhueBuildOptions - try: - diyhueBuildOptions = [] - portNumber = getExternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - diyhueBuildOptions.append([ - "Change external WUI Port Number from: {port}".format(port=portNumber), - enterPortNumberExec - ]) - except: # Error getting port - pass - diyhueBuildOptions.append(["Go back", goBack]) - - def runOptionsMenu(): - createMenu() - menuEntryPoint() - return True - - def renderHotZone(term, menu, selection, hotzoneLocation): - lineLengthAtTextStart = 71 - print(term.move(hotzoneLocation[0], hotzoneLocation[1])) - for (index, menuItem) in enumerate(menu): - toPrint = "" - if index == selection: - toPrint += ('{bv} -> {t.blue_on_green} {title} {t.normal} <-'.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) - else: - toPrint += ('{bv} {t.normal} {title} '.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) - - for i in range(lineLengthAtTextStart - len(menuItem[0])): - toPrint += " " - - toPrint += "{bv}".format(bv=specialChars[renderMode]["borderVertical"]) - - toPrint = term.center(toPrint) - - print(toPrint) - - def mainRender(needsRender, menu, selection): - term = Terminal() - - if needsRender == 1: - print(term.clear()) - print(term.move_y(term.height // 16)) - print(term.black_on_cornsilk4(term.center('IOTstack DIY Hue Options'))) - print("") - print(term.center(commonTopBorder(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Select Option to configure {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - - if needsRender >= 1: - renderHotZone(term, menu, selection, hotzoneLocation) - - if needsRender == 1: - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - if not hideHelpText: - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Enter] to run command or save input {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Escape] to go back to build stack menu {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonBottomBorder(renderMode))) - - def runSelection(selection): - import types - global diyhueBuildOptions - if len(diyhueBuildOptions[selection]) > 1 and isinstance(diyhueBuildOptions[selection][1], types.FunctionType): - diyhueBuildOptions[selection][1]() - else: - print(term.green_reverse('IOTstack Error: No function assigned to menu item: "{}"'.format(nodeRedBuildOptions[selection][0]))) - - def isMenuItemSelectable(menu, index): - if len(menu) > index: - if len(menu[index]) > 2: - if menu[index][2]["skip"] == True: - return False - return True - - def menuEntryPoint(): - # These need to be reglobalised due to eval() - global currentMenuItemIndex - global selectionInProgress - global menuNavigateDirection - global needsRender - global hideHelpText - global diyhueBuildOptions - term = Terminal() - with term.fullscreen(): - menuNavigateDirection = 0 - mainRender(needsRender, diyhueBuildOptions, currentMenuItemIndex) - selectionInProgress = True - with term.cbreak(): - while selectionInProgress: - menuNavigateDirection = 0 - - if needsRender: # Only rerender when changed to prevent flickering - mainRender(needsRender, diyhueBuildOptions, currentMenuItemIndex) - needsRender = 0 - - key = term.inkey(esc_delay=0.05) - if key.is_sequence: - if key.name == 'KEY_TAB': - menuNavigateDirection += 1 - if key.name == 'KEY_DOWN': - menuNavigateDirection += 1 - if key.name == 'KEY_UP': - menuNavigateDirection -= 1 - if key.name == 'KEY_LEFT': - goBack() - if key.name == 'KEY_ENTER': - runSelection(currentMenuItemIndex) - if key.name == 'KEY_ESCAPE': - return True - elif key: - if key == 'h': # H pressed - if hideHelpText: - hideHelpText = False - else: - hideHelpText = True - mainRender(1, diyhueBuildOptions, currentMenuItemIndex) - - if menuNavigateDirection != 0: # If a direction was pressed, find next selectable item - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(diyhueBuildOptions) - needsRender = 2 - - while not isMenuItemSelectable(diyhueBuildOptions, currentMenuItemIndex): - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(diyhueBuildOptions) - return True - - #################### - # End menu section - #################### - - - if haltOnErrors: - eval(toRun)() - else: - try: - eval(toRun)() - except: - pass - -# This check isn't required, but placed here for debugging purposes -global currentServiceName # Name of the current service -if currentServiceName == 'diyhue': - main() -else: - print("Error. '{}' Tried to run 'diyhue' config".format(currentServiceName)) diff --git a/.templates/dozzle/build.py b/.templates/dozzle/build.py deleted file mode 100755 index 04208d9ac..000000000 --- a/.templates/dozzle/build.py +++ /dev/null @@ -1,308 +0,0 @@ -#!/usr/bin/env python3 - -issues = {} # Returned issues dict -buildHooks = {} # Options, and others hooks -haltOnErrors = True - -# Main wrapper function. Required to make local vars work correctly -def main(): - import os - import time - import ruamel.yaml - import signal - import sys - from blessed import Terminal - - from deps.chars import specialChars, commonTopBorder, commonBottomBorder, commonEmptyLine - from deps.consts import servicesDirectory, templatesDirectory - from deps.common_functions import getExternalPorts, getInternalPorts, checkPortConflicts, enterPortNumberWithWhiptail - - yaml = ruamel.yaml.YAML() - yaml.preserve_quotes = True - - global dockerComposeServicesYaml # The loaded memory YAML of all checked services - global toRun # Switch for which function to run when executed - global buildHooks # Where to place the options menu result - global currentServiceName # Name of the current service - global issues # Returned issues dict - global haltOnErrors # Turn on to allow erroring - global hideHelpText # Showing and hiding the help controls text - global serviceService - - serviceService = servicesDirectory + currentServiceName - - try: # If not already set, then set it. - hideHelpText = hideHelpText - except: - hideHelpText = False - - # runtime vars - portConflicts = [] - - # This lets the menu know whether to put " >> Options " or not - # This function is REQUIRED. - def checkForOptionsHook(): - try: - buildHooks["options"] = callable(runOptionsMenu) - except: - buildHooks["options"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPreBuildHook(): - try: - buildHooks["preBuildHook"] = callable(preBuild) - except: - buildHooks["preBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPostBuildHook(): - try: - buildHooks["postBuildHook"] = callable(postBuild) - except: - buildHooks["postBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForRunChecksHook(): - try: - buildHooks["runChecksHook"] = callable(runChecks) - except: - buildHooks["runChecksHook"] = False - return buildHooks - return buildHooks - - # This service will not check anything unless this is set - # This function is optional, and will run each time the menu is rendered - def runChecks(): - checkForIssues() - return [] - - # This function is optional, and will run after the docker-compose.yml file is written to disk. - def postBuild(): - return True - - # This function is optional, and will run just before the build docker-compose.yml code. - def preBuild(): - return True - - # ##################################### - # Supporting functions below - # ##################################### - - def checkForIssues(): - for (index, serviceName) in enumerate(dockerComposeServicesYaml): - if not currentServiceName == serviceName: # Skip self - currentServicePorts = getExternalPorts(currentServiceName, dockerComposeServicesYaml) - portConflicts = checkPortConflicts(serviceName, currentServicePorts, dockerComposeServicesYaml) - if (len(portConflicts) > 0): - issues["portConflicts"] = portConflicts - - # ##################################### - # End Supporting functions - # ##################################### - - ############################ - # Menu Logic - ############################ - - global currentMenuItemIndex - global selectionInProgress - global menuNavigateDirection - global needsRender - - selectionInProgress = True - currentMenuItemIndex = 0 - menuNavigateDirection = 0 - needsRender = 1 - term = Terminal() - hotzoneLocation = [((term.height // 16) + 6), 0] - - def goBack(): - global selectionInProgress - global needsRender - selectionInProgress = False - needsRender = 1 - return True - - def enterPortNumberExec(): - # global term - global needsRender - global dockerComposeServicesYaml - externalPort = getExternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - internalPort = getInternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - newPortNumber = enterPortNumberWithWhiptail(term, dockerComposeServicesYaml, currentServiceName, hotzoneLocation, externalPort) - - if newPortNumber > 0: - dockerComposeServicesYaml[currentServiceName]["ports"][0] = "{newExtPort}:{oldIntPort}".format( - newExtPort = newPortNumber, - oldIntPort = internalPort - ) - createMenu() - needsRender = 1 - - def onResize(sig, action): - global dozzleBuildOptions - global currentMenuItemIndex - mainRender(1, dozzleBuildOptions, currentMenuItemIndex) - - dozzleBuildOptions = [] - - def createMenu(): - global dozzleBuildOptions - try: - dozzleBuildOptions = [] - portNumber = getExternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - # dozzleBuildOptions.append([ - # "Change external WUI Port Number from: {port}".format(port=portNumber), - # enterPortNumberExec - # ]) - except: # Error getting port - pass - dozzleBuildOptions.append(["Go back", goBack]) - - def runOptionsMenu(): - createMenu() - menuEntryPoint() - return True - - def renderHotZone(term, menu, selection, hotzoneLocation): - lineLengthAtTextStart = 71 - print(term.move(hotzoneLocation[0], hotzoneLocation[1])) - for (index, menuItem) in enumerate(menu): - toPrint = "" - if index == selection: - toPrint += ('{bv} -> {t.blue_on_green} {title} {t.normal} <-'.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) - else: - toPrint += ('{bv} {t.normal} {title} '.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) - - for i in range(lineLengthAtTextStart - len(menuItem[0])): - toPrint += " " - - toPrint += "{bv}".format(bv=specialChars[renderMode]["borderVertical"]) - - toPrint = term.center(toPrint) - - print(toPrint) - - def mainRender(needsRender, menu, selection): - term = Terminal() - - if needsRender == 1: - print(term.clear()) - print(term.move_y(term.height // 16)) - print(term.black_on_cornsilk4(term.center('IOTstack Dozzle Options'))) - print("") - print(term.center(commonTopBorder(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Select Option to configure {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - - if needsRender >= 1: - renderHotZone(term, menu, selection, hotzoneLocation) - - if needsRender == 1: - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - if not hideHelpText: - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Enter] to run command or save input {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Escape] to go back to build stack menu {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonBottomBorder(renderMode))) - - def runSelection(selection): - import types - global dozzleBuildOptions - if len(dozzleBuildOptions[selection]) > 1 and isinstance(dozzleBuildOptions[selection][1], types.FunctionType): - dozzleBuildOptions[selection][1]() - else: - print(term.green_reverse('IOTstack Error: No function assigned to menu item: "{}"'.format(nodeRedBuildOptions[selection][0]))) - - def isMenuItemSelectable(menu, index): - if len(menu) > index: - if len(menu[index]) > 2: - if menu[index][2]["skip"] == True: - return False - return True - - def menuEntryPoint(): - # These need to be reglobalised due to eval() - global currentMenuItemIndex - global selectionInProgress - global menuNavigateDirection - global needsRender - global hideHelpText - global dozzleBuildOptions - term = Terminal() - with term.fullscreen(): - menuNavigateDirection = 0 - mainRender(needsRender, dozzleBuildOptions, currentMenuItemIndex) - selectionInProgress = True - with term.cbreak(): - while selectionInProgress: - menuNavigateDirection = 0 - - if needsRender: # Only rerender when changed to prevent flickering - mainRender(needsRender, dozzleBuildOptions, currentMenuItemIndex) - needsRender = 0 - - key = term.inkey(esc_delay=0.05) - if key.is_sequence: - if key.name == 'KEY_TAB': - menuNavigateDirection += 1 - if key.name == 'KEY_DOWN': - menuNavigateDirection += 1 - if key.name == 'KEY_UP': - menuNavigateDirection -= 1 - if key.name == 'KEY_LEFT': - goBack() - if key.name == 'KEY_ENTER': - runSelection(currentMenuItemIndex) - if key.name == 'KEY_ESCAPE': - return True - elif key: - if key == 'h': # H pressed - if hideHelpText: - hideHelpText = False - else: - hideHelpText = True - mainRender(1, dozzleBuildOptions, currentMenuItemIndex) - - if menuNavigateDirection != 0: # If a direction was pressed, find next selectable item - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(dozzleBuildOptions) - needsRender = 2 - - while not isMenuItemSelectable(dozzleBuildOptions, currentMenuItemIndex): - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(dozzleBuildOptions) - return True - - #################### - # End menu section - #################### - - - if haltOnErrors: - eval(toRun)() - else: - try: - eval(toRun)() - except: - pass - -# This check isn't required, but placed here for debugging purposes -global currentServiceName # Name of the current service -if currentServiceName == 'dozzle': - main() -else: - print("Error. '{}' Tried to run 'dozzle' config".format(currentServiceName)) diff --git a/.templates/env.yml b/.templates/env.yml deleted file mode 100755 index b34de3f76..000000000 --- a/.templates/env.yml +++ /dev/null @@ -1,42 +0,0 @@ -networks: - iotstack_nw: # Exposed by your host. - # external: true - name: IOTstack_Net - driver: bridge - ipam: - driver: default - config: - - subnet: 10.77.60.0/24 - # - gateway: 10.77.60.1 - - iotstack_nw_internal: # For interservice communication. No access to outside - name: IOTstack_Net_Internal - driver: bridge - internal: true - ipam: - driver: default - config: - - subnet: 10.77.76.0/24 - # - gateway: 10.77.76.1 - - vpn_nw: # Network specifically for VPN - name: IOTstack_VPN - driver: bridge - ipam: - driver: default - config: - - subnet: 10.77.88.0/24 - # - gateway: 192.18.200.1 - - nextcloud_internal: # Network for NextCloud service - name: IOTstack_NextCloud - driver: bridge - internal: true - - # default: - # external: true - # name: iotstack_nw - - # hosts_nw: - # driver: hosts - \ No newline at end of file diff --git a/.templates/espruinohub/build.py b/.templates/espruinohub/build.py deleted file mode 100755 index 84a45e335..000000000 --- a/.templates/espruinohub/build.py +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env python3 - -issues = {} # Returned issues dict -buildHooks = {} # Options, and others hooks -haltOnErrors = True - -# Main wrapper function. Required to make local vars work correctly -def main(): - global dockerComposeServicesYaml # The loaded memory YAML of all checked services - global toRun # Switch for which function to run when executed - global buildHooks # Where to place the options menu result - global currentServiceName # Name of the current service - global issues # Returned issues dict - global haltOnErrors # Turn on to allow erroring - - # runtime vars - portConflicts = [] - - # This lets the menu know whether to put " >> Options " or not - # This function is REQUIRED. - def checkForOptionsHook(): - try: - buildHooks["options"] = callable(runOptionsMenu) - except: - buildHooks["options"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPreBuildHook(): - try: - buildHooks["preBuildHook"] = callable(preBuild) - except: - buildHooks["preBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPostBuildHook(): - try: - buildHooks["postBuildHook"] = callable(postBuild) - except: - buildHooks["postBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForRunChecksHook(): - try: - buildHooks["runChecksHook"] = callable(runChecks) - except: - buildHooks["runChecksHook"] = False - return buildHooks - return buildHooks - - # This service will not check anything unless this is set - # This function is optional, and will run each time the menu is rendered - def runChecks(): - checkForIssues() - return [] - - # This function is optional, and will run after the docker-compose.yml file is written to disk. - def postBuild(): - return True - - # This function is optional, and will run just before the build docker-compose.yml code. - def preBuild(): - return True - - # ##################################### - # Supporting functions below - # ##################################### - - def checkForIssues(): - return True - - if haltOnErrors: - eval(toRun)() - else: - try: - eval(toRun)() - except: - pass - -# This check isn't required, but placed here for debugging purposes -global currentServiceName # Name of the current service -if currentServiceName == 'espruinohub': - main() -else: - print("Error. '{}' Tried to run 'espruinohub' config".format(currentServiceName)) diff --git a/.templates/example_template/example_build.py b/.templates/example_template/example_build.py deleted file mode 100755 index f55f8da3e..000000000 --- a/.templates/example_template/example_build.py +++ /dev/null @@ -1,312 +0,0 @@ -#!/usr/bin/env python3 - -# Be warned that globals and variable scopes do not function normally in this Python script. This is because this script is eval'd with exec. - -issues = {} # Returned issues dict -buildHooks = {} # Options, and others hooks -haltOnErrors = True - -# Main wrapper function. Required to make local vars work correctly -def main(): - from blessed import Terminal - from deps.chars import specialChars, commonTopBorder, commonBottomBorder, commonEmptyLine # Common functions used when creating menu - import types - import time - - global dockerComposeServicesYaml # The loaded memory YAML of all checked services - global toRun # Switch for which function to run when executed - global buildHooks # Where to place the options menu result - global currentServiceName # Name of the current service - global issues # Returned issues dict - global haltOnErrors # Turn on to allow erroring - global hideHelpText - - try: # If not already set, then set it to prevent errors. - hideHelpText = hideHelpText - except: - hideHelpText = False - - # runtime vars - portConflicts = [] - - # This lets the menu know whether to put " >> Options " or not - # This function is REQUIRED. - def checkForOptionsHook(): - try: - buildHooks["options"] = callable(runOptionsMenu) - except: - buildHooks["options"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPreBuildHook(): - try: - buildHooks["preBuildHook"] = callable(preBuild) - except: - buildHooks["preBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPostBuildHook(): - try: - buildHooks["postBuildHook"] = callable(postBuild) - except: - buildHooks["postBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForRunChecksHook(): - try: - buildHooks["runChecksHook"] = callable(runChecks) - except: - buildHooks["runChecksHook"] = False - return buildHooks - return buildHooks - - # This service will not check anything unless this is set - # This function is optional, and will run each time the menu is rendered - def runChecks(): - checkForIssues() - return [] - - # This is the menu that will run for " >> Options " - def runOptionsMenu(): - menuEntryPoint() - return True - - # This function is optional, and will run after the docker-compose.yml file is written to disk. - def postBuild(): - return True - - # This function is optional, and will run just before the build docker-compose.yml code. - def preBuild(): - return True - - # ##################################### - # Supporting functions below - # ##################################### - - def checkForIssues(): - for (index, serviceName) in enumerate(dockerComposeServicesYaml): - if not currentServiceName == serviceName: # Skip self - currentServicePorts = getExternalPorts(currentServiceName) - portConflicts = checkPortConflicts(serviceName, currentServicePorts) - if (len(portConflicts) > 0): - issues["portConflicts"] = portConflicts - - def getExternalPorts(serviceName): - externalPorts = [] - try: - yamlService = dockerComposeServicesYaml[serviceName] - if "ports" in yamlService: - for (index, port) in enumerate(yamlService["ports"]): - try: - externalAndInternal = port.split(":") - externalPorts.append(externalAndInternal[0]) - except: - pass - except: - pass - return externalPorts - - def checkPortConflicts(serviceName, currentPorts): - portConflicts = [] - if not currentServiceName == serviceName: - yamlService = dockerComposeServicesYaml[serviceName] - servicePorts = getExternalPorts(serviceName) - for (index, servicePort) in enumerate(servicePorts): - for (index, currentPort) in enumerate(currentPorts): - if (servicePort == currentPort): - portConflicts.append([servicePort, serviceName]) - return portConflicts - - - - # ##################################### - # Example menu below - # ##################################### - # You can build your menu system any way you like. This one is provided as an example. - # Checkout Blessed for full functionality, like text entry and so on at: https://blessed.readthedocs.io/en/latest/ - - # The functions the menu executes are below. They must be placed before the menu list 'menuItemsExample' - def menuCmdItem1(): - print("You chose item1!") - return True - - def menuCmdAnotherItem(): - print("This is another menu item") - return True - - def nop(): - return True - - def menuCmdStillAnotherItem(): - print("This is still another menu item") - return True - - def goBack(): - global selectionInProgress - selectionInProgress = False - return True - - # The actual menu - menuItemsExample = [ - ["Item 1", menuCmdItem1], - ["Another item", menuCmdAnotherItem], - ["I'm skipped!", nop, { "skip": True }], - ["Still another item", menuCmdStillAnotherItem], - ["Error item"], - ["Error item"], - ["Some custom thing", nop, { "customProperty": True }], - ["I'm also skipped!", nop, { "skip": True }], - ["Go back", goBack] - ] - - # Vars that the menu uses - global currentMenuItemIndex - global selectionInProgress - global menuNavigateDirection - global needsRender - - selectionInProgress = True - currentMenuItemIndex = 0 - menuNavigateDirection = 0 - needsRender = True - - # This is the main rendering function for the menu - def mainRender(menu, selection): - term = Terminal() - print(term.clear()) - - print(term.clear()) - print(term.move_y(term.height // 16)) - print(term.black_on_cornsilk4(term.center('IOTstack Example Commands'))) - print("") - print(term.center(commonTopBorder(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Select Command to run {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - - print(term.center(commonEmptyLine(renderMode))) - - lineLengthAtTextStart = 71 - - for (index, menuItem) in enumerate(menu): - toPrint = "" - if index == selection: # This checks if the current rendering item is the one that's selected - toPrint += ('{bv} -> {t.blue_on_green} {title} {t.normal} <-'.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) - else: - if len(menu[index]) > 2 and "customProperty" in menu[index][2] and menu[index][2]["customProperty"] == True: # A custom property check example - toPrint += ('{bv} {t.black_on_green} {title} {t.normal} '.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) - else: - toPrint += ('{bv} {t.normal} {title} '.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) - - for i in range(lineLengthAtTextStart - len(menuItem[0])): # Pad the remainder of the line - toPrint += " " - - toPrint += "{bv}".format(bv=specialChars[renderMode]["borderVertical"]) - - toPrint = term.center(toPrint) - - print(toPrint) - - print(term.center(commonEmptyLine(renderMode))) - if not hideHelpText: - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Enter] to run command {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Escape] to go back to build stack menu {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonBottomBorder(renderMode))) - - - def runSelection(selection): - term = Terminal() - if len(menuItemsExample[selection]) > 1 and isinstance(menuItemsExample[selection][1], types.FunctionType): - menuItemsExample[selection][1]() - else: - print(term.green_reverse('IOTstack Example Error: No function assigned to menu item: "{}"'.format(menuItemsExample[selection][0]))) - - def isMenuItemSelectable(menu, index): - if len(menu) > index: - if len(menu[index]) > 2: - if "skip" in menu[index][2] and menu[index][2]["skip"] == True: - return False - return True - - def menuEntryPoint(): - # These need to be reglobalised due to eval() - global currentMenuItemIndex - global selectionInProgress - global menuNavigateDirection - global needsRender - global hideHelpText - term = Terminal() - with term.fullscreen(): - menuNavigateDirection = 0 - mainRender(menuItemsExample, currentMenuItemIndex) - selectionInProgress = True - with term.cbreak(): - while selectionInProgress: - menuNavigateDirection = 0 - - if needsRender: # Only rerender when changed to prevent flickering - mainRender(menuItemsExample, currentMenuItemIndex) - needsRender = False - - key = term.inkey(esc_delay=0.05) - if key.is_sequence: - if key.name == 'KEY_TAB': - menuNavigateDirection += 1 - if key.name == 'KEY_DOWN': - menuNavigateDirection += 1 - if key.name == 'KEY_UP': - menuNavigateDirection -= 1 - if key.name == 'KEY_LEFT': - goBack() - if key.name == 'KEY_ENTER': - runSelection(currentMenuItemIndex) - if key.name == 'KEY_ESCAPE': - return True - elif key: - if key == 'h': # H pressed - if hideHelpText: - hideHelpText = False - else: - hideHelpText = True - mainRender(1, menuItemsExample, currentMenuItemIndex) - - if menuNavigateDirection != 0: # If a direction was pressed, find next selectable item - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(menuItemsExample) - needsRender = True - - while not isMenuItemSelectable(menuItemsExample, currentMenuItemIndex): - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(menuItemsExample) - return True - - - - - - # Entrypoint for execution - if haltOnErrors: - eval(toRun)() - else: - try: - eval(toRun)() - except: - pass - -# This check isn't required, but placed here for debugging purposes -global currentServiceName # Name of the current service -if currentServiceName == 'SERVICENAME': - main() -else: - print("Error. '{}' Tried to run 'SERVICENAME' config".format(currentServiceName)) diff --git a/.templates/example_template/example_service.yml b/.templates/example_template/example_service.yml deleted file mode 100755 index b7b54c1c6..000000000 --- a/.templates/example_template/example_service.yml +++ /dev/null @@ -1,11 +0,0 @@ -containerNameGoesHere: - container_name: containerNameGoesHere - restart: unless-stopped - image: example_template/core:latest - ports: - - "8070:80/tcp" # Always ensure the WUI port is first in the ports list. - - "1900:1900/udp" - env_file: - - ./services/example_template.env - volumes: - - ./volumes/example_template/:/opt/example_template/ diff --git a/.templates/gitea/build.py b/.templates/gitea/build.py deleted file mode 100755 index e783e8956..000000000 --- a/.templates/gitea/build.py +++ /dev/null @@ -1,309 +0,0 @@ -#!/usr/bin/env python3 - -issues = {} # Returned issues dict -buildHooks = {} # Options, and others hooks -haltOnErrors = True - -# Main wrapper function. Required to make local vars work correctly -def main(): - import os - import time - import ruamel.yaml - import signal - import sys - from blessed import Terminal - - from deps.chars import specialChars, commonTopBorder, commonBottomBorder, commonEmptyLine - from deps.consts import servicesDirectory, templatesDirectory - from deps.common_functions import getExternalPorts, getInternalPorts, checkPortConflicts, enterPortNumberWithWhiptail - - yaml = ruamel.yaml.YAML() - yaml.preserve_quotes = True - - global dockerComposeServicesYaml # The loaded memory YAML of all checked services - global toRun # Switch for which function to run when executed - global buildHooks # Where to place the options menu result - global currentServiceName # Name of the current service - global issues # Returned issues dict - global haltOnErrors # Turn on to allow erroring - global hideHelpText # Showing and hiding the help controls text - global serviceService - - serviceService = servicesDirectory + currentServiceName - serviceTemplate = templatesDirectory + currentServiceName - - try: # If not already set, then set it. - hideHelpText = hideHelpText - except: - hideHelpText = False - - # runtime vars - portConflicts = [] - - # This lets the menu know whether to put " >> Options " or not - # This function is REQUIRED. - def checkForOptionsHook(): - try: - buildHooks["options"] = callable(runOptionsMenu) - except: - buildHooks["options"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPreBuildHook(): - try: - buildHooks["preBuildHook"] = callable(preBuild) - except: - buildHooks["preBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPostBuildHook(): - try: - buildHooks["postBuildHook"] = callable(postBuild) - except: - buildHooks["postBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForRunChecksHook(): - try: - buildHooks["runChecksHook"] = callable(runChecks) - except: - buildHooks["runChecksHook"] = False - return buildHooks - return buildHooks - - # This service will not check anything unless this is set - # This function is optional, and will run each time the menu is rendered - def runChecks(): - checkForIssues() - return [] - - # This function is optional, and will run after the docker-compose.yml file is written to disk. - def postBuild(): - return True - - # This function is optional, and will run just before the build docker-compose.yml code. - def preBuild(): - return True - - # ##################################### - # Supporting functions below - # ##################################### - - def checkForIssues(): - for (index, serviceName) in enumerate(dockerComposeServicesYaml): - if not currentServiceName == serviceName: # Skip self - currentServicePorts = getExternalPorts(currentServiceName, dockerComposeServicesYaml) - portConflicts = checkPortConflicts(serviceName, currentServicePorts, dockerComposeServicesYaml) - if (len(portConflicts) > 0): - issues["portConflicts"] = portConflicts - - # ##################################### - # End Supporting functions - # ##################################### - - ############################ - # Menu Logic - ############################ - - global currentMenuItemIndex - global selectionInProgress - global menuNavigateDirection - global needsRender - - selectionInProgress = True - currentMenuItemIndex = 0 - menuNavigateDirection = 0 - needsRender = 1 - term = Terminal() - hotzoneLocation = [((term.height // 16) + 6), 0] - - def goBack(): - global selectionInProgress - global needsRender - selectionInProgress = False - needsRender = 1 - return True - - def enterPortNumberExec(): - # global term - global needsRender - global dockerComposeServicesYaml - externalPort = getExternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - internalPort = getInternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - newPortNumber = enterPortNumberWithWhiptail(term, dockerComposeServicesYaml, currentServiceName, hotzoneLocation, externalPort) - - if newPortNumber > 0: - dockerComposeServicesYaml[currentServiceName]["ports"][0] = "{newExtPort}:{oldIntPort}".format( - newExtPort = newPortNumber, - oldIntPort = internalPort - ) - createMenu() - needsRender = 1 - - def onResize(sig, action): - global giteaBuildOptions - global currentMenuItemIndex - mainRender(1, giteaBuildOptions, currentMenuItemIndex) - - giteaBuildOptions = [] - - def createMenu(): - global giteaBuildOptions - try: - giteaBuildOptions = [] - portNumber = getExternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - giteaBuildOptions.append([ - "Change external WUI Port Number from: {port}".format(port=portNumber), - enterPortNumberExec - ]) - except: # Error getting port - pass - giteaBuildOptions.append(["Go back", goBack]) - - def runOptionsMenu(): - createMenu() - menuEntryPoint() - return True - - def renderHotZone(term, menu, selection, hotzoneLocation): - lineLengthAtTextStart = 71 - print(term.move(hotzoneLocation[0], hotzoneLocation[1])) - for (index, menuItem) in enumerate(menu): - toPrint = "" - if index == selection: - toPrint += ('{bv} -> {t.blue_on_green} {title} {t.normal} <-'.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) - else: - toPrint += ('{bv} {t.normal} {title} '.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) - - for i in range(lineLengthAtTextStart - len(menuItem[0])): - toPrint += " " - - toPrint += "{bv}".format(bv=specialChars[renderMode]["borderVertical"]) - - toPrint = term.center(toPrint) - - print(toPrint) - - def mainRender(needsRender, menu, selection): - term = Terminal() - - if needsRender == 1: - print(term.clear()) - print(term.move_y(term.height // 16)) - print(term.black_on_cornsilk4(term.center('IOTstack Gitea Options'))) - print("") - print(term.center(commonTopBorder(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Select Option to configure {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - - if needsRender >= 1: - renderHotZone(term, menu, selection, hotzoneLocation) - - if needsRender == 1: - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - if not hideHelpText: - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Enter] to run command or save input {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Escape] to go back to build stack menu {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonBottomBorder(renderMode))) - - def runSelection(selection): - import types - global giteaBuildOptions - if len(giteaBuildOptions[selection]) > 1 and isinstance(giteaBuildOptions[selection][1], types.FunctionType): - giteaBuildOptions[selection][1]() - else: - print(term.green_reverse('IOTstack Error: No function assigned to menu item: "{}"'.format(nodeRedBuildOptions[selection][0]))) - - def isMenuItemSelectable(menu, index): - if len(menu) > index: - if len(menu[index]) > 2: - if menu[index][2]["skip"] == True: - return False - return True - - def menuEntryPoint(): - # These need to be reglobalised due to eval() - global currentMenuItemIndex - global selectionInProgress - global menuNavigateDirection - global needsRender - global hideHelpText - global giteaBuildOptions - term = Terminal() - with term.fullscreen(): - menuNavigateDirection = 0 - mainRender(needsRender, giteaBuildOptions, currentMenuItemIndex) - selectionInProgress = True - with term.cbreak(): - while selectionInProgress: - menuNavigateDirection = 0 - - if needsRender: # Only rerender when changed to prevent flickering - mainRender(needsRender, giteaBuildOptions, currentMenuItemIndex) - needsRender = 0 - - key = term.inkey(esc_delay=0.05) - if key.is_sequence: - if key.name == 'KEY_TAB': - menuNavigateDirection += 1 - if key.name == 'KEY_DOWN': - menuNavigateDirection += 1 - if key.name == 'KEY_UP': - menuNavigateDirection -= 1 - if key.name == 'KEY_LEFT': - goBack() - if key.name == 'KEY_ENTER': - runSelection(currentMenuItemIndex) - if key.name == 'KEY_ESCAPE': - return True - elif key: - if key == 'h': # H pressed - if hideHelpText: - hideHelpText = False - else: - hideHelpText = True - mainRender(1, giteaBuildOptions, currentMenuItemIndex) - - if menuNavigateDirection != 0: # If a direction was pressed, find next selectable item - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(giteaBuildOptions) - needsRender = 2 - - while not isMenuItemSelectable(giteaBuildOptions, currentMenuItemIndex): - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(giteaBuildOptions) - return True - - #################### - # End menu section - #################### - - - if haltOnErrors: - eval(toRun)() - else: - try: - eval(toRun)() - except: - pass - -# This check isn't required, but placed here for debugging purposes -global currentServiceName # Name of the current service -if currentServiceName == 'gitea': - main() -else: - print("Error. '{}' Tried to run 'gitea' config".format(currentServiceName)) diff --git a/.templates/grafana/build.py b/.templates/grafana/build.py deleted file mode 100755 index 7da3d8934..000000000 --- a/.templates/grafana/build.py +++ /dev/null @@ -1,306 +0,0 @@ -#!/usr/bin/env python3 - -issues = {} # Returned issues dict -buildHooks = {} # Options, and others hooks -haltOnErrors = True - -# Main wrapper function. Required to make local vars work correctly -def main(): - import os - import time - from blessed import Terminal - from deps.chars import specialChars, commonTopBorder, commonBottomBorder, commonEmptyLine, padText - from deps.consts import servicesDirectory, templatesDirectory, volumesDirectory - from deps.common_functions import getExternalPorts, getInternalPorts, checkPortConflicts, enterPortNumberWithWhiptail - - global dockerComposeServicesYaml # The loaded memory YAML of all checked services - global toRun # Switch for which function to run when executed - global buildHooks # Where to place the options menu result - global currentServiceName # Name of the current service - global issues # Returned issues dict - global haltOnErrors # Turn on to allow erroring - - # runtime vars - portConflicts = [] - serviceVolume = volumesDirectory + currentServiceName - serviceService = servicesDirectory + currentServiceName - serviceTemplate = templatesDirectory + currentServiceName - - try: # If not already set, then set it. - hideHelpText = hideHelpText - except: - hideHelpText = False - - documentationHint = 'https://sensorsiot.github.io/IOTstack/Containers/Grafana' - - # This lets the menu know whether to put " >> Options " or not - # This function is REQUIRED. - def checkForOptionsHook(): - try: - buildHooks["options"] = callable(runOptionsMenu) - except: - buildHooks["options"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPreBuildHook(): - try: - buildHooks["preBuildHook"] = callable(preBuild) - except: - buildHooks["preBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPostBuildHook(): - try: - buildHooks["postBuildHook"] = callable(postBuild) - except: - buildHooks["postBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForRunChecksHook(): - try: - buildHooks["runChecksHook"] = callable(runChecks) - except: - buildHooks["runChecksHook"] = False - return buildHooks - return buildHooks - - # This service will not check anything unless this is set - # This function is optional, and will run each time the menu is rendered - def runChecks(): - checkForIssues() - return [] - - # This function is optional, and will run after the docker-compose.yml file is written to disk. - def postBuild(): - return True - - # This function is optional, and will run just before the build docker-compose.yml code. - def preBuild(): - return True - - # ##################################### - # Supporting functions below - # ##################################### - - def checkForIssues(): - for (index, serviceName) in enumerate(dockerComposeServicesYaml): - if not currentServiceName == serviceName: # Skip self - currentServicePorts = getExternalPorts(currentServiceName, dockerComposeServicesYaml) - portConflicts = checkPortConflicts(serviceName, currentServicePorts, dockerComposeServicesYaml) - if (len(portConflicts) > 0): - issues["portConflicts"] = portConflicts - - ############################ - # Menu Logic - ############################ - - global currentMenuItemIndex - global selectionInProgress - global menuNavigateDirection - global needsRender - - selectionInProgress = True - currentMenuItemIndex = 0 - menuNavigateDirection = 0 - needsRender = 1 - term = Terminal() - hotzoneLocation = [((term.height // 16) + 6), 0] - - def goBack(): - global selectionInProgress - global needsRender - selectionInProgress = False - needsRender = 1 - return True - - def enterPortNumberExec(): - # global term - global needsRender - global dockerComposeServicesYaml - externalPort = getExternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - internalPort = getInternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - newPortNumber = enterPortNumberWithWhiptail(term, dockerComposeServicesYaml, currentServiceName, hotzoneLocation, externalPort) - - if newPortNumber > 0: - dockerComposeServicesYaml[currentServiceName]["ports"][0] = "{newExtPort}:{oldIntPort}".format( - newExtPort = newPortNumber, - oldIntPort = internalPort - ) - createMenu() - needsRender = 1 - - def onResize(sig, action): - global grafanaBuildOptions - global currentMenuItemIndex - mainRender(1, grafanaBuildOptions, currentMenuItemIndex) - - grafanaBuildOptions = [] - - def createMenu(): - global grafanaBuildOptions - try: - grafanaBuildOptions = [] - portNumber = getExternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - grafanaBuildOptions.append([ - "Change external WUI Port Number from: {port}".format(port=portNumber), - enterPortNumberExec - ]) - except: # Error getting port - pass - grafanaBuildOptions.append(["Go back", goBack]) - - def runOptionsMenu(): - createMenu() - menuEntryPoint() - return True - - def renderHotZone(term, menu, selection, hotzoneLocation): - lineLengthAtTextStart = 71 - print(term.move(hotzoneLocation[0], hotzoneLocation[1])) - for (index, menuItem) in enumerate(menu): - toPrint = "" - if index == selection: - toPrint += ('{bv} -> {t.blue_on_green} {title} {t.normal} <-'.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) - else: - toPrint += ('{bv} {t.normal} {title} '.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) - - for i in range(lineLengthAtTextStart - len(menuItem[0])): - toPrint += " " - - toPrint += "{bv}".format(bv=specialChars[renderMode]["borderVertical"]) - - toPrint = term.center(toPrint) - - print(toPrint) - - def mainRender(needsRender, menu, selection): - term = Terminal() - - if needsRender == 1: - print(term.clear()) - print(term.move_y(term.height // 16)) - print(term.black_on_cornsilk4(term.center('IOTstack Grafana Options'))) - print("") - print(term.center(commonTopBorder(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Select Option to configure {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - - if needsRender >= 1: - renderHotZone(term, menu, selection, hotzoneLocation) - - if needsRender == 1: - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - if not hideHelpText: - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Enter] to run command or save input {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Escape] to go back to build stack menu {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - if len(documentationHint) > 1: - if len(documentationHint) > 56: - documentationAndPadding = padText(documentationHint, 71) - print(term.center("{bv} Documentation: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} {dap} {bv}".format(bv=specialChars[renderMode]["borderVertical"], dap=documentationAndPadding))) - else: - documentationAndPadding = padText(documentationHint, 56) - print(term.center("{bv} Documentation: {dap} {bv}".format(bv=specialChars[renderMode]["borderVertical"], dap=documentationAndPadding))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonBottomBorder(renderMode))) - - def runSelection(selection): - import types - global grafanaBuildOptions - if len(grafanaBuildOptions[selection]) > 1 and isinstance(grafanaBuildOptions[selection][1], types.FunctionType): - grafanaBuildOptions[selection][1]() - else: - print(term.green_reverse('IOTstack Error: No function assigned to menu item: "{}"'.format(nodeRedBuildOptions[selection][0]))) - - def isMenuItemSelectable(menu, index): - if len(menu) > index: - if len(menu[index]) > 2: - if menu[index][2]["skip"] == True: - return False - return True - - def menuEntryPoint(): - # These need to be reglobalised due to eval() - global currentMenuItemIndex - global selectionInProgress - global menuNavigateDirection - global needsRender - global hideHelpText - global grafanaBuildOptions - term = Terminal() - with term.fullscreen(): - menuNavigateDirection = 0 - mainRender(needsRender, grafanaBuildOptions, currentMenuItemIndex) - selectionInProgress = True - with term.cbreak(): - while selectionInProgress: - menuNavigateDirection = 0 - - if needsRender: # Only rerender when changed to prevent flickering - mainRender(needsRender, grafanaBuildOptions, currentMenuItemIndex) - needsRender = 0 - - key = term.inkey(esc_delay=0.05) - if key.is_sequence: - if key.name == 'KEY_TAB': - menuNavigateDirection += 1 - if key.name == 'KEY_DOWN': - menuNavigateDirection += 1 - if key.name == 'KEY_UP': - menuNavigateDirection -= 1 - if key.name == 'KEY_LEFT': - goBack() - if key.name == 'KEY_ENTER': - runSelection(currentMenuItemIndex) - if key.name == 'KEY_ESCAPE': - return True - elif key: - if key == 'h': # H pressed - if hideHelpText: - hideHelpText = False - else: - hideHelpText = True - mainRender(1, grafanaBuildOptions, currentMenuItemIndex) - - if menuNavigateDirection != 0: # If a direction was pressed, find next selectable item - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(grafanaBuildOptions) - needsRender = 2 - - while not isMenuItemSelectable(grafanaBuildOptions, currentMenuItemIndex): - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(grafanaBuildOptions) - return True - - #################### - # End menu section - #################### - - if haltOnErrors: - eval(toRun)() - else: - try: - eval(toRun)() - except: - pass - -# This check isn't required, but placed here for debugging purposes -global currentServiceName # Name of the current service -if currentServiceName == 'grafana': - main() -else: - print("Error. '{}' Tried to run 'grafana' config".format(currentServiceName)) diff --git a/.templates/grafana/service.yml b/.templates/grafana/service.yml deleted file mode 100644 index da5caebcd..000000000 --- a/.templates/grafana/service.yml +++ /dev/null @@ -1,16 +0,0 @@ -grafana: - container_name: grafana - image: grafana/grafana - restart: unless-stopped - user: "0" - ports: - - "3000:3000" - environment: - - GF_PATHS_DATA=/var/lib/grafana - - GF_PATHS_LOGS=/var/log/grafana - volumes: - - ./volumes/grafana/data:/var/lib/grafana - - ./volumes/grafana/log:/var/log/grafana - networks: - - iotstack_nw - diff --git a/.templates/home_assistant/build.py b/.templates/home_assistant/build.py deleted file mode 100755 index 2a4e69b77..000000000 --- a/.templates/home_assistant/build.py +++ /dev/null @@ -1,319 +0,0 @@ -#!/usr/bin/env python3 - -issues = {} # Returned issues dict -buildHooks = {} # Options, and others hooks -haltOnErrors = True - -# Main wrapper function. Required to make local vars work correctly -def main(): - import os - import time - import ruamel.yaml - import signal - import sys - from blessed import Terminal - - from deps.chars import specialChars, commonTopBorder, commonBottomBorder, commonEmptyLine, padText - from deps.consts import servicesDirectory, templatesDirectory - from deps.common_functions import getExternalPorts, getInternalPorts, checkPortConflicts, enterPortNumberWithWhiptail - - yaml = ruamel.yaml.YAML() - yaml.preserve_quotes = True - - global dockerComposeServicesYaml # The loaded memory YAML of all checked services - global toRun # Switch for which function to run when executed - global buildHooks # Where to place the options menu result - global currentServiceName # Name of the current service - global issues # Returned issues dict - global haltOnErrors # Turn on to allow erroring - global hideHelpText # Showing and hiding the help controls text - global serviceService - - serviceService = servicesDirectory + currentServiceName - - try: # If not already set, then set it. - hideHelpText = hideHelpText - except: - hideHelpText = False - - documentationHint = 'https://sensorsiot.github.io/IOTstack/Containers/Home-Assistant' - - # runtime vars - portConflicts = [] - - # This lets the menu know whether to put " >> Options " or not - # This function is REQUIRED. - def checkForOptionsHook(): - try: - buildHooks["options"] = callable(runOptionsMenu) - except: - buildHooks["options"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPreBuildHook(): - try: - buildHooks["preBuildHook"] = callable(preBuild) - except: - buildHooks["preBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPostBuildHook(): - try: - buildHooks["postBuildHook"] = callable(postBuild) - except: - buildHooks["postBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForRunChecksHook(): - try: - buildHooks["runChecksHook"] = callable(runChecks) - except: - buildHooks["runChecksHook"] = False - return buildHooks - return buildHooks - - # This service will not check anything unless this is set - # This function is optional, and will run each time the menu is rendered - def runChecks(): - checkForIssues() - return [] - - # This function is optional, and will run after the docker-compose.yml file is written to disk. - def postBuild(): - return True - - # This function is optional, and will run just before the build docker-compose.yml code. - def preBuild(): - return True - - # ##################################### - # Supporting functions below - # ##################################### - - def checkForIssues(): - for (index, serviceName) in enumerate(dockerComposeServicesYaml): - if not currentServiceName == serviceName: # Skip self - currentServicePorts = getExternalPorts(currentServiceName, dockerComposeServicesYaml) - portConflicts = checkPortConflicts(serviceName, currentServicePorts, dockerComposeServicesYaml) - if (len(portConflicts) > 0): - issues["portConflicts"] = portConflicts - - # ##################################### - # End Supporting functions - # ##################################### - - ############################ - # Menu Logic - ############################ - - global currentMenuItemIndex - global selectionInProgress - global menuNavigateDirection - global needsRender - - selectionInProgress = True - currentMenuItemIndex = 0 - menuNavigateDirection = 0 - needsRender = 1 - term = Terminal() - hotzoneLocation = [((term.height // 16) + 6), 0] - - def goBack(): - global selectionInProgress - global needsRender - selectionInProgress = False - needsRender = 1 - return True - - def enterPortNumberExec(): - # global term - global needsRender - global dockerComposeServicesYaml - externalPort = getExternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - internalPort = getInternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - newPortNumber = enterPortNumberWithWhiptail(term, dockerComposeServicesYaml, currentServiceName, hotzoneLocation, externalPort) - - if newPortNumber > 0: - dockerComposeServicesYaml[currentServiceName]["ports"][0] = "{newExtPort}:{oldIntPort}".format( - newExtPort = newPortNumber, - oldIntPort = internalPort - ) - createMenu() - needsRender = 1 - - def onResize(sig, action): - global homeAssistantBuildOptions - global currentMenuItemIndex - mainRender(1, homeAssistantBuildOptions, currentMenuItemIndex) - - homeAssistantBuildOptions = [] - - def createMenu(): - global homeAssistantBuildOptions - try: - homeAssistantBuildOptions = [] - portNumber = getExternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - # homeAssistantBuildOptions.append([ - # "Change external WUI Port Number from: {port}".format(port=portNumber), - # enterPortNumberExec - # ]) - except: # Error getting port - pass - homeAssistantBuildOptions.append(["Go back", goBack]) - - def runOptionsMenu(): - createMenu() - menuEntryPoint() - return True - - def renderHotZone(term, menu, selection, hotzoneLocation): - lineLengthAtTextStart = 71 - print(term.move(hotzoneLocation[0], hotzoneLocation[1])) - for (index, menuItem) in enumerate(menu): - toPrint = "" - if index == selection: - toPrint += ('{bv} -> {t.blue_on_green} {title} {t.normal} <-'.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) - else: - toPrint += ('{bv} {t.normal} {title} '.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) - - for i in range(lineLengthAtTextStart - len(menuItem[0])): - toPrint += " " - - toPrint += "{bv}".format(bv=specialChars[renderMode]["borderVertical"]) - - toPrint = term.center(toPrint) - - print(toPrint) - - def mainRender(needsRender, menu, selection): - term = Terminal() - - if needsRender == 1: - print(term.clear()) - print(term.move_y(term.height // 16)) - print(term.black_on_cornsilk4(term.center('IOTstack Home Assistant (Container) Options'))) - print("") - print(term.center(commonTopBorder(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Select Option to configure {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - - if needsRender >= 1: - renderHotZone(term, menu, selection, hotzoneLocation) - - if needsRender == 1: - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - if not hideHelpText: - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Enter] to run command or save input {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Escape] to go back to build stack menu {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - if len(documentationHint) > 1: - if len(documentationHint) > 56: - documentationAndPadding = padText(documentationHint, 71) - print(term.center("{bv} Documentation: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} {dap} {bv}".format(bv=specialChars[renderMode]["borderVertical"], dap=documentationAndPadding))) - else: - documentationAndPadding = padText(documentationHint, 56) - print(term.center("{bv} Documentation: {dap} {bv}".format(bv=specialChars[renderMode]["borderVertical"], dap=documentationAndPadding))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonBottomBorder(renderMode))) - - def runSelection(selection): - import types - global homeAssistantBuildOptions - if len(homeAssistantBuildOptions[selection]) > 1 and isinstance(homeAssistantBuildOptions[selection][1], types.FunctionType): - homeAssistantBuildOptions[selection][1]() - else: - print(term.green_reverse('IOTstack Error: No function assigned to menu item: "{}"'.format(nodeRedBuildOptions[selection][0]))) - - def isMenuItemSelectable(menu, index): - if len(menu) > index: - if len(menu[index]) > 2: - if menu[index][2]["skip"] == True: - return False - return True - - def menuEntryPoint(): - # These need to be reglobalised due to eval() - global currentMenuItemIndex - global selectionInProgress - global menuNavigateDirection - global needsRender - global hideHelpText - global homeAssistantBuildOptions - term = Terminal() - with term.fullscreen(): - menuNavigateDirection = 0 - mainRender(needsRender, homeAssistantBuildOptions, currentMenuItemIndex) - selectionInProgress = True - with term.cbreak(): - while selectionInProgress: - menuNavigateDirection = 0 - - if needsRender: # Only rerender when changed to prevent flickering - mainRender(needsRender, homeAssistantBuildOptions, currentMenuItemIndex) - needsRender = 0 - - key = term.inkey(esc_delay=0.05) - if key.is_sequence: - if key.name == 'KEY_TAB': - menuNavigateDirection += 1 - if key.name == 'KEY_DOWN': - menuNavigateDirection += 1 - if key.name == 'KEY_UP': - menuNavigateDirection -= 1 - if key.name == 'KEY_LEFT': - goBack() - if key.name == 'KEY_ENTER': - runSelection(currentMenuItemIndex) - if key.name == 'KEY_ESCAPE': - return True - elif key: - if key == 'h': # H pressed - if hideHelpText: - hideHelpText = False - else: - hideHelpText = True - mainRender(1, homeAssistantBuildOptions, currentMenuItemIndex) - - if menuNavigateDirection != 0: # If a direction was pressed, find next selectable item - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(homeAssistantBuildOptions) - needsRender = 2 - - while not isMenuItemSelectable(homeAssistantBuildOptions, currentMenuItemIndex): - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(homeAssistantBuildOptions) - return True - - #################### - # End menu section - #################### - - - if haltOnErrors: - eval(toRun)() - else: - try: - eval(toRun)() - except: - pass - -# This check isn't required, but placed here for debugging purposes -global currentServiceName # Name of the current service -if currentServiceName == 'home_assistant': - main() -else: - print("Error. '{}' Tried to run 'home_assistant' config".format(currentServiceName)) diff --git a/.templates/home_assistant/service.yml b/.templates/home_assistant/service.yml deleted file mode 100755 index a9a0c8fe7..000000000 --- a/.templates/home_assistant/service.yml +++ /dev/null @@ -1,11 +0,0 @@ -home_assistant: - container_name: home_assistant - image: homeassistant/home-assistant:stable - restart: unless-stopped - ports: - - "8123:8123" - volumes: - - /etc/localtime:/etc/localtime:ro - - ./volumes/home_assistant:/config - #network_mode: host - diff --git a/.templates/homebridge/build.py b/.templates/homebridge/build.py deleted file mode 100755 index 2eeb8bdfa..000000000 --- a/.templates/homebridge/build.py +++ /dev/null @@ -1,318 +0,0 @@ -#!/usr/bin/env python3 - -issues = {} # Returned issues dict -buildHooks = {} # Options, and others hooks -haltOnErrors = True - -# Main wrapper function. Required to make local vars work correctly -def main(): - import os - import time - import ruamel.yaml - import signal - import sys - from blessed import Terminal - - from deps.chars import specialChars, commonTopBorder, commonBottomBorder, commonEmptyLine - from deps.consts import servicesDirectory, templatesDirectory, servicesFileName - from deps.common_functions import getExternalPorts, getInternalPorts, checkPortConflicts, enterPortNumberWithWhiptail - - yaml = ruamel.yaml.YAML() - yaml.preserve_quotes = True - - global dockerComposeServicesYaml # The loaded memory YAML of all checked services - global toRun # Switch for which function to run when executed - global buildHooks # Where to place the options menu result - global currentServiceName # Name of the current service - global issues # Returned issues dict - global haltOnErrors # Turn on to allow erroring - global hideHelpText # Showing and hiding the help controls text - global serviceService - - serviceService = servicesDirectory + currentServiceName - serviceTemplate = templatesDirectory + currentServiceName - - try: # If not already set, then set it. - hideHelpText = hideHelpText - except: - hideHelpText = False - - # runtime vars - portConflicts = [] - - # This lets the menu know whether to put " >> Options " or not - # This function is REQUIRED. - def checkForOptionsHook(): - try: - buildHooks["options"] = callable(runOptionsMenu) - except: - buildHooks["options"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPreBuildHook(): - try: - buildHooks["preBuildHook"] = callable(preBuild) - except: - buildHooks["preBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPostBuildHook(): - try: - buildHooks["postBuildHook"] = callable(postBuild) - except: - buildHooks["postBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForRunChecksHook(): - try: - buildHooks["runChecksHook"] = callable(runChecks) - except: - buildHooks["runChecksHook"] = False - return buildHooks - return buildHooks - - # This service will not check anything unless this is set - # This function is optional, and will run each time the menu is rendered - def runChecks(): - checkForIssues() - return [] - - # This function is optional, and will run after the docker-compose.yml file is written to disk. - def postBuild(): - return True - - # This function is optional, and will run just before the build docker-compose.yml code. - def preBuild(): - global dockerComposeServicesYaml - global currentServiceName - with open((r'%s/' % serviceTemplate) + servicesFileName) as objServiceFile: - serviceFile = yaml.load(objServiceFile) - if "environment" in serviceFile[currentServiceName]: - wuiPort = getInternalPorts(currentServiceName, serviceFile)[0] - for (envIndex, envName) in enumerate(serviceFile[currentServiceName]["environment"]): - # Load default values from service.yml and update compose file - dockerComposeServicesYaml[currentServiceName]["environment"][envIndex] = serviceFile[currentServiceName]["environment"][envIndex].replace("%WUIPort%", wuiPort) - return True - - # ##################################### - # Supporting functions below - # ##################################### - - def checkForIssues(): - for (index, serviceName) in enumerate(dockerComposeServicesYaml): - if not currentServiceName == serviceName: # Skip self - currentServicePorts = getExternalPorts(currentServiceName, dockerComposeServicesYaml) - portConflicts = checkPortConflicts(serviceName, currentServicePorts, dockerComposeServicesYaml) - if (len(portConflicts) > 0): - issues["portConflicts"] = portConflicts - - # ##################################### - # End Supporting functions - # ##################################### - - ############################ - # Menu Logic - ############################ - - global currentMenuItemIndex - global selectionInProgress - global menuNavigateDirection - global needsRender - - selectionInProgress = True - currentMenuItemIndex = 0 - menuNavigateDirection = 0 - needsRender = 1 - term = Terminal() - hotzoneLocation = [((term.height // 16) + 6), 0] - - def goBack(): - global selectionInProgress - global needsRender - selectionInProgress = False - needsRender = 1 - return True - - def enterPortNumberExec(): - # global term - global needsRender - global dockerComposeServicesYaml - externalPort = getExternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - internalPort = getInternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - newPortNumber = enterPortNumberWithWhiptail(term, dockerComposeServicesYaml, currentServiceName, hotzoneLocation, externalPort) - - if newPortNumber > 0: - dockerComposeServicesYaml[currentServiceName]["ports"][0] = "{newExtPort}:{oldIntPort}".format( - newExtPort = newPortNumber, - oldIntPort = internalPort - ) - createMenu() - needsRender = 1 - - def onResize(sig, action): - global homeBridgeBuildOptions - global currentMenuItemIndex - mainRender(1, homeBridgeBuildOptions, currentMenuItemIndex) - - homeBridgeBuildOptions = [] - - def createMenu(): - global homeBridgeBuildOptions - try: - homeBridgeBuildOptions = [] - portNumber = getExternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - homeBridgeBuildOptions.append([ - "Change external WUI Port Number from: {port}".format(port=portNumber), - enterPortNumberExec - ]) - except: # Error getting port - pass - homeBridgeBuildOptions.append(["Go back", goBack]) - - def runOptionsMenu(): - createMenu() - menuEntryPoint() - return True - - def renderHotZone(term, menu, selection, hotzoneLocation): - lineLengthAtTextStart = 71 - print(term.move(hotzoneLocation[0], hotzoneLocation[1])) - for (index, menuItem) in enumerate(menu): - toPrint = "" - if index == selection: - toPrint += ('{bv} -> {t.blue_on_green} {title} {t.normal} <-'.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) - else: - toPrint += ('{bv} {t.normal} {title} '.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) - - for i in range(lineLengthAtTextStart - len(menuItem[0])): - toPrint += " " - - toPrint += "{bv}".format(bv=specialChars[renderMode]["borderVertical"]) - - toPrint = term.center(toPrint) - - print(toPrint) - - def mainRender(needsRender, menu, selection): - term = Terminal() - - if needsRender == 1: - print(term.clear()) - print(term.move_y(term.height // 16)) - print(term.black_on_cornsilk4(term.center('IOTstack Homebridg Server Options'))) - print("") - print(term.center(commonTopBorder(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Select Option to configure {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - - if needsRender >= 1: - renderHotZone(term, menu, selection, hotzoneLocation) - - if needsRender == 1: - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - if not hideHelpText: - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Enter] to run command or save input {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Escape] to go back to build stack menu {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonBottomBorder(renderMode))) - - def runSelection(selection): - import types - global homeBridgeBuildOptions - if len(homeBridgeBuildOptions[selection]) > 1 and isinstance(homeBridgeBuildOptions[selection][1], types.FunctionType): - homeBridgeBuildOptions[selection][1]() - else: - print(term.green_reverse('IOTstack Error: No function assigned to menu item: "{}"'.format(nodeRedBuildOptions[selection][0]))) - - def isMenuItemSelectable(menu, index): - if len(menu) > index: - if len(menu[index]) > 2: - if menu[index][2]["skip"] == True: - return False - return True - - def menuEntryPoint(): - # These need to be reglobalised due to eval() - global currentMenuItemIndex - global selectionInProgress - global menuNavigateDirection - global needsRender - global hideHelpText - global homeBridgeBuildOptions - term = Terminal() - with term.fullscreen(): - menuNavigateDirection = 0 - mainRender(needsRender, homeBridgeBuildOptions, currentMenuItemIndex) - selectionInProgress = True - with term.cbreak(): - while selectionInProgress: - menuNavigateDirection = 0 - - if needsRender: # Only rerender when changed to prevent flickering - mainRender(needsRender, homeBridgeBuildOptions, currentMenuItemIndex) - needsRender = 0 - - key = term.inkey(esc_delay=0.05) - if key.is_sequence: - if key.name == 'KEY_TAB': - menuNavigateDirection += 1 - if key.name == 'KEY_DOWN': - menuNavigateDirection += 1 - if key.name == 'KEY_UP': - menuNavigateDirection -= 1 - if key.name == 'KEY_LEFT': - goBack() - if key.name == 'KEY_ENTER': - runSelection(currentMenuItemIndex) - if key.name == 'KEY_ESCAPE': - return True - elif key: - if key == 'h': # H pressed - if hideHelpText: - hideHelpText = False - else: - hideHelpText = True - mainRender(1, homeBridgeBuildOptions, currentMenuItemIndex) - - if menuNavigateDirection != 0: # If a direction was pressed, find next selectable item - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(homeBridgeBuildOptions) - needsRender = 2 - - while not isMenuItemSelectable(homeBridgeBuildOptions, currentMenuItemIndex): - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(homeBridgeBuildOptions) - return True - - #################### - # End menu section - #################### - - - if haltOnErrors: - eval(toRun)() - else: - try: - eval(toRun)() - except: - pass - -# This check isn't required, but placed here for debugging purposes -global currentServiceName # Name of the current service -if currentServiceName == 'homebridge': - main() -else: - print("Error. '{}' Tried to run 'homebridge' config".format(currentServiceName)) diff --git a/.templates/influxdb/build.py b/.templates/influxdb/build.py deleted file mode 100755 index c8c5e2b68..000000000 --- a/.templates/influxdb/build.py +++ /dev/null @@ -1,393 +0,0 @@ -#!/usr/bin/env python3 - -issues = {} # Returned issues dict -buildHooks = {} # Options, and others hooks -haltOnErrors = True - -# Main wrapper function. Required to make local vars work correctly -def main(): - import os - import time - import ruamel.yaml - import signal - import sys - import subprocess - from blessed import Terminal - - from deps.chars import specialChars, commonTopBorder, commonBottomBorder, commonEmptyLine, padText - from deps.consts import servicesDirectory, templatesDirectory, servicesFileName, buildSettingsFileName - from deps.common_functions import getExternalPorts, checkPortConflicts, generateRandomString - - yaml = ruamel.yaml.YAML() - yaml.preserve_quotes = True - - global dockerComposeServicesYaml # The loaded memory YAML of all checked services - global toRun # Switch for which function to run when executed - global buildHooks # Where to place the options menu result - global currentServiceName # Name of the current service - global issues # Returned issues dict - global haltOnErrors # Turn on to allow erroring - global hideHelpText # Showing and hiding the help controls text - global serviceService - - serviceService = servicesDirectory + currentServiceName - serviceTemplate = templatesDirectory + currentServiceName - buildSettings = serviceService + buildSettingsFileName - - try: # If not already set, then set it. - hideHelpText = hideHelpText - except: - hideHelpText = False - - documentationHint = 'https://sensorsiot.github.io/IOTstack/Containers/InfluxDB' - - # runtime vars - portConflicts = [] - - # This lets the menu know whether to put " >> Options " or not - # This function is REQUIRED. - def checkForOptionsHook(): - try: - buildHooks["options"] = callable(runOptionsMenu) - except: - buildHooks["options"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPreBuildHook(): - try: - buildHooks["preBuildHook"] = callable(preBuild) - except: - buildHooks["preBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPostBuildHook(): - try: - buildHooks["postBuildHook"] = callable(postBuild) - except: - buildHooks["postBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForRunChecksHook(): - try: - buildHooks["runChecksHook"] = callable(runChecks) - except: - buildHooks["runChecksHook"] = False - return buildHooks - return buildHooks - - # This service will not check anything unless this is set - # This function is optional, and will run each time the menu is rendered - def runChecks(): - checkForIssues() - return [] - - # This function is optional, and will run after the docker-compose.yml file is written to disk. - def postBuild(): - return True - - # This function is optional, and will run just before the build docker-compose.yml code. - def preBuild(): - # Multi-service: - with open((r'%s/' % serviceTemplate) + servicesFileName) as objServiceFile: - serviceYamlTemplate = yaml.load(objServiceFile) - - oldBuildCache = {} - try: - with open(r'%s' % buildCache) as objBuildCache: - oldBuildCache = yaml.load(objBuildCache) - except: - pass - - buildCacheServices = {} - if "services" in oldBuildCache: - buildCacheServices = oldBuildCache["services"] - - if not os.path.exists(serviceService): - os.makedirs(serviceService, exist_ok=True) - - if os.path.exists(buildSettings): - # Password randomisation - with open(r'%s' % buildSettings) as objBuildSettingsFile: - influxDbYamlBuildOptions = yaml.load(objBuildSettingsFile) - if ( - influxDbYamlBuildOptions["databasePasswordOption"] == "Randomise database password for this build" - or influxDbYamlBuildOptions["databasePasswordOption"] == "Randomise database password every build" - or influxDbYamlBuildOptions["databasePasswordOption"] == "Use default password for this build" - ): - if influxDbYamlBuildOptions["databasePasswordOption"] == "Use default password for this build": - randomPassword = "IOtSt4ckInfluX" - else: - randomPassword = generateRandomString() - for (index, serviceName) in enumerate(serviceYamlTemplate): - dockerComposeServicesYaml[serviceName] = serviceYamlTemplate[serviceName] - if "environment" in serviceYamlTemplate[serviceName]: - for (envIndex, envName) in enumerate(serviceYamlTemplate[serviceName]["environment"]): - envName = envName.replace("%randomPassword%", randomPassword) - dockerComposeServicesYaml[serviceName]["environment"][envIndex] = envName - - # Ensure you update the "Do nothing" and other 2 strings used for password settings in 'passwords.py' - if (influxDbYamlBuildOptions["databasePasswordOption"] == "Randomise database password for this build"): - influxDbYamlBuildOptions["databasePasswordOption"] = "Do nothing" - with open(buildSettings, 'w') as outputFile: - yaml.dump(influxDbYamlBuildOptions, outputFile) - else: # Do nothing - don't change password - for (index, serviceName) in enumerate(buildCacheServices): - if serviceName in buildCacheServices: # Load service from cache if exists (to maintain password) - dockerComposeServicesYaml[serviceName] = buildCacheServices[serviceName] - else: - dockerComposeServicesYaml[serviceName] = serviceYamlTemplate[serviceName] - - else: - print("InfluxDB Warning: Build settings file not found, using default password") - time.sleep(1) - randomPassword = "IOtSt4ckInfluX" - for (index, serviceName) in enumerate(serviceYamlTemplate): - dockerComposeServicesYaml[serviceName] = serviceYamlTemplate[serviceName] - if "environment" in serviceYamlTemplate[serviceName]: - for (envIndex, envName) in enumerate(serviceYamlTemplate[serviceName]["environment"]): - envName = envName.replace("%randomPassword%", randomPassword) - dockerComposeServicesYaml[serviceName]["environment"][envIndex] = envName - influxDbYamlBuildOptions = { - "version": "1", - "application": "IOTstack", - "service": "InfluxDB", - "comment": "InfluxDB Build Options" - } - - influxDbYamlBuildOptions["databasePasswordOption"] = "Do nothing" - with open(buildSettings, 'w') as outputFile: - yaml.dump(influxDbYamlBuildOptions, outputFile) - - return True - - # ##################################### - # Supporting functions below - # ##################################### - - def checkForIssues(): - for (index, serviceName) in enumerate(dockerComposeServicesYaml): - if not currentServiceName == serviceName: # Skip self - currentServicePorts = getExternalPorts(currentServiceName, dockerComposeServicesYaml) - portConflicts = checkPortConflicts(serviceName, currentServicePorts, dockerComposeServicesYaml) - if (len(portConflicts) > 0): - issues["portConflicts"] = portConflicts - - # ##################################### - # End Supporting functions - # ##################################### - - ############################ - # Menu Logic - ############################ - - global currentMenuItemIndex - global selectionInProgress - global menuNavigateDirection - global needsRender - - selectionInProgress = True - currentMenuItemIndex = 0 - menuNavigateDirection = 0 - needsRender = 1 - term = Terminal() - hotzoneLocation = [((term.height // 16) + 6), 0] - - def goBack(): - global selectionInProgress - global needsRender - selectionInProgress = False - needsRender = 1 - return True - - def setPasswordOptions(): - global needsRender - global hasRebuiltAddons - passwordOptionsMenuFilePath = "./.templates/{currentService}/passwords.py".format(currentService=currentServiceName) - with open(passwordOptionsMenuFilePath, "rb") as pythonDynamicImportFile: - code = compile(pythonDynamicImportFile.read(), passwordOptionsMenuFilePath, "exec") - execGlobals = { - "currentServiceName": currentServiceName, - "renderMode": renderMode - } - execLocals = {} - screenActive = False - exec(code, execGlobals, execLocals) - signal.signal(signal.SIGWINCH, onResize) - screenActive = True - needsRender = 1 - - def onResize(sig, action): - global influxDbBuildOptions - global currentMenuItemIndex - mainRender(1, influxDbBuildOptions, currentMenuItemIndex) - - influxDbBuildOptions = [] - - def createMenu(): - global influxDbBuildOptions - global serviceService - - influxDbBuildOptions = [] - # influxDbBuildOptions.append([ - # "InfluxDB Password Options", - # setPasswordOptions - # ]) - - influxDbBuildOptions.append(["Go back", goBack]) - - def runOptionsMenu(): - createMenu() - menuEntryPoint() - return True - - def renderHotZone(term, menu, selection, hotzoneLocation): - lineLengthAtTextStart = 71 - print(term.move(hotzoneLocation[0], hotzoneLocation[1])) - for (index, menuItem) in enumerate(menu): - toPrint = "" - if index == selection: - toPrint += ('{bv} -> {t.blue_on_green} {title} {t.normal} <-'.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) - else: - toPrint += ('{bv} {t.normal} {title} '.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) - - for i in range(lineLengthAtTextStart - len(menuItem[0])): - toPrint += " " - - toPrint += "{bv}".format(bv=specialChars[renderMode]["borderVertical"]) - - toPrint = term.center(toPrint) - - print(toPrint) - - def mainRender(needsRender, menu, selection): - term = Terminal() - - if needsRender == 1: - print(term.clear()) - print(term.move_y(term.height // 16)) - print(term.black_on_cornsilk4(term.center('IOTstack InfluxDB Options'))) - print("") - print(term.center(commonTopBorder(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Select Option to configure {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - - if needsRender >= 1: - renderHotZone(term, menu, selection, hotzoneLocation) - - if needsRender == 1: - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - if not hideHelpText: - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Enter] to run command or save input {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Escape] to go back to build stack menu {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - if len(documentationHint) > 1: - if len(documentationHint) > 56: - documentationAndPadding = padText(documentationHint, 71) - print(term.center("{bv} Documentation: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} {dap} {bv}".format(bv=specialChars[renderMode]["borderVertical"], dap=documentationAndPadding))) - else: - documentationAndPadding = padText(documentationHint, 56) - print(term.center("{bv} Documentation: {dap} {bv}".format(bv=specialChars[renderMode]["borderVertical"], dap=documentationAndPadding))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonBottomBorder(renderMode))) - - def runSelection(selection): - import types - global influxDbBuildOptions - if len(influxDbBuildOptions[selection]) > 1 and isinstance(influxDbBuildOptions[selection][1], types.FunctionType): - influxDbBuildOptions[selection][1]() - else: - print(term.green_reverse('IOTstack Error: No function assigned to menu item: "{}"'.format(nodeRedBuildOptions[selection][0]))) - - def isMenuItemSelectable(menu, index): - if len(menu) > index: - if len(menu[index]) > 2: - if menu[index][2]["skip"] == True: - return False - return True - - def menuEntryPoint(): - # These need to be reglobalised due to eval() - global currentMenuItemIndex - global selectionInProgress - global menuNavigateDirection - global needsRender - global hideHelpText - global influxDbBuildOptions - term = Terminal() - with term.fullscreen(): - menuNavigateDirection = 0 - mainRender(needsRender, influxDbBuildOptions, currentMenuItemIndex) - selectionInProgress = True - with term.cbreak(): - while selectionInProgress: - menuNavigateDirection = 0 - - if needsRender: # Only rerender when changed to prevent flickering - mainRender(needsRender, influxDbBuildOptions, currentMenuItemIndex) - needsRender = 0 - - key = term.inkey(esc_delay=0.05) - if key.is_sequence: - if key.name == 'KEY_TAB': - menuNavigateDirection += 1 - if key.name == 'KEY_DOWN': - menuNavigateDirection += 1 - if key.name == 'KEY_UP': - menuNavigateDirection -= 1 - if key.name == 'KEY_LEFT': - goBack() - if key.name == 'KEY_ENTER': - runSelection(currentMenuItemIndex) - if key.name == 'KEY_ESCAPE': - return True - elif key: - if key == 'h': # H pressed - if hideHelpText: - hideHelpText = False - else: - hideHelpText = True - mainRender(1, influxDbBuildOptions, currentMenuItemIndex) - - if menuNavigateDirection != 0: # If a direction was pressed, find next selectable item - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(influxDbBuildOptions) - needsRender = 2 - - while not isMenuItemSelectable(influxDbBuildOptions, currentMenuItemIndex): - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(influxDbBuildOptions) - return True - - #################### - # End menu section - #################### - - - if haltOnErrors: - eval(toRun)() - else: - try: - eval(toRun)() - except: - pass - -# This check isn't required, but placed here for debugging purposes -global currentServiceName # Name of the current service -if currentServiceName == 'influxdb': - main() -else: - print("Error. '{}' Tried to run 'influxdb' config".format(currentServiceName)) diff --git a/.templates/influxdb/passwords.py b/.templates/influxdb/passwords.py deleted file mode 100755 index fc27f915e..000000000 --- a/.templates/influxdb/passwords.py +++ /dev/null @@ -1,326 +0,0 @@ -#!/usr/bin/env python3 - -import signal - -def main(): - from blessed import Terminal - from deps.chars import specialChars, commonTopBorder, commonBottomBorder, commonEmptyLine - from deps.consts import servicesDirectory, templatesDirectory, buildSettingsFileName - import time - import subprocess - import ruamel.yaml - import os - - global signal - global currentServiceName - global menuSelectionInProgress - global mainMenuList - global currentMenuItemIndex - global renderMode - global paginationSize - global paginationStartIndex - global hideHelpText - - yaml = ruamel.yaml.YAML() - yaml.preserve_quotes = True - - try: # If not already set, then set it. - hideHelpText = hideHelpText - except: - hideHelpText = False - - term = Terminal() - hotzoneLocation = [((term.height // 16) + 6), 0] - paginationToggle = [10, term.height - 25] - paginationStartIndex = 0 - paginationSize = paginationToggle[0] - - serviceService = servicesDirectory + currentServiceName - serviceTemplate = templatesDirectory + currentServiceName - buildSettings = serviceService + buildSettingsFileName - - def goBack(): - global menuSelectionInProgress - global needsRender - menuSelectionInProgress = False - needsRender = 1 - return True - - mainMenuList = [] - - hotzoneLocation = [((term.height // 16) + 6), 0] - - menuSelectionInProgress = True - currentMenuItemIndex = 0 - menuNavigateDirection = 0 - - # Render Modes: - # 0 = No render needed - # 1 = Full render - # 2 = Hotzone only - needsRender = 1 - - def onResize(sig, action): - global mainMenuList - global currentMenuItemIndex - mainRender(1, mainMenuList, currentMenuItemIndex) - - def generateLineText(text, textLength=None, paddingBefore=0, lineLength=64): - result = "" - for i in range(paddingBefore): - result += " " - - textPrintableCharactersLength = textLength - - if (textPrintableCharactersLength) == None: - textPrintableCharactersLength = len(text) - - result += text - remainingSpace = lineLength - textPrintableCharactersLength - - for i in range(remainingSpace): - result += " " - - return result - - def renderHotZone(term, renderType, menu, selection, hotzoneLocation, paddingBefore = 4): - global paginationSize - selectedTextLength = len("-> ") - - print(term.move(hotzoneLocation[0], hotzoneLocation[1])) - - if paginationStartIndex >= 1: - print(term.center("{b} {uaf} {uaf}{uaf}{uaf} {ual} {b}".format( - b=specialChars[renderMode]["borderVertical"], - uaf=specialChars[renderMode]["upArrowFull"], - ual=specialChars[renderMode]["upArrowLine"] - ))) - else: - print(term.center(commonEmptyLine(renderMode))) - - for (index, menuItem) in enumerate(menu): # Menu loop - if index >= paginationStartIndex and index < paginationStartIndex + paginationSize: - lineText = generateLineText(menuItem[0], paddingBefore=paddingBefore) - - # Menu highlight logic - if index == selection: - formattedLineText = '-> {t.blue_on_green}{title}{t.normal} <-'.format(t=term, title=menuItem[0]) - paddedLineText = generateLineText(formattedLineText, textLength=len(menuItem[0]) + selectedTextLength, paddingBefore=paddingBefore - selectedTextLength) - toPrint = paddedLineText - else: - toPrint = '{title}{t.normal}'.format(t=term, title=lineText) - # ##### - - # Menu check render logic - if menuItem[1]["checked"]: - toPrint = " (X) " + toPrint - else: - toPrint = " ( ) " + toPrint - - toPrint = "{bv} {toPrint} {bv}".format(bv=specialChars[renderMode]["borderVertical"], toPrint=toPrint) # Generate border - toPrint = term.center(toPrint) # Center Text (All lines should have the same amount of printable characters) - # ##### - print(toPrint) - - if paginationStartIndex + paginationSize < len(menu): - print(term.center("{b} {daf} {daf}{daf}{daf} {dal} {b}".format( - b=specialChars[renderMode]["borderVertical"], - daf=specialChars[renderMode]["downArrowFull"], - dal=specialChars[renderMode]["downArrowLine"] - ))) - else: - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - - - def mainRender(needsRender, menu, selection): - global paginationStartIndex - global paginationSize - term = Terminal() - - if selection >= paginationStartIndex + paginationSize: - paginationStartIndex = selection - (paginationSize - 1) + 1 - needsRender = 1 - - if selection <= paginationStartIndex - 1: - paginationStartIndex = selection - needsRender = 1 - - if needsRender == 1: - print(term.clear()) - print(term.move_y(term.height // 16)) - print(term.black_on_cornsilk4(term.center('IOTstack InfluxDB Password Options'))) - print("") - print(term.center(commonTopBorder(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Select Password Option {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - - if needsRender >= 1: - renderHotZone(term, needsRender, menu, selection, hotzoneLocation) - - if needsRender == 1: - print(term.center(commonEmptyLine(renderMode))) - if not hideHelpText: - if term.height < 32: - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Not enough vertical room to render controls help text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - else: - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Space] to select option {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Enter] to build and save option {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Escape] to cancel changes {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonBottomBorder(renderMode))) - - def runSelection(selection): - import types - if len(mainMenuList[selection]) > 1 and isinstance(mainMenuList[selection][1], types.FunctionType): - mainMenuList[selection][1]() - else: - print(term.green_reverse('IOTstack Error: No function assigned to menu item: "{}"'.format(mainMenuList[selection][0]))) - - def isMenuItemSelectable(menu, index): - if len(menu) > index: - if len(menu[index]) > 1: - if "skip" in menu[index][1] and menu[index][1]["skip"] == True: - return False - return True - - def loadOptionsMenu(): - global mainMenuList - mainMenuList.append(["Use default database password for this build", { "checked": True }]) - mainMenuList.append(["Randomise database password for this build", { "checked": False }]) - mainMenuList.append(["Randomise database password every build", { "checked": False }]) - mainMenuList.append(["Do nothing", { "checked": False }]) - - def checkMenuItem(selection): - global mainMenuList - for (index, menuItem) in enumerate(mainMenuList): - mainMenuList[index][1]["checked"] = False - - mainMenuList[selection][1]["checked"] = True - - def saveOptions(): - try: - if not os.path.exists(serviceService): - os.makedirs(serviceService, exist_ok=True) - - if os.path.exists(buildSettings): - with open(r'%s' % buildSettings) as objBuildSettingsFile: - influxDbYamlBuildOptions = yaml.load(objBuildSettingsFile) - else: - influxDbYamlBuildOptions = { - "version": "1", - "application": "IOTstack", - "service": "influxdb", - "comment": "Build Settings", - } - - influxDbYamlBuildOptions["databasePasswordOption"] = "" - - for (index, menuOption) in enumerate(mainMenuList): - if menuOption[1]["checked"]: - influxDbYamlBuildOptions["databasePasswordOption"] = menuOption[0] - break - - with open(buildSettings, 'w') as outputFile: - yaml.dump(influxDbYamlBuildOptions, outputFile) - - except Exception as err: - print("Error saving InfluxDB Password options", currentServiceName) - print(err) - return False - return True - - def loadOptions(): - try: - if not os.path.exists(serviceService): - os.makedirs(serviceService, exist_ok=True) - - if os.path.exists(buildSettings): - with open(r'%s' % buildSettings) as objBuildSettingsFile: - influxDbYamlBuildOptions = yaml.load(objBuildSettingsFile) - - for (index, menuOption) in enumerate(mainMenuList): - if menuOption[0] == influxDbYamlBuildOptions["databasePasswordOption"]: - checkMenuItem(index) - break - - except Exception as err: - print("Error loading InfluxDB Password options", currentServiceName) - print(err) - return False - return True - - - if __name__ == 'builtins': - global signal - term = Terminal() - signal.signal(signal.SIGWINCH, onResize) - loadOptionsMenu() - loadOptions() - with term.fullscreen(): - menuNavigateDirection = 0 - mainRender(needsRender, mainMenuList, currentMenuItemIndex) - menuSelectionInProgress = True - with term.cbreak(): - while menuSelectionInProgress: - menuNavigateDirection = 0 - - if not needsRender == 0: # Only rerender when changed to prevent flickering - mainRender(needsRender, mainMenuList, currentMenuItemIndex) - needsRender = 0 - - key = term.inkey(esc_delay=0.05) - if key.is_sequence: - if key.name == 'KEY_TAB': - if paginationSize == paginationToggle[0]: - paginationSize = paginationToggle[1] - else: - paginationSize = paginationToggle[0] - mainRender(1, mainMenuList, currentMenuItemIndex) - if key.name == 'KEY_DOWN': - menuNavigateDirection += 1 - if key.name == 'KEY_UP': - menuNavigateDirection -= 1 - if key.name == 'KEY_ENTER': - if saveOptions(): - return True - else: - print("Something went wrong. Try saving the list again.") - if key.name == 'KEY_ESCAPE': - menuSelectionInProgress = False - return True - elif key: - if key == ' ': # Space pressed - checkMenuItem(currentMenuItemIndex) # Update checked list - needsRender = 2 - elif key == 'h': # H pressed - if hideHelpText: - hideHelpText = False - else: - hideHelpText = True - mainRender(1, mainMenuList, currentMenuItemIndex) - - if menuNavigateDirection != 0: # If a direction was pressed, find next selectable item - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(mainMenuList) - needsRender = 2 - - while not isMenuItemSelectable(mainMenuList, currentMenuItemIndex): - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(mainMenuList) - return True - - return True - -originalSignalHandler = signal.getsignal(signal.SIGINT) -main() -signal.signal(signal.SIGWINCH, originalSignalHandler) diff --git a/.templates/influxdb/terminal.sh b/.templates/influxdb/terminal.sh deleted file mode 100755 index 6486beb80..000000000 --- a/.templates/influxdb/terminal.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -echo "You are about to enter the influxdb console:" -echo "" -echo "IOTstack influxdb Documentation: https://sensorsiot.github.io/IOTstack/Containers/InfluxDB/" -echo "" -echo "to create a db: CREATE DATABASE myname" -echo "to show existing a databases: SHOW DATABASES" -echo "to use a specific db: USE myname" -echo "" -echo "to exit type: EXIT" -echo "" -echo "docker exec -it influxdb influx" - -docker exec -it influxdb influx diff --git a/.templates/mariadb/build.py b/.templates/mariadb/build.py deleted file mode 100755 index e8893b6e9..000000000 --- a/.templates/mariadb/build.py +++ /dev/null @@ -1,398 +0,0 @@ -#!/usr/bin/env python3 - -issues = {} # Returned issues dict -buildHooks = {} # Options, and others hooks -haltOnErrors = True - -# Main wrapper function. Required to make local vars work correctly -def main(): - import os - import time - import sys - import ruamel.yaml - import signal - import subprocess - from blessed import Terminal - - from deps.chars import specialChars, commonTopBorder, commonBottomBorder, commonEmptyLine, padText - from deps.consts import servicesDirectory, templatesDirectory, servicesFileName, buildSettingsFileName - from deps.common_functions import getExternalPorts, checkPortConflicts, generateRandomString - - yaml = ruamel.yaml.YAML() - yaml.preserve_quotes = True - - global dockerComposeServicesYaml # The loaded memory YAML of all checked services - global toRun # Switch for which function to run when executed - global buildHooks # Where to place the options menu result - global currentServiceName # Name of the current service - global issues # Returned issues dict - global haltOnErrors # Turn on to allow erroring - global hideHelpText # Showing and hiding the help controls text - global serviceService - - serviceService = servicesDirectory + currentServiceName - serviceTemplate = templatesDirectory + currentServiceName - buildSettings = serviceService + buildSettingsFileName - - try: # If not already set, then set it. - hideHelpText = hideHelpText - except: - hideHelpText = False - - documentationHint = 'https://sensorsiot.github.io/IOTstack/Containers/MariaDB' - - # runtime vars - portConflicts = [] - - # This lets the menu know whether to put " >> Options " or not - # This function is REQUIRED. - def checkForOptionsHook(): - try: - buildHooks["options"] = callable(runOptionsMenu) - except: - buildHooks["options"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPreBuildHook(): - try: - buildHooks["preBuildHook"] = callable(preBuild) - except: - buildHooks["preBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPostBuildHook(): - try: - buildHooks["postBuildHook"] = callable(postBuild) - except: - buildHooks["postBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForRunChecksHook(): - try: - buildHooks["runChecksHook"] = callable(runChecks) - except: - buildHooks["runChecksHook"] = False - return buildHooks - return buildHooks - - # This service will not check anything unless this is set - # This function is optional, and will run each time the menu is rendered - def runChecks(): - checkForIssues() - return [] - - # This function is optional, and will run after the docker-compose.yml file is written to disk. - def postBuild(): - return True - - # This function is optional, and will run just before the build docker-compose.yml code. - def preBuild(): - # Multi-service: - with open((r'%s/' % serviceTemplate) + servicesFileName) as objServiceFile: - serviceYamlTemplate = yaml.load(objServiceFile) - - oldBuildCache = {} - try: - with open(r'%s' % buildCache) as objBuildCache: - oldBuildCache = yaml.load(objBuildCache) - except: - pass - - buildCacheServices = {} - if "services" in oldBuildCache: - buildCacheServices = oldBuildCache["services"] - - if not os.path.exists(serviceService): - os.makedirs(serviceService, exist_ok=True) - - if os.path.exists(buildSettings): - # Password randomisation - with open(r'%s' % buildSettings) as objBuildSettingsFile: - mariaDbYamlBuildOptions = yaml.load(objBuildSettingsFile) - if ( - mariaDbYamlBuildOptions["databasePasswordOption"] == "Randomise database password for this build" - or mariaDbYamlBuildOptions["databasePasswordOption"] == "Randomise database password every build" - or mariaDbYamlBuildOptions["databasePasswordOption"] == "Use default password for this build" - ): - if mariaDbYamlBuildOptions["databasePasswordOption"] == "Use default password for this build": - newAdminPassword = "IOtSt4ckToorMariaDb" - newPassword = "IOtSt4ckmariaDbPw" - else: - newAdminPassword = generateRandomString() - newPassword = generateRandomString() - for (index, serviceName) in enumerate(serviceYamlTemplate): - dockerComposeServicesYaml[serviceName] = serviceYamlTemplate[serviceName] - if "environment" in serviceYamlTemplate[serviceName]: - for (envIndex, envName) in enumerate(serviceYamlTemplate[serviceName]["environment"]): - envName = envName.replace("%randomAdminPassword%", newAdminPassword) - envName = envName.replace("%randomPassword%", newPassword) - dockerComposeServicesYaml[serviceName]["environment"][envIndex] = envName - - # Ensure you update the "Do nothing" and other 2 strings used for password settings in 'passwords.py' - if (mariaDbYamlBuildOptions["databasePasswordOption"] == "Randomise database password for this build"): - mariaDbYamlBuildOptions["databasePasswordOption"] = "Do nothing" - with open(buildSettings, 'w') as outputFile: - yaml.dump(mariaDbYamlBuildOptions, outputFile) - else: # Do nothing - don't change password - for (index, serviceName) in enumerate(buildCacheServices): - if serviceName in buildCacheServices: # Load service from cache if exists (to maintain password) - dockerComposeServicesYaml[serviceName] = buildCacheServices[serviceName] - else: - dockerComposeServicesYaml[serviceName] = serviceYamlTemplate[serviceName] - - else: - print("MariaDB Warning: Build settings file not found, using default password") - time.sleep(1) - newAdminPassword = "IOtSt4ckToorMariaDb" - newPassword = "IOtSt4ckmariaDbPw" - for (index, serviceName) in enumerate(serviceYamlTemplate): - dockerComposeServicesYaml[serviceName] = serviceYamlTemplate[serviceName] - if "environment" in serviceYamlTemplate[serviceName]: - for (envIndex, envName) in enumerate(serviceYamlTemplate[serviceName]["environment"]): - envName = envName.replace("%randomAdminPassword%", newAdminPassword) - envName = envName.replace("%randomPassword%", newPassword) - dockerComposeServicesYaml[serviceName]["environment"][envIndex] = envName - mariaDbYamlBuildOptions = { - "version": "1", - "application": "IOTstack", - "service": "MariaDB", - "comment": "MariaDB Build Options" - } - - mariaDbYamlBuildOptions["databasePasswordOption"] = "Do nothing" - with open(buildSettings, 'w') as outputFile: - yaml.dump(mariaDbYamlBuildOptions, outputFile) - - return True - - # ##################################### - # Supporting functions below - # ##################################### - - def checkForIssues(): - for (index, serviceName) in enumerate(dockerComposeServicesYaml): - if not currentServiceName == serviceName: # Skip self - currentServicePorts = getExternalPorts(currentServiceName, dockerComposeServicesYaml) - portConflicts = checkPortConflicts(serviceName, currentServicePorts, dockerComposeServicesYaml) - if (len(portConflicts) > 0): - issues["portConflicts"] = portConflicts - - # ##################################### - # End Supporting functions - # ##################################### - - ############################ - # Menu Logic - ############################ - - global currentMenuItemIndex - global selectionInProgress - global menuNavigateDirection - global needsRender - - selectionInProgress = True - currentMenuItemIndex = 0 - menuNavigateDirection = 0 - needsRender = 1 - term = Terminal() - hotzoneLocation = [((term.height // 16) + 6), 0] - - def goBack(): - global selectionInProgress - global needsRender - selectionInProgress = False - needsRender = 1 - return True - - def setPasswordOptions(): - global needsRender - global hasRebuiltAddons - passwordOptionsMenuFilePath = "./.templates/{currentService}/passwords.py".format(currentService=currentServiceName) - with open(passwordOptionsMenuFilePath, "rb") as pythonDynamicImportFile: - code = compile(pythonDynamicImportFile.read(), passwordOptionsMenuFilePath, "exec") - execGlobals = { - "currentServiceName": currentServiceName, - "renderMode": renderMode - } - execLocals = {} - screenActive = False - exec(code, execGlobals, execLocals) - signal.signal(signal.SIGWINCH, onResize) - screenActive = True - needsRender = 1 - - def onResize(sig, action): - global mariaDbBuildOptions - global currentMenuItemIndex - mainRender(1, mariaDbBuildOptions, currentMenuItemIndex) - - mariaDbBuildOptions = [] - - def createMenu(): - global mariaDbBuildOptions - global serviceService - - mariaDbBuildOptions = [] - mariaDbBuildOptions.append([ - "MariaDB Password Options", - setPasswordOptions - ]) - - mariaDbBuildOptions.append(["Go back", goBack]) - - def runOptionsMenu(): - createMenu() - menuEntryPoint() - return True - - def renderHotZone(term, menu, selection, hotzoneLocation): - lineLengthAtTextStart = 71 - print(term.move(hotzoneLocation[0], hotzoneLocation[1])) - for (index, menuItem) in enumerate(menu): - toPrint = "" - if index == selection: - toPrint += ('{bv} -> {t.blue_on_green} {title} {t.normal} <-'.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) - else: - toPrint += ('{bv} {t.normal} {title} '.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) - - for i in range(lineLengthAtTextStart - len(menuItem[0])): - toPrint += " " - - toPrint += "{bv}".format(bv=specialChars[renderMode]["borderVertical"]) - - toPrint = term.center(toPrint) - - print(toPrint) - - def mainRender(needsRender, menu, selection): - term = Terminal() - - if needsRender == 1: - print(term.clear()) - print(term.move_y(term.height // 16)) - print(term.black_on_cornsilk4(term.center('IOTstack MariaDB Options'))) - print("") - print(term.center(commonTopBorder(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Select Option to configure {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - - if needsRender >= 1: - renderHotZone(term, menu, selection, hotzoneLocation) - - if needsRender == 1: - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - if not hideHelpText: - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Enter] to run command or save input {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Escape] to go back to build stack menu {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - if len(documentationHint) > 1: - if len(documentationHint) > 56: - documentationAndPadding = padText(documentationHint, 71) - print(term.center("{bv} Documentation: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} {dap} {bv}".format(bv=specialChars[renderMode]["borderVertical"], dap=documentationAndPadding))) - else: - documentationAndPadding = padText(documentationHint, 56) - print(term.center("{bv} Documentation: {dap} {bv}".format(bv=specialChars[renderMode]["borderVertical"], dap=documentationAndPadding))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonBottomBorder(renderMode))) - - def runSelection(selection): - import types - global mariaDbBuildOptions - if len(mariaDbBuildOptions[selection]) > 1 and isinstance(mariaDbBuildOptions[selection][1], types.FunctionType): - mariaDbBuildOptions[selection][1]() - else: - print(term.green_reverse('IOTstack Error: No function assigned to menu item: "{}"'.format(nodeRedBuildOptions[selection][0]))) - - def isMenuItemSelectable(menu, index): - if len(menu) > index: - if len(menu[index]) > 2: - if menu[index][2]["skip"] == True: - return False - return True - - def menuEntryPoint(): - # These need to be reglobalised due to eval() - global currentMenuItemIndex - global selectionInProgress - global menuNavigateDirection - global needsRender - global hideHelpText - global mariaDbBuildOptions - term = Terminal() - with term.fullscreen(): - menuNavigateDirection = 0 - mainRender(needsRender, mariaDbBuildOptions, currentMenuItemIndex) - selectionInProgress = True - with term.cbreak(): - while selectionInProgress: - menuNavigateDirection = 0 - - if needsRender: # Only rerender when changed to prevent flickering - mainRender(needsRender, mariaDbBuildOptions, currentMenuItemIndex) - needsRender = 0 - - key = term.inkey(esc_delay=0.05) - if key.is_sequence: - if key.name == 'KEY_TAB': - menuNavigateDirection += 1 - if key.name == 'KEY_DOWN': - menuNavigateDirection += 1 - if key.name == 'KEY_UP': - menuNavigateDirection -= 1 - if key.name == 'KEY_LEFT': - goBack() - if key.name == 'KEY_ENTER': - runSelection(currentMenuItemIndex) - if key.name == 'KEY_ESCAPE': - return True - elif key: - if key == 'h': # H pressed - if hideHelpText: - hideHelpText = False - else: - hideHelpText = True - mainRender(1, mariaDbBuildOptions, currentMenuItemIndex) - - if menuNavigateDirection != 0: # If a direction was pressed, find next selectable item - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(mariaDbBuildOptions) - needsRender = 2 - - while not isMenuItemSelectable(mariaDbBuildOptions, currentMenuItemIndex): - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(mariaDbBuildOptions) - return True - - #################### - # End menu section - #################### - - - if haltOnErrors: - eval(toRun)() - else: - try: - eval(toRun)() - except: - pass - -# This check isn't required, but placed here for debugging purposes -global currentServiceName # Name of the current service -if currentServiceName == 'mariadb': - main() -else: - print("Error. '{}' Tried to run 'mariadb' config".format(currentServiceName)) diff --git a/.templates/mariadb/passwords.py b/.templates/mariadb/passwords.py deleted file mode 100755 index 92f38fcf1..000000000 --- a/.templates/mariadb/passwords.py +++ /dev/null @@ -1,326 +0,0 @@ -#!/usr/bin/env python3 - -import signal - -def main(): - from blessed import Terminal - from deps.chars import specialChars, commonTopBorder, commonBottomBorder, commonEmptyLine - from deps.consts import servicesDirectory, templatesDirectory, buildSettingsFileName - import time - import subprocess - import ruamel.yaml - import os - - global signal - global currentServiceName - global menuSelectionInProgress - global mainMenuList - global currentMenuItemIndex - global renderMode - global paginationSize - global paginationStartIndex - global hideHelpText - - yaml = ruamel.yaml.YAML() - yaml.preserve_quotes = True - - try: # If not already set, then set it. - hideHelpText = hideHelpText - except: - hideHelpText = False - - term = Terminal() - hotzoneLocation = [((term.height // 16) + 6), 0] - paginationToggle = [10, term.height - 25] - paginationStartIndex = 0 - paginationSize = paginationToggle[0] - - serviceService = servicesDirectory + currentServiceName - serviceTemplate = templatesDirectory + currentServiceName - buildSettings = serviceService + buildSettingsFileName - - def goBack(): - global menuSelectionInProgress - global needsRender - menuSelectionInProgress = False - needsRender = 1 - return True - - mainMenuList = [] - - hotzoneLocation = [((term.height // 16) + 6), 0] - - menuSelectionInProgress = True - currentMenuItemIndex = 0 - menuNavigateDirection = 0 - - # Render Modes: - # 0 = No render needed - # 1 = Full render - # 2 = Hotzone only - needsRender = 1 - - def onResize(sig, action): - global mainMenuList - global currentMenuItemIndex - mainRender(1, mainMenuList, currentMenuItemIndex) - - def generateLineText(text, textLength=None, paddingBefore=0, lineLength=64): - result = "" - for i in range(paddingBefore): - result += " " - - textPrintableCharactersLength = textLength - - if (textPrintableCharactersLength) == None: - textPrintableCharactersLength = len(text) - - result += text - remainingSpace = lineLength - textPrintableCharactersLength - - for i in range(remainingSpace): - result += " " - - return result - - def renderHotZone(term, renderType, menu, selection, hotzoneLocation, paddingBefore = 4): - global paginationSize - selectedTextLength = len("-> ") - - print(term.move(hotzoneLocation[0], hotzoneLocation[1])) - - if paginationStartIndex >= 1: - print(term.center("{b} {uaf} {uaf}{uaf}{uaf} {ual} {b}".format( - b=specialChars[renderMode]["borderVertical"], - uaf=specialChars[renderMode]["upArrowFull"], - ual=specialChars[renderMode]["upArrowLine"] - ))) - else: - print(term.center(commonEmptyLine(renderMode))) - - for (index, menuItem) in enumerate(menu): # Menu loop - if index >= paginationStartIndex and index < paginationStartIndex + paginationSize: - lineText = generateLineText(menuItem[0], paddingBefore=paddingBefore) - - # Menu highlight logic - if index == selection: - formattedLineText = '-> {t.blue_on_green}{title}{t.normal} <-'.format(t=term, title=menuItem[0]) - paddedLineText = generateLineText(formattedLineText, textLength=len(menuItem[0]) + selectedTextLength, paddingBefore=paddingBefore - selectedTextLength) - toPrint = paddedLineText - else: - toPrint = '{title}{t.normal}'.format(t=term, title=lineText) - # ##### - - # Menu check render logic - if menuItem[1]["checked"]: - toPrint = " (X) " + toPrint - else: - toPrint = " ( ) " + toPrint - - toPrint = "{bv} {toPrint} {bv}".format(bv=specialChars[renderMode]["borderVertical"], toPrint=toPrint) # Generate border - toPrint = term.center(toPrint) # Center Text (All lines should have the same amount of printable characters) - # ##### - print(toPrint) - - if paginationStartIndex + paginationSize < len(menu): - print(term.center("{b} {daf} {daf}{daf}{daf} {dal} {b}".format( - b=specialChars[renderMode]["borderVertical"], - daf=specialChars[renderMode]["downArrowFull"], - dal=specialChars[renderMode]["downArrowLine"] - ))) - else: - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - - - def mainRender(needsRender, menu, selection): - global paginationStartIndex - global paginationSize - term = Terminal() - - if selection >= paginationStartIndex + paginationSize: - paginationStartIndex = selection - (paginationSize - 1) + 1 - needsRender = 1 - - if selection <= paginationStartIndex - 1: - paginationStartIndex = selection - needsRender = 1 - - if needsRender == 1: - print(term.clear()) - print(term.move_y(term.height // 16)) - print(term.black_on_cornsilk4(term.center('IOTstack MariaDB Password Options'))) - print("") - print(term.center(commonTopBorder(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Select Password Option {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - - if needsRender >= 1: - renderHotZone(term, needsRender, menu, selection, hotzoneLocation) - - if needsRender == 1: - print(term.center(commonEmptyLine(renderMode))) - if not hideHelpText: - if term.height < 32: - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Not enough vertical room to render controls help text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - else: - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Space] to select option {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Enter] to build and save option {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Escape] to cancel changes {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonBottomBorder(renderMode))) - - def runSelection(selection): - import types - if len(mainMenuList[selection]) > 1 and isinstance(mainMenuList[selection][1], types.FunctionType): - mainMenuList[selection][1]() - else: - print(term.green_reverse('IOTstack Error: No function assigned to menu item: "{}"'.format(mainMenuList[selection][0]))) - - def isMenuItemSelectable(menu, index): - if len(menu) > index: - if len(menu[index]) > 1: - if "skip" in menu[index][1] and menu[index][1]["skip"] == True: - return False - return True - - def loadOptionsMenu(): - global mainMenuList - mainMenuList.append(["Use default password for this build", { "checked": True }]) - mainMenuList.append(["Randomise database password for this build", { "checked": False }]) - mainMenuList.append(["Randomise database password every build", { "checked": False }]) - mainMenuList.append(["Do nothing", { "checked": False }]) - - def checkMenuItem(selection): - global mainMenuList - for (index, menuItem) in enumerate(mainMenuList): - mainMenuList[index][1]["checked"] = False - - mainMenuList[selection][1]["checked"] = True - - def saveOptions(): - try: - if not os.path.exists(serviceService): - os.makedirs(serviceService, exist_ok=True) - - if os.path.exists(buildSettings): - with open(r'%s' % buildSettings) as objBuildSettingsFile: - mariaDbYamlBuildOptions = yaml.load(objBuildSettingsFile) - else: - mariaDbYamlBuildOptions = { - "version": "1", - "application": "IOTstack", - "service": "mariadb", - "comment": "Build Settings", - } - - mariaDbYamlBuildOptions["databasePasswordOption"] = "" - - for (index, menuOption) in enumerate(mainMenuList): - if menuOption[1]["checked"]: - mariaDbYamlBuildOptions["databasePasswordOption"] = menuOption[0] - break - - with open(buildSettings, 'w') as outputFile: - yaml.dump(mariaDbYamlBuildOptions, outputFile) - - except Exception as err: - print("Error saving MariaDB Password options", currentServiceName) - print(err) - return False - return True - - def loadOptions(): - try: - if not os.path.exists(serviceService): - os.makedirs(serviceService, exist_ok=True) - - if os.path.exists(buildSettings): - with open(r'%s' % buildSettings) as objBuildSettingsFile: - mariaDbYamlBuildOptions = yaml.load(objBuildSettingsFile) - - for (index, menuOption) in enumerate(mainMenuList): - if menuOption[0] == mariaDbYamlBuildOptions["databasePasswordOption"]: - checkMenuItem(index) - break - - except Exception as err: - print("Error loading MariaDB Password options", currentServiceName) - print(err) - return False - return True - - - if __name__ == 'builtins': - global signal - term = Terminal() - signal.signal(signal.SIGWINCH, onResize) - loadOptionsMenu() - loadOptions() - with term.fullscreen(): - menuNavigateDirection = 0 - mainRender(needsRender, mainMenuList, currentMenuItemIndex) - menuSelectionInProgress = True - with term.cbreak(): - while menuSelectionInProgress: - menuNavigateDirection = 0 - - if not needsRender == 0: # Only rerender when changed to prevent flickering - mainRender(needsRender, mainMenuList, currentMenuItemIndex) - needsRender = 0 - - key = term.inkey(esc_delay=0.05) - if key.is_sequence: - if key.name == 'KEY_TAB': - if paginationSize == paginationToggle[0]: - paginationSize = paginationToggle[1] - else: - paginationSize = paginationToggle[0] - mainRender(1, mainMenuList, currentMenuItemIndex) - if key.name == 'KEY_DOWN': - menuNavigateDirection += 1 - if key.name == 'KEY_UP': - menuNavigateDirection -= 1 - if key.name == 'KEY_ENTER': - if saveOptions(): - return True - else: - print("Something went wrong. Try saving the list again.") - if key.name == 'KEY_ESCAPE': - menuSelectionInProgress = False - return True - elif key: - if key == ' ': # Space pressed - checkMenuItem(currentMenuItemIndex) # Update checked list - needsRender = 2 - elif key == 'h': # H pressed - if hideHelpText: - hideHelpText = False - else: - hideHelpText = True - mainRender(1, mainMenuList, currentMenuItemIndex) - - if menuNavigateDirection != 0: # If a direction was pressed, find next selectable item - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(mainMenuList) - needsRender = 2 - - while not isMenuItemSelectable(mainMenuList, currentMenuItemIndex): - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(mainMenuList) - return True - - return True - -originalSignalHandler = signal.getsignal(signal.SIGINT) -main() -signal.signal(signal.SIGWINCH, originalSignalHandler) diff --git a/.templates/mariadb/terminal.sh b/.templates/mariadb/terminal.sh deleted file mode 100755 index 0aa87ed20..000000000 --- a/.templates/mariadb/terminal.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -echo "run 'mysql -uroot -p' for terminal access" -echo "" -echo "IOTstack mariadb Documentation: https://sensorsiot.github.io/IOTstack/Containers/MariaDB/" -echo "" -echo "docker exec -it mariadb bash" - -docker exec -it mariadb bash diff --git a/.templates/mosquitto/docker-entrypoint.sh b/.templates/mosquitto/docker-entrypoint.sh deleted file mode 100644 index 835b0c781..000000000 --- a/.templates/mosquitto/docker-entrypoint.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/ash -set -e - -# Set permissions -user="$(id -u)" -if [ "$user" = '0' -a -d "/mosquitto" ]; then - - rsync -arp --ignore-existing /${IOTSTACK_DEFAULTS_DIR}/ "/mosquitto" - - chown -R mosquitto:mosquitto /mosquitto - -fi - -exec "$@" - diff --git a/.templates/motioneye/build.py b/.templates/motioneye/build.py deleted file mode 100755 index 5d46dbae5..000000000 --- a/.templates/motioneye/build.py +++ /dev/null @@ -1,332 +0,0 @@ -#!/usr/bin/env python3 - -issues = {} # Returned issues dict -buildHooks = {} # Options, and others hooks -haltOnErrors = True - -# Main wrapper function. Required to make local vars work correctly -def main(): - import os - import time - import ruamel.yaml - import signal - import sys - from blessed import Terminal - - from deps.chars import specialChars, commonTopBorder, commonBottomBorder, commonEmptyLine, padText - from deps.consts import servicesDirectory, templatesDirectory - from deps.common_functions import getExternalPorts, getInternalPorts, checkPortConflicts, enterPortNumberWithWhiptail - - yaml = ruamel.yaml.YAML() - yaml.preserve_quotes = True - - global dockerComposeServicesYaml # The loaded memory YAML of all checked services - global toRun # Switch for which function to run when executed - global buildHooks # Where to place the options menu result - global currentServiceName # Name of the current service - global issues # Returned issues dict - global haltOnErrors # Turn on to allow erroring - global hideHelpText # Showing and hiding the help controls text - global serviceService - - serviceService = servicesDirectory + currentServiceName - serviceTemplate = templatesDirectory + currentServiceName - - try: # If not already set, then set it. - hideHelpText = hideHelpText - except: - hideHelpText = False - - documentationHint = 'https://sensorsiot.github.io/IOTstack/Containers/MotionEye' - - # runtime vars - portConflicts = [] - - # This lets the menu know whether to put " >> Options " or not - # This function is REQUIRED. - def checkForOptionsHook(): - try: - buildHooks["options"] = callable(runOptionsMenu) - except: - buildHooks["options"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPreBuildHook(): - try: - buildHooks["preBuildHook"] = callable(preBuild) - except: - buildHooks["preBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPostBuildHook(): - try: - buildHooks["postBuildHook"] = callable(postBuild) - except: - buildHooks["postBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForRunChecksHook(): - try: - buildHooks["runChecksHook"] = callable(runChecks) - except: - buildHooks["runChecksHook"] = False - return buildHooks - return buildHooks - - # This service will not check anything unless this is set - # This function is optional, and will run each time the menu is rendered - def runChecks(): - checkForIssues() - return [] - - # This function is optional, and will run after the docker-compose.yml file is written to disk. - def postBuild(): - return True - - # This function is optional, and will run just before the build docker-compose.yml code. - def preBuild(): - global dockerComposeServicesYaml - global currentServiceName - if os.path.exists('/dev/video0'): - dockerComposeServicesYaml[currentServiceName]["devices"] = [ - '/dev/video0' - ] - - # Setup service directory - if not os.path.exists(serviceService): - os.makedirs(serviceService, exist_ok=True) - os.makedirs(serviceService + '/etc_motioneye', exist_ok=True) - os.makedirs(serviceService + '/var_lib_motioneye', exist_ok=True) - return True - - # ##################################### - # Supporting functions below - # ##################################### - - def checkForIssues(): - for (index, serviceName) in enumerate(dockerComposeServicesYaml): - if not currentServiceName == serviceName: # Skip self - currentServicePorts = getExternalPorts(currentServiceName, dockerComposeServicesYaml) - portConflicts = checkPortConflicts(serviceName, currentServicePorts, dockerComposeServicesYaml) - if (len(portConflicts) > 0): - issues["portConflicts"] = portConflicts - - # ##################################### - # End Supporting functions - # ##################################### - - ############################ - # Menu Logic - ############################ - - global currentMenuItemIndex - global selectionInProgress - global menuNavigateDirection - global needsRender - - selectionInProgress = True - currentMenuItemIndex = 0 - menuNavigateDirection = 0 - needsRender = 1 - term = Terminal() - hotzoneLocation = [((term.height // 16) + 6), 0] - - def goBack(): - global selectionInProgress - global needsRender - selectionInProgress = False - needsRender = 1 - return True - - def enterPortNumberExec(): - # global term - global needsRender - global dockerComposeServicesYaml - externalPort = getExternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - internalPort = getInternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - newPortNumber = enterPortNumberWithWhiptail(term, dockerComposeServicesYaml, currentServiceName, hotzoneLocation, externalPort) - - if newPortNumber > 0: - dockerComposeServicesYaml[currentServiceName]["ports"][0] = "{newExtPort}:{oldIntPort}".format( - newExtPort = newPortNumber, - oldIntPort = internalPort - ) - createMenu() - needsRender = 1 - - def onResize(sig, action): - global motionEyeBuildOptions - global currentMenuItemIndex - mainRender(1, motionEyeBuildOptions, currentMenuItemIndex) - - motionEyeBuildOptions = [] - - def createMenu(): - global motionEyeBuildOptions - try: - motionEyeBuildOptions = [] - portNumber = getExternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - motionEyeBuildOptions.append([ - "Change external WUI Port Number from: {port}".format(port=portNumber), - enterPortNumberExec - ]) - except: # Error getting port - pass - motionEyeBuildOptions.append(["Go back", goBack]) - - def runOptionsMenu(): - createMenu() - menuEntryPoint() - return True - - def renderHotZone(term, menu, selection, hotzoneLocation): - lineLengthAtTextStart = 71 - print(term.move(hotzoneLocation[0], hotzoneLocation[1])) - for (index, menuItem) in enumerate(menu): - toPrint = "" - if index == selection: - toPrint += ('{bv} -> {t.blue_on_green} {title} {t.normal} <-'.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) - else: - toPrint += ('{bv} {t.normal} {title} '.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) - - for i in range(lineLengthAtTextStart - len(menuItem[0])): - toPrint += " " - - toPrint += "{bv}".format(bv=specialChars[renderMode]["borderVertical"]) - - toPrint = term.center(toPrint) - - print(toPrint) - - def mainRender(needsRender, menu, selection): - term = Terminal() - - if needsRender == 1: - print(term.clear()) - print(term.move_y(term.height // 16)) - print(term.black_on_cornsilk4(term.center('IOTstack MotionEye Options'))) - print("") - print(term.center(commonTopBorder(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Select Option to configure {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - - if needsRender >= 1: - renderHotZone(term, menu, selection, hotzoneLocation) - - if needsRender == 1: - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - if not hideHelpText: - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Enter] to run command or save input {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Escape] to go back to build stack menu {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - if len(documentationHint) > 1: - if len(documentationHint) > 56: - documentationAndPadding = padText(documentationHint, 71) - print(term.center("{bv} Documentation: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} {dap} {bv}".format(bv=specialChars[renderMode]["borderVertical"], dap=documentationAndPadding))) - else: - documentationAndPadding = padText(documentationHint, 56) - print(term.center("{bv} Documentation: {dap} {bv}".format(bv=specialChars[renderMode]["borderVertical"], dap=documentationAndPadding))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonBottomBorder(renderMode))) - - def runSelection(selection): - import types - global motionEyeBuildOptions - if len(motionEyeBuildOptions[selection]) > 1 and isinstance(motionEyeBuildOptions[selection][1], types.FunctionType): - motionEyeBuildOptions[selection][1]() - else: - print(term.green_reverse('IOTstack Error: No function assigned to menu item: "{}"'.format(nodeRedBuildOptions[selection][0]))) - - def isMenuItemSelectable(menu, index): - if len(menu) > index: - if len(menu[index]) > 2: - if menu[index][2]["skip"] == True: - return False - return True - - def menuEntryPoint(): - # These need to be reglobalised due to eval() - global currentMenuItemIndex - global selectionInProgress - global menuNavigateDirection - global needsRender - global hideHelpText - global motionEyeBuildOptions - term = Terminal() - with term.fullscreen(): - menuNavigateDirection = 0 - mainRender(needsRender, motionEyeBuildOptions, currentMenuItemIndex) - selectionInProgress = True - with term.cbreak(): - while selectionInProgress: - menuNavigateDirection = 0 - - if needsRender: # Only rerender when changed to prevent flickering - mainRender(needsRender, motionEyeBuildOptions, currentMenuItemIndex) - needsRender = 0 - - key = term.inkey(esc_delay=0.05) - if key.is_sequence: - if key.name == 'KEY_TAB': - menuNavigateDirection += 1 - if key.name == 'KEY_DOWN': - menuNavigateDirection += 1 - if key.name == 'KEY_UP': - menuNavigateDirection -= 1 - if key.name == 'KEY_LEFT': - goBack() - if key.name == 'KEY_ENTER': - runSelection(currentMenuItemIndex) - if key.name == 'KEY_ESCAPE': - return True - elif key: - if key == 'h': # H pressed - if hideHelpText: - hideHelpText = False - else: - hideHelpText = True - mainRender(1, motionEyeBuildOptions, currentMenuItemIndex) - - if menuNavigateDirection != 0: # If a direction was pressed, find next selectable item - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(motionEyeBuildOptions) - needsRender = 2 - - while not isMenuItemSelectable(motionEyeBuildOptions, currentMenuItemIndex): - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(motionEyeBuildOptions) - return True - - #################### - # End menu section - #################### - - - if haltOnErrors: - eval(toRun)() - else: - try: - eval(toRun)() - except: - pass - -# This check isn't required, but placed here for debugging purposes -global currentServiceName # Name of the current service -if currentServiceName == 'motioneye': - main() -else: - print("Error. '{}' Tried to run 'motioneye' config".format(currentServiceName)) diff --git a/.templates/nextcloud/build.py b/.templates/nextcloud/build.py deleted file mode 100755 index 88aec5d90..000000000 --- a/.templates/nextcloud/build.py +++ /dev/null @@ -1,432 +0,0 @@ -#!/usr/bin/env python3 - -issues = {} # Returned issues dict -buildHooks = {} # Options, and others hooks -haltOnErrors = True - -# Main wrapper function. Required to make local vars work correctly -def main(): - import os - import time - import ruamel.yaml - import signal - import sys - import subprocess - - from blessed import Terminal - from deps.chars import specialChars, commonTopBorder, commonBottomBorder, commonEmptyLine, padText - from deps.consts import servicesDirectory, templatesDirectory, volumesDirectory, buildSettingsFileName, buildCache, servicesFileName - from deps.common_functions import getExternalPorts, getInternalPorts, checkPortConflicts, enterPortNumberWithWhiptail, generateRandomString - - yaml = ruamel.yaml.YAML() - yaml.preserve_quotes = True - - global dockerComposeServicesYaml # The loaded memory YAML of all checked services - global toRun # Switch for which function to run when executed - global buildHooks # Where to place the options menu result - global currentServiceName # Name of the current service - global issues # Returned issues dict - global haltOnErrors # Turn on to allow erroring - global hideHelpText # Showing and hiding the help controls text - global serviceService - - serviceVolume = volumesDirectory + currentServiceName - serviceService = servicesDirectory + currentServiceName - serviceTemplate = templatesDirectory + currentServiceName - buildSettings = serviceService + buildSettingsFileName - - try: # If not already set, then set it. - hideHelpText = hideHelpText - except: - hideHelpText = False - - documentationHint = 'https://sensorsiot.github.io/IOTstack/Containers/NextCloud' - - # runtime vars - portConflicts = [] - - # This lets the menu know whether to put " >> Options " or not - # This function is REQUIRED. - def checkForOptionsHook(): - try: - buildHooks["options"] = callable(runOptionsMenu) - except: - buildHooks["options"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPreBuildHook(): - try: - buildHooks["preBuildHook"] = callable(preBuild) - except: - buildHooks["preBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPostBuildHook(): - try: - buildHooks["postBuildHook"] = callable(postBuild) - except: - buildHooks["postBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForRunChecksHook(): - try: - buildHooks["runChecksHook"] = callable(runChecks) - except: - buildHooks["runChecksHook"] = False - return buildHooks - return buildHooks - - # This service will not check anything unless this is set - # This function is optional, and will run each time the menu is rendered - def runChecks(): - checkForIssues() - return [] - - # This function is optional, and will run after the docker-compose.yml file is written to disk. - def postBuild(): - commandToRun = "chmod -R 0770 %s" % serviceVolume + '/html' - print('[Nextcloud::postBuild]: %s' % commandToRun) - subprocess.call(commandToRun, shell=True) - return True - - # This function is optional, and will run just before the build docker-compose.yml code. - def preBuild(): - global dockerComposeServicesYaml - # Setup service directory - if not os.path.exists(serviceService): - os.makedirs(serviceService, exist_ok=True) - - os.makedirs(serviceVolume, exist_ok=True) - os.makedirs(serviceVolume + '/html', exist_ok=True) - - # Multi-service: - with open((r'%s/' % serviceTemplate) + servicesFileName) as objServiceFile: - servicesListed = yaml.load(objServiceFile) - - oldBuildCache = {} - try: - with open(r'%s' % buildCache) as objBuildCache: - oldBuildCache = yaml.load(objBuildCache) - except: - pass - - buildCacheServices = {} - if "services" in oldBuildCache: - buildCacheServices = oldBuildCache["services"] - - if not os.path.exists(serviceService): - os.makedirs(serviceService, exist_ok=True) - - if os.path.exists(buildSettings): - - # Password randomisation - with open(r'%s' % buildSettings) as objBuildSettingsFile: - nextCloudYamlBuildOptions = yaml.load(objBuildSettingsFile) - if ( - nextCloudYamlBuildOptions["databasePasswordOption"] == "Randomise database passwords for this build" - or nextCloudYamlBuildOptions["databasePasswordOption"] == "Randomise database passwords every build" - or nextCloudYamlBuildOptions["databasePasswordOption"] == "Use default passwords for this build" - ): - if nextCloudYamlBuildOptions["databasePasswordOption"] == "Use default passwords for this build": - mySqlRootPassword = "IOtSt4ckToorMySqlDb" - mySqlPassword = "IOtSt4ckmySqlDbPw" - else: - mySqlPassword = generateRandomString() - mySqlRootPassword = generateRandomString() - - for (index, serviceName) in enumerate(servicesListed): - dockerComposeServicesYaml[serviceName] = servicesListed[serviceName] - if "environment" in servicesListed[serviceName]: - for (envIndex, envName) in enumerate(servicesListed[serviceName]["environment"]): - envName = envName.replace("%randomMySqlPassword%", mySqlPassword) - dockerComposeServicesYaml[serviceName]["environment"][envIndex] = envName.replace("%randomPassword%", mySqlRootPassword) - - # Ensure you update the "Do nothing" and other 2 strings used for password settings in 'passwords.py' - if (nextCloudYamlBuildOptions["databasePasswordOption"] == "Randomise database passwords for this build"): - nextCloudYamlBuildOptions["databasePasswordOption"] = "Do nothing" - with open(buildSettings, 'w') as outputFile: - yaml.dump(nextCloudYamlBuildOptions, outputFile) - else: # Do nothing - don't change password - for (index, serviceName) in enumerate(buildCacheServices): - if serviceName in buildCacheServices: # Load service from cache if exists (to maintain password) - dockerComposeServicesYaml[serviceName] = buildCacheServices[serviceName] - else: - dockerComposeServicesYaml[serviceName] = servicesListed[serviceName] - - else: - print("NextCloud Warning: Build settings file not found, using default password") - time.sleep(1) - mySqlRootPassword = "IOtSt4ckToorMySqlDb" - mySqlPassword = "IOtSt4ckmySqlDbPw" - for (index, serviceName) in enumerate(servicesListed): - dockerComposeServicesYaml[serviceName] = servicesListed[serviceName] - if "environment" in servicesListed[serviceName]: - for (envIndex, envName) in enumerate(servicesListed[serviceName]["environment"]): - envName = envName.replace("%randomMySqlPassword%", mySqlPassword) - dockerComposeServicesYaml[serviceName]["environment"][envIndex] = envName.replace("%randomPassword%", mySqlRootPassword) - nextCloudYamlBuildOptions = { - "version": "1", - "application": "IOTstack", - "service": "NextCloud", - "comment": "NextCloud Build Options" - } - - nextCloudYamlBuildOptions["databasePasswordOption"] = "Do nothing" - with open(buildSettings, 'w') as outputFile: - yaml.dump(nextCloudYamlBuildOptions, outputFile) - - - return True - - # ##################################### - # Supporting functions below - # ##################################### - - def checkForIssues(): - for (index, serviceName) in enumerate(dockerComposeServicesYaml): - if not currentServiceName == serviceName: # Skip self - currentServicePorts = getExternalPorts(currentServiceName, dockerComposeServicesYaml) - portConflicts = checkPortConflicts(serviceName, currentServicePorts, dockerComposeServicesYaml) - if (len(portConflicts) > 0): - issues["portConflicts"] = portConflicts - - # ##################################### - # End Supporting functions - # ##################################### - - ############################ - # Menu Logic - ############################ - - global currentMenuItemIndex - global selectionInProgress - global menuNavigateDirection - global needsRender - - selectionInProgress = True - currentMenuItemIndex = 0 - menuNavigateDirection = 0 - needsRender = 1 - term = Terminal() - hotzoneLocation = [((term.height // 16) + 6), 0] - - def goBack(): - global selectionInProgress - global needsRender - selectionInProgress = False - needsRender = 1 - return True - - def enterPortNumberExec(): - # global term - global needsRender - global dockerComposeServicesYaml - externalPort = getExternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - internalPort = getInternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - newPortNumber = enterPortNumberWithWhiptail(term, dockerComposeServicesYaml, currentServiceName, hotzoneLocation, externalPort) - - if newPortNumber > 0: - dockerComposeServicesYaml[currentServiceName]["ports"][0] = "{newExtPort}:{oldIntPort}".format( - newExtPort = newPortNumber, - oldIntPort = internalPort - ) - createMenu() - needsRender = 1 - - def setPasswordOptions(): - global needsRender - global hasRebuiltAddons - passwordOptionsMenuFilePath = "./.templates/{currentService}/passwords.py".format(currentService=currentServiceName) - with open(passwordOptionsMenuFilePath, "rb") as pythonDynamicImportFile: - code = compile(pythonDynamicImportFile.read(), passwordOptionsMenuFilePath, "exec") - execGlobals = { - "currentServiceName": currentServiceName, - "renderMode": renderMode - } - execLocals = {} - screenActive = False - exec(code, execGlobals, execLocals) - signal.signal(signal.SIGWINCH, onResize) - screenActive = True - needsRender = 1 - - def onResize(sig, action): - global nextCloudBuildOptions - global currentMenuItemIndex - mainRender(1, nextCloudBuildOptions, currentMenuItemIndex) - - nextCloudBuildOptions = [] - - def createMenu(): - global nextCloudBuildOptions - try: - nextCloudBuildOptions = [] - portNumber = getExternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - nextCloudBuildOptions.append([ - "Change external WUI Port Number from: {port}".format(port=portNumber), - enterPortNumberExec - ]) - except: # Error getting port - pass - nextCloudBuildOptions.append([ - "Database Password Options", - setPasswordOptions - ]) - nextCloudBuildOptions.append(["Go back", goBack]) - - def runOptionsMenu(): - createMenu() - menuEntryPoint() - return True - - def renderHotZone(term, menu, selection, hotzoneLocation): - lineLengthAtTextStart = 71 - print(term.move(hotzoneLocation[0], hotzoneLocation[1])) - for (index, menuItem) in enumerate(menu): - toPrint = "" - if index == selection: - toPrint += ('{bv} -> {t.blue_on_green} {title} {t.normal} <-'.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) - else: - toPrint += ('{bv} {t.normal} {title} '.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) - - for i in range(lineLengthAtTextStart - len(menuItem[0])): - toPrint += " " - - toPrint += "{bv}".format(bv=specialChars[renderMode]["borderVertical"]) - - toPrint = term.center(toPrint) - - print(toPrint) - - def mainRender(needsRender, menu, selection): - term = Terminal() - - if needsRender == 1: - print(term.clear()) - print(term.move_y(term.height // 16)) - print(term.black_on_cornsilk4(term.center('IOTstack Next Cloud Options'))) - print("") - print(term.center(commonTopBorder(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Select Option to configure {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - - if needsRender >= 1: - renderHotZone(term, menu, selection, hotzoneLocation) - - if needsRender == 1: - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - if not hideHelpText: - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Enter] to run command or save input {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Escape] to go back to build stack menu {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - if len(documentationHint) > 1: - if len(documentationHint) > 56: - documentationAndPadding = padText(documentationHint, 71) - print(term.center("{bv} Documentation: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} {dap} {bv}".format(bv=specialChars[renderMode]["borderVertical"], dap=documentationAndPadding))) - else: - documentationAndPadding = padText(documentationHint, 56) - print(term.center("{bv} Documentation: {dap} {bv}".format(bv=specialChars[renderMode]["borderVertical"], dap=documentationAndPadding))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonBottomBorder(renderMode))) - - def runSelection(selection): - import types - global nextCloudBuildOptions - if len(nextCloudBuildOptions[selection]) > 1 and isinstance(nextCloudBuildOptions[selection][1], types.FunctionType): - nextCloudBuildOptions[selection][1]() - else: - print(term.green_reverse('IOTstack Error: No function assigned to menu item: "{}"'.format(nodeRedBuildOptions[selection][0]))) - - def isMenuItemSelectable(menu, index): - if len(menu) > index: - if len(menu[index]) > 2: - if menu[index][2]["skip"] == True: - return False - return True - - def menuEntryPoint(): - # These need to be reglobalised due to eval() - global currentMenuItemIndex - global selectionInProgress - global menuNavigateDirection - global needsRender - global hideHelpText - global nextCloudBuildOptions - term = Terminal() - with term.fullscreen(): - menuNavigateDirection = 0 - mainRender(needsRender, nextCloudBuildOptions, currentMenuItemIndex) - selectionInProgress = True - with term.cbreak(): - while selectionInProgress: - menuNavigateDirection = 0 - - if needsRender: # Only rerender when changed to prevent flickering - mainRender(needsRender, nextCloudBuildOptions, currentMenuItemIndex) - needsRender = 0 - - key = term.inkey(esc_delay=0.05) - if key.is_sequence: - if key.name == 'KEY_TAB': - menuNavigateDirection += 1 - if key.name == 'KEY_DOWN': - menuNavigateDirection += 1 - if key.name == 'KEY_UP': - menuNavigateDirection -= 1 - if key.name == 'KEY_LEFT': - goBack() - if key.name == 'KEY_ENTER': - runSelection(currentMenuItemIndex) - if key.name == 'KEY_ESCAPE': - return True - elif key: - if key == 'h': # H pressed - if hideHelpText: - hideHelpText = False - else: - hideHelpText = True - mainRender(1, nextCloudBuildOptions, currentMenuItemIndex) - - if menuNavigateDirection != 0: # If a direction was pressed, find next selectable item - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(nextCloudBuildOptions) - needsRender = 2 - - while not isMenuItemSelectable(nextCloudBuildOptions, currentMenuItemIndex): - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(nextCloudBuildOptions) - return True - - #################### - # End menu section - #################### - - - if haltOnErrors: - eval(toRun)() - else: - try: - eval(toRun)() - except: - pass - -# This check isn't required, but placed here for debugging purposes -global currentServiceName # Name of the current service -if currentServiceName == 'nextcloud': - main() -else: - print("Error. '{}' Tried to run 'nextcloud' config".format(currentServiceName)) diff --git a/.templates/nextcloud/passwords.py b/.templates/nextcloud/passwords.py deleted file mode 100755 index e54e09063..000000000 --- a/.templates/nextcloud/passwords.py +++ /dev/null @@ -1,328 +0,0 @@ -#!/usr/bin/env python3 - -import signal - -def main(): - from blessed import Terminal - from deps.chars import specialChars, commonTopBorder, commonBottomBorder, commonEmptyLine - from deps.consts import servicesDirectory, templatesDirectory, buildSettingsFileName - import time - import subprocess - import ruamel.yaml - import os - - global signal - global currentServiceName - global menuSelectionInProgress - global mainMenuList - global currentMenuItemIndex - global renderMode - global paginationSize - global paginationStartIndex - global hideHelpText - - yaml = ruamel.yaml.YAML() - yaml.preserve_quotes = True - - try: # If not already set, then set it. - hideHelpText = hideHelpText - except: - hideHelpText = False - - term = Terminal() - hotzoneLocation = [((term.height // 16) + 6), 0] - paginationToggle = [10, term.height - 25] - paginationStartIndex = 0 - paginationSize = paginationToggle[0] - - serviceService = servicesDirectory + currentServiceName - serviceTemplate = templatesDirectory + currentServiceName - buildSettings = serviceService + buildSettingsFileName - - def goBack(): - global menuSelectionInProgress - global needsRender - menuSelectionInProgress = False - needsRender = 1 - return True - - mainMenuList = [] - - hotzoneLocation = [((term.height // 16) + 6), 0] - - menuSelectionInProgress = True - currentMenuItemIndex = 0 - menuNavigateDirection = 0 - - # Render Modes: - # 0 = No render needed - # 1 = Full render - # 2 = Hotzone only - needsRender = 1 - - def onResize(sig, action): - global mainMenuList - global currentMenuItemIndex - mainRender(1, mainMenuList, currentMenuItemIndex) - - def generateLineText(text, textLength=None, paddingBefore=0, lineLength=64): - result = "" - for i in range(paddingBefore): - result += " " - - textPrintableCharactersLength = textLength - - if (textPrintableCharactersLength) == None: - textPrintableCharactersLength = len(text) - - result += text - remainingSpace = lineLength - textPrintableCharactersLength - - for i in range(remainingSpace): - result += " " - - return result - - def renderHotZone(term, renderType, menu, selection, hotzoneLocation, paddingBefore = 4): - global paginationSize - selectedTextLength = len("-> ") - - print(term.move(hotzoneLocation[0], hotzoneLocation[1])) - - if paginationStartIndex >= 1: - print(term.center("{b} {uaf} {uaf}{uaf}{uaf} {ual} {b}".format( - b=specialChars[renderMode]["borderVertical"], - uaf=specialChars[renderMode]["upArrowFull"], - ual=specialChars[renderMode]["upArrowLine"] - ))) - else: - print(term.center(commonEmptyLine(renderMode))) - - for (index, menuItem) in enumerate(menu): # Menu loop - if index >= paginationStartIndex and index < paginationStartIndex + paginationSize: - lineText = generateLineText(menuItem[0], paddingBefore=paddingBefore) - - # Menu highlight logic - if index == selection: - formattedLineText = '-> {t.blue_on_green}{title}{t.normal} <-'.format(t=term, title=menuItem[0]) - paddedLineText = generateLineText(formattedLineText, textLength=len(menuItem[0]) + selectedTextLength, paddingBefore=paddingBefore - selectedTextLength) - toPrint = paddedLineText - else: - toPrint = '{title}{t.normal}'.format(t=term, title=lineText) - # ##### - - # Menu check render logic - if menuItem[1]["checked"]: - toPrint = " (X) " + toPrint - else: - toPrint = " ( ) " + toPrint - - toPrint = "{bv} {toPrint} {bv}".format(bv=specialChars[renderMode]["borderVertical"], toPrint=toPrint) # Generate border - toPrint = term.center(toPrint) # Center Text (All lines should have the same amount of printable characters) - # ##### - print(toPrint) - - if paginationStartIndex + paginationSize < len(menu): - print(term.center("{b} {daf} {daf}{daf}{daf} {dal} {b}".format( - b=specialChars[renderMode]["borderVertical"], - daf=specialChars[renderMode]["downArrowFull"], - dal=specialChars[renderMode]["downArrowLine"] - ))) - else: - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - - - def mainRender(needsRender, menu, selection): - global paginationStartIndex - global paginationSize - term = Terminal() - - if selection >= paginationStartIndex + paginationSize: - paginationStartIndex = selection - (paginationSize - 1) + 1 - needsRender = 1 - - if selection <= paginationStartIndex - 1: - paginationStartIndex = selection - needsRender = 1 - - if needsRender == 1: - print(term.clear()) - print(term.move_y(term.height // 16)) - print(term.black_on_cornsilk4(term.center('IOTstack NextCloud Password Options'))) - print("") - print(term.center(commonTopBorder(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Select Password Option {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - - if needsRender >= 1: - renderHotZone(term, needsRender, menu, selection, hotzoneLocation) - - if needsRender == 1: - print(term.center(commonEmptyLine(renderMode))) - if not hideHelpText: - if term.height < 32: - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Not enough vertical room to render controls help text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - else: - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Space] to select option {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Enter] to build and save option {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Escape] to cancel changes {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonBottomBorder(renderMode))) - - def runSelection(selection): - import types - if len(mainMenuList[selection]) > 1 and isinstance(mainMenuList[selection][1], types.FunctionType): - mainMenuList[selection][1]() - else: - print(term.green_reverse('IOTstack Error: No function assigned to menu item: "{}"'.format(mainMenuList[selection][0]))) - - def isMenuItemSelectable(menu, index): - if len(menu) > index: - if len(menu[index]) > 1: - if "skip" in menu[index][1] and menu[index][1]["skip"] == True: - return False - return True - - def loadOptionsMenu(): - global mainMenuList - mainMenuList.append(["Use default passwords for this build", { "checked": True }]) - mainMenuList.append(["Randomise passwords for this build", { "checked": False }]) - mainMenuList.append(["Randomise passwords every build", { "checked": False }]) - mainMenuList.append(["Do nothing", { "checked": False }]) - - def checkMenuItem(selection): - global mainMenuList - for (index, menuItem) in enumerate(mainMenuList): - mainMenuList[index][1]["checked"] = False - - mainMenuList[selection][1]["checked"] = True - - def saveOptions(): - try: - if not os.path.exists(serviceService): - os.makedirs(serviceService, exist_ok=True) - - if os.path.exists(buildSettings): - with open(r'%s' % buildSettings) as objBuildSettingsFile: - nextCloudYamlBuildOptions = yaml.load(objBuildSettingsFile) - else: - nextCloudYamlBuildOptions = { - "version": "1", - "application": "IOTstack", - "service": "NextCloud", - "comment": "NextCloud Build Options" - } - - nextCloudYamlBuildOptions["databasePasswordOption"] = "" - - for (index, menuOption) in enumerate(mainMenuList): - if menuOption[1]["checked"]: - nextCloudYamlBuildOptions["databasePasswordOption"] = menuOption[0] - break - - with open(buildSettings, 'w') as outputFile: - yaml.dump(nextCloudYamlBuildOptions, outputFile) - - except Exception as err: - print("Error saving NextCloud Password options", currentServiceName) - print(err) - return False - global hasRebuiltHardwareSelection - hasRebuiltHardwareSelection = True - return True - - def loadOptions(): - try: - if not os.path.exists(serviceService): - os.makedirs(serviceService, exist_ok=True) - - if os.path.exists(buildSettings): - with open(r'%s' % buildSettings) as objBuildSettingsFile: - nextCloudYamlBuildOptions = yaml.load(objBuildSettingsFile) - - for (index, menuOption) in enumerate(mainMenuList): - if menuOption[0] == nextCloudYamlBuildOptions["databasePasswordOption"]: - checkMenuItem(index) - break - - except Exception as err: - print("Error loading NextCloud Password options", currentServiceName) - print(err) - return False - return True - - - if __name__ == 'builtins': - global signal - term = Terminal() - signal.signal(signal.SIGWINCH, onResize) - loadOptionsMenu() - loadOptions() - with term.fullscreen(): - menuNavigateDirection = 0 - mainRender(needsRender, mainMenuList, currentMenuItemIndex) - menuSelectionInProgress = True - with term.cbreak(): - while menuSelectionInProgress: - menuNavigateDirection = 0 - - if not needsRender == 0: # Only rerender when changed to prevent flickering - mainRender(needsRender, mainMenuList, currentMenuItemIndex) - needsRender = 0 - - key = term.inkey(esc_delay=0.05) - if key.is_sequence: - if key.name == 'KEY_TAB': - if paginationSize == paginationToggle[0]: - paginationSize = paginationToggle[1] - else: - paginationSize = paginationToggle[0] - mainRender(1, mainMenuList, currentMenuItemIndex) - if key.name == 'KEY_DOWN': - menuNavigateDirection += 1 - if key.name == 'KEY_UP': - menuNavigateDirection -= 1 - if key.name == 'KEY_ENTER': - if saveOptions(): - return True - else: - print("Something went wrong. Try saving the list again.") - if key.name == 'KEY_ESCAPE': - menuSelectionInProgress = False - return True - elif key: - if key == ' ': # Space pressed - checkMenuItem(currentMenuItemIndex) # Update checked list - needsRender = 2 - elif key == 'h': # H pressed - if hideHelpText: - hideHelpText = False - else: - hideHelpText = True - mainRender(1, mainMenuList, currentMenuItemIndex) - - if menuNavigateDirection != 0: # If a direction was pressed, find next selectable item - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(mainMenuList) - needsRender = 2 - - while not isMenuItemSelectable(mainMenuList, currentMenuItemIndex): - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(mainMenuList) - return True - - return True - -originalSignalHandler = signal.getsignal(signal.SIGINT) -main() -signal.signal(signal.SIGWINCH, originalSignalHandler) diff --git a/.templates/nodered/addons.yml b/.templates/nodered/addons.yml deleted file mode 100755 index bb0fc38d2..000000000 --- a/.templates/nodered/addons.yml +++ /dev/null @@ -1,55 +0,0 @@ -version: 1 -application: "IOTstack" -service: "nodered" -comment: "Addons available for NodeRed" -dockerFileInstallCommand: "RUN cd /usr/src/node-red && npm install --save " -addons: - default_on: - - "node-red-node-pi-gpiod" - - "node-red-contrib-influxdb" - - "node-red-contrib-boolean-logic" - - "node-red-node-rbe" - - "node-red-configurable-ping" - - "node-red-dashboard" - default_off: - - "node-red-node-openweathermap" - - "node-red-contrib-discord" - - "node-red-node-email" - - "node-red-node-google" - - "node-red-node-emoncms" - - "node-red-node-geofence" - - "node-red-node-ping" - - "node-red-node-random" - - "node-red-node-smooth" - - "node-red-node-darksky" - - "node-red-node-sqlite" - - "node-red-node-serialport" - - "node-red-contrib-config" - - "node-red-contrib-grove" - - "node-red-contrib-diode" - - "node-red-contrib-sunevents" - - "node-red-contrib-bigtimer" - - "node-red-contrib-esplogin" - - "node-red-contrib-timeout" - - "node-red-contrib-moment" - - "node-red-contrib-telegrambot" - - "node-red-contrib-particle" - - "node-red-contrib-web-worldmap" - - "node-red-contrib-ramp-thermostat" - - "node-red-contrib-isonline" - - "node-red-contrib-npm" - - "node-red-contrib-file-function" - - "node-red-contrib-home-assistant-websocket" - - "node-red-contrib-blynk-ws" - - "node-red-contrib-owntracks" - - "node-red-contrib-alexa-local" - - "node-red-contrib-heater-controller" - - "node-red-contrib-deconz" - - "node-red-contrib-generic-ble" - - "node-red-contrib-zigbee2mqtt" - - "node-red-contrib-vcgencmd" - - "node-red-contrib-themes/midnight-red" - - "node-red-contrib-tf-function" - - "node-red-contrib-tf-model" - - "node-red-contrib-post-object-detection" - - "node-red-contrib-bert-tokenizer" \ No newline at end of file diff --git a/.templates/nodered/build.py b/.templates/nodered/build.py deleted file mode 100755 index 705b64183..000000000 --- a/.templates/nodered/build.py +++ /dev/null @@ -1,398 +0,0 @@ -#!/usr/bin/env python3 - -issues = {} # Returned issues dict -buildHooks = {} # Options, and others hooks -haltOnErrors = True - -# Main wrapper function. Required to make local vars work correctly -def main(): - import os - import time - import ruamel.yaml - import signal - import sys - from blessed import Terminal - from deps.chars import specialChars, commonTopBorder, commonBottomBorder, commonEmptyLine, padText - from deps.consts import servicesDirectory, templatesDirectory - from deps.common_functions import getExternalPorts, getInternalPorts, checkPortConflicts, enterPortNumberWithWhiptail - - global dockerComposeServicesYaml # The loaded memory YAML of all checked services - global renderMode # For rendering fancy or basic ascii characters - global toRun # Switch for which function to run when executed - global buildHooks # Where to place the options menu result - global currentServiceName # Name of the current service - global issues # Returned issues dict - global haltOnErrors # Turn on to allow erroring - global serviceService - global serviceTemplate - global addonsFile - global hideHelpText - global hasRebuiltAddons - - try: # If not already set, then set it. - hideHelpText = hideHelpText - except: - hideHelpText = False - - documentationHint = 'https://sensorsiot.github.io/IOTstack/Containers/Node-RED' - - yaml = ruamel.yaml.YAML() - yaml.preserve_quotes = True - - # runtime vars - portConflicts = [] - hasRebuiltAddons = False - - serviceService = servicesDirectory + currentServiceName - serviceTemplate = templatesDirectory + currentServiceName - addonsFile = serviceService + "/addons_list.yml" - - dockerfileTemplateReplace = "%run npm install modules list%" - - # This lets the menu know whether to put " >> Options " or not - # This function is REQUIRED. - def checkForOptionsHook(): - global buildHooks - try: - buildHooks["options"] = callable(runOptionsMenu) - except: - buildHooks["options"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPreBuildHook(): - try: - buildHooks["preBuildHook"] = callable(preBuild) - except: - buildHooks["preBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPostBuildHook(): - try: - buildHooks["postBuildHook"] = callable(postBuild) - except: - buildHooks["postBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForRunChecksHook(): - try: - buildHooks["runChecksHook"] = callable(runChecks) - except: - buildHooks["runChecksHook"] = False - return buildHooks - return buildHooks - - # This service will not check anything unless this is set - # This function is optional, and will run each time the menu is rendered - def runChecks(): - checkForIssues() - return [] - - # This function is optional, and will run after the docker-compose.yml file is written to disk. - def postBuild(): - return True - - # This function is optional, and will run just before the build docker-compose.yml code. - def preBuild(): - import time - - # Setup service directory - if not os.path.exists(serviceService): - os.makedirs(serviceService, exist_ok=True) - - # Other prebuild steps - print("Starting NodeRed Build script") - time.sleep(0.1) - with open(r'%s/Dockerfile.template' % serviceTemplate, 'r') as dockerTemplate: - templateData = dockerTemplate.read() - - with open(r'%s' % addonsFile) as objAddonsFile: - addonsSelected = yaml.load(objAddonsFile) - - addonsInstallCommands = "" - if os.path.exists(addonsFile): - installCommand = addonsSelected["dockerFileInstallCommand"] - for (index, addonName) in enumerate(addonsSelected["addons"]): - if (addonName == 'node-red-node-sqlite'): # SQLite requires a special param - addonsInstallCommands = addonsInstallCommands + "{installCommand} --unsafe-perm {addonName}\n".format(addonName=addonName, installCommand=installCommand) - else: - addonsInstallCommands = addonsInstallCommands + "{installCommand} {addonName}\n".format(addonName=addonName, installCommand=installCommand) - - templateData = templateData.replace(dockerfileTemplateReplace, addonsInstallCommands) - - with open(r'%s/Dockerfile' % serviceService, 'w') as dockerTemplate: - dockerTemplate.write(templateData) - print("Finished NodeRed Build script") - time.sleep(0.3) - return True - - # ##################################### - # Supporting functions below - # ##################################### - - def checkForIssues(): - fileIssues = checkFiles() - if (len(fileIssues) > 0): - issues["fileIssues"] = fileIssues - for (index, serviceName) in enumerate(dockerComposeServicesYaml): - if not currentServiceName == serviceName: # Skip self - currentServicePorts = getExternalPorts(currentServiceName, dockerComposeServicesYaml) - portConflicts = checkPortConflicts(serviceName, currentServicePorts, dockerComposeServicesYaml) - if (len(portConflicts) > 0): - issues["portConflicts"] = portConflicts - - def checkFiles(): - fileIssues = [] - if not os.path.exists(serviceService + '/addons_list.yml'): - fileIssues.append('/addons_list.yml does not exist. Build addons file in options to fix. This is optional') - return fileIssues - - ############################ - # Menu Logic - ############################ - - global currentMenuItemIndex - global selectionInProgress - global menuNavigateDirection - global needsRender - - selectionInProgress = True - currentMenuItemIndex = 0 - menuNavigateDirection = 0 - needsRender = 1 - term = Terminal() - hotzoneLocation = [((term.height // 16) + 6), 0] - - def goBack(): - global selectionInProgress - global needsRender - selectionInProgress = False - needsRender = 1 - return True - - def enterPortNumberExec(): - # global term - global needsRender - global dockerComposeServicesYaml - externalPort = getExternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - internalPort = getInternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - newPortNumber = enterPortNumberWithWhiptail(term, dockerComposeServicesYaml, currentServiceName, hotzoneLocation, externalPort) - - if newPortNumber > 0: - dockerComposeServicesYaml[currentServiceName]["ports"][0] = "{newExtPort}:{oldIntPort}".format( - newExtPort = newPortNumber, - oldIntPort = internalPort - ) - createMenu() - needsRender = 1 - - def onResize(sig, action): - global nodeRedBuildOptions - global currentMenuItemIndex - mainRender(1, nodeRedBuildOptions, currentMenuItemIndex) - - def selectNodeRedAddons(): - global needsRender - global hasRebuiltAddons - dockerCommandsFilePath = "./.templates/nodered/addons.py" - with open(dockerCommandsFilePath, "rb") as pythonDynamicImportFile: - code = compile(pythonDynamicImportFile.read(), dockerCommandsFilePath, "exec") - # execGlobals = globals() - # execLocals = locals() - execGlobals = { - "currentServiceName": currentServiceName, - "renderMode": renderMode - } - execLocals = {} - screenActive = False - exec(code, execGlobals, execLocals) - signal.signal(signal.SIGWINCH, onResize) - try: - hasRebuiltAddons = execGlobals["hasRebuiltAddons"] - except: - hasRebuiltAddons = False - screenActive = True - needsRender = 1 - - nodeRedBuildOptions = [] - - def createMenu(): - global nodeRedBuildOptions - try: - nodeRedBuildOptions = [] - portNumber = getExternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - nodeRedBuildOptions.append([ - "Change external WUI Port Number from: {port}".format(port=portNumber), - enterPortNumberExec - ]) - except: # Error getting port - pass - nodeRedBuildOptions.append(["Go back", goBack]) - - if os.path.exists(serviceService + '/addons_list.yml'): - nodeRedBuildOptions.insert(0, ["Select & overwrite addons list", selectNodeRedAddons]) - else: - nodeRedBuildOptions.insert(0, ["Select & build addons list", selectNodeRedAddons]) - - def runOptionsMenu(): - createMenu() - menuEntryPoint() - return True - - def renderHotZone(term, menu, selection, hotzoneLocation): - lineLengthAtTextStart = 71 - print(term.move(hotzoneLocation[0], hotzoneLocation[1])) - for (index, menuItem) in enumerate(menu): - toPrint = "" - if index == selection: - toPrint += ('{bv} -> {t.blue_on_green} {title} {t.normal} <-'.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) - else: - toPrint += ('{bv} {t.normal} {title} '.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) - - for i in range(lineLengthAtTextStart - len(menuItem[0])): - toPrint += " " - - toPrint += "{bv}".format(bv=specialChars[renderMode]["borderVertical"]) - - toPrint = term.center(toPrint) - - print(toPrint) - - def mainRender(needsRender, menu, selection): - term = Terminal() - - if needsRender == 1: - print(term.clear()) - print(term.move_y(term.height // 16)) - print(term.black_on_cornsilk4(term.center('IOTstack NodeRed Options'))) - print("") - print(term.center(commonTopBorder(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Select Option to configure {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - - if needsRender >= 1: - renderHotZone(term, menu, selection, hotzoneLocation) - - if needsRender == 1: - if os.path.exists(serviceService + '/addons_list.yml'): - if hasRebuiltAddons: - print(term.center(commonEmptyLine(renderMode))) - print(term.center('{bv} {t.grey_on_blue4} {text} {t.normal}{t.white_on_black}{t.normal} {bv}'.format(t=term, text="Addons list has been rebuilt: addons_list.yml", bv=specialChars[renderMode]["borderVertical"]))) - else: - print(term.center(commonEmptyLine(renderMode))) - print(term.center('{bv} {t.grey_on_blue4} {text} {t.normal}{t.white_on_black}{t.normal} {bv}'.format(t=term, text="Using existing addons_list.yml for addons installation", bv=specialChars[renderMode]["borderVertical"]))) - else: - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - if not hideHelpText: - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Enter] to run command or save input {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Escape] to go back to build stack menu {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - if len(documentationHint) > 1: - if len(documentationHint) > 56: - documentationAndPadding = padText(documentationHint, 71) - print(term.center("{bv} Documentation: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} {dap} {bv}".format(bv=specialChars[renderMode]["borderVertical"], dap=documentationAndPadding))) - else: - documentationAndPadding = padText(documentationHint, 56) - print(term.center("{bv} Documentation: {dap} {bv}".format(bv=specialChars[renderMode]["borderVertical"], dap=documentationAndPadding))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonBottomBorder(renderMode))) - - def runSelection(selection): - import types - global nodeRedBuildOptions - if len(nodeRedBuildOptions[selection]) > 1 and isinstance(nodeRedBuildOptions[selection][1], types.FunctionType): - nodeRedBuildOptions[selection][1]() - else: - print(term.green_reverse('IOTstack Error: No function assigned to menu item: "{}"'.format(nodeRedBuildOptions[selection][0]))) - - def isMenuItemSelectable(menu, index): - if len(menu) > index: - if len(menu[index]) > 2: - if menu[index][2]["skip"] == True: - return False - return True - - def menuEntryPoint(): - # These need to be reglobalised due to eval() - global currentMenuItemIndex - global selectionInProgress - global menuNavigateDirection - global needsRender - global hideHelpText - global nodeRedBuildOptions - term = Terminal() - with term.fullscreen(): - menuNavigateDirection = 0 - mainRender(needsRender, nodeRedBuildOptions, currentMenuItemIndex) - selectionInProgress = True - with term.cbreak(): - while selectionInProgress: - menuNavigateDirection = 0 - - if needsRender: # Only rerender when changed to prevent flickering - mainRender(needsRender, nodeRedBuildOptions, currentMenuItemIndex) - needsRender = 0 - - key = term.inkey(esc_delay=0.05) - if key.is_sequence: - if key.name == 'KEY_TAB': - menuNavigateDirection += 1 - if key.name == 'KEY_DOWN': - menuNavigateDirection += 1 - if key.name == 'KEY_UP': - menuNavigateDirection -= 1 - if key.name == 'KEY_LEFT': - goBack() - if key.name == 'KEY_ENTER': - runSelection(currentMenuItemIndex) - if key.name == 'KEY_ESCAPE': - return True - elif key: - if key == 'h': # H pressed - if hideHelpText: - hideHelpText = False - else: - hideHelpText = True - mainRender(1, nodeRedBuildOptions, currentMenuItemIndex) - - if menuNavigateDirection != 0: # If a direction was pressed, find next selectable item - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(nodeRedBuildOptions) - needsRender = 2 - - while not isMenuItemSelectable(nodeRedBuildOptions, currentMenuItemIndex): - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(nodeRedBuildOptions) - return True - - #################### - # End menu section - #################### - - if haltOnErrors: - eval(toRun)() - else: - try: - eval(toRun)() - except: - pass - -# This check isn't required, but placed here for debugging purposes -global currentServiceName # Name of the current service -if currentServiceName == 'nodered': - main() -else: - print("Error. '{}' Tried to run 'nodered' config".format(currentServiceName)) diff --git a/.templates/nodered/terminal.sh b/.templates/nodered/terminal.sh deleted file mode 100755 index 59c1297e0..000000000 --- a/.templates/nodered/terminal.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -echo 'to generate a new password hash copy the following line and change PASSWORD' -echo $'node -e "console.log(require(\'bcryptjs\').hashSync(process.argv[1], 8));" PASSWORD' -echo 'then "exit"' - -docker exec -it nodered bash diff --git a/.templates/openhab/build.py b/.templates/openhab/build.py deleted file mode 100755 index fd9fd3932..000000000 --- a/.templates/openhab/build.py +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env python3 - -issues = {} # Returned issues dict -buildHooks = {} # Options, and others hooks -haltOnErrors = True - -# Main wrapper function. Required to make local vars work correctly -def main(): - - global dockerComposeServicesYaml # The loaded memory YAML of all checked services - global toRun # Switch for which function to run when executed - global buildHooks # Where to place the options menu result - global currentServiceName # Name of the current service - global issues # Returned issues dict - global haltOnErrors # Turn on to allow erroring - - # This lets the menu know whether to put " >> Options " or not - # This function is REQUIRED. - def checkForOptionsHook(): - try: - buildHooks["options"] = callable(runOptionsMenu) - except: - buildHooks["options"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPreBuildHook(): - try: - buildHooks["preBuildHook"] = callable(preBuild) - except: - buildHooks["preBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPostBuildHook(): - try: - buildHooks["postBuildHook"] = callable(postBuild) - except: - buildHooks["postBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForRunChecksHook(): - try: - buildHooks["runChecksHook"] = callable(runChecks) - except: - buildHooks["runChecksHook"] = False - return buildHooks - return buildHooks - - # This service will not check anything unless this is set - # This function is optional, and will run each time the menu is rendered - def runChecks(): - return [] - - # This function is optional, and will run after the docker-compose.yml file is written to disk. - def postBuild(): - return True - - # This function is optional, and will run just before the build docker-compose.yml code. - def preBuild(): - return True - - # ##################################### - # Supporting functions below - # ##################################### - - # None - - # ##################################### - # End Supporting functions - # ##################################### - - if haltOnErrors: - eval(toRun)() - else: - try: - eval(toRun)() - except: - pass - -# This check isn't required, but placed here for debugging purposes -global currentServiceName # Name of the current service -if currentServiceName == 'openhab': - main() -else: - print("Error. '{}' Tried to run 'openhab' config".format(currentServiceName)) diff --git a/.templates/pihole/build.py b/.templates/pihole/build.py deleted file mode 100755 index e2cc017b4..000000000 --- a/.templates/pihole/build.py +++ /dev/null @@ -1,411 +0,0 @@ -#!/usr/bin/env python3 - -issues = {} # Returned issues dict -buildHooks = {} # Options, and others hooks -haltOnErrors = True - -# Main wrapper function. Required to make local vars work correctly -def main(): - import os - import time - import ruamel.yaml - import signal - import sys - import subprocess - - from blessed import Terminal - from deps.chars import specialChars, commonTopBorder, commonBottomBorder, commonEmptyLine, padText - from deps.consts import servicesDirectory, templatesDirectory, volumesDirectory, servicesFileName, buildSettingsFileName - from deps.common_functions import getExternalPorts, getInternalPorts, checkPortConflicts, enterPortNumberWithWhiptail, generateRandomString - - global dockerComposeServicesYaml # The loaded memory YAML of all checked services - global toRun # Switch for which function to run when executed - global buildHooks # Where to place the options menu result - global currentServiceName # Name of the current service - global issues # Returned issues dict - global haltOnErrors # Turn on to allow erroring - - # runtime vars - portConflicts = [] - serviceVolume = volumesDirectory + currentServiceName - serviceService = servicesDirectory + currentServiceName - serviceTemplate = templatesDirectory + currentServiceName - buildSettings = serviceService + buildSettingsFileName - - yaml = ruamel.yaml.YAML() - yaml.preserve_quotes = True - - try: # If not already set, then set it. - hideHelpText = hideHelpText - except: - hideHelpText = False - - documentationHint = 'https://sensorsiot.github.io/IOTstack/Containers/Pi-hole' - - # This lets the menu know whether to put " >> Options " or not - # This function is REQUIRED. - def checkForOptionsHook(): - try: - buildHooks["options"] = callable(runOptionsMenu) - except: - buildHooks["options"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPreBuildHook(): - try: - buildHooks["preBuildHook"] = callable(preBuild) - except: - buildHooks["preBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPostBuildHook(): - try: - buildHooks["postBuildHook"] = callable(postBuild) - except: - buildHooks["postBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForRunChecksHook(): - try: - buildHooks["runChecksHook"] = callable(runChecks) - except: - buildHooks["runChecksHook"] = False - return buildHooks - return buildHooks - - # This service will not check anything unless this is set - # This function is optional, and will run each time the menu is rendered - def runChecks(): - checkForIssues() - return [] - - # This function is optional, and will run after the docker-compose.yml file is written to disk. - def postBuild(): - return True - - # This function is optional, and will run just before the build docker-compose.yml code. - def preBuild(): - # Multi-service: - with open((r'%s/' % serviceTemplate) + servicesFileName) as objServiceFile: - serviceYamlTemplate = yaml.load(objServiceFile) - - oldBuildCache = {} - try: - with open(r'%s' % buildCache) as objBuildCache: - oldBuildCache = yaml.load(objBuildCache) - except: - pass - - buildCacheServices = {} - if "services" in oldBuildCache: - buildCacheServices = oldBuildCache["services"] - - if not os.path.exists(serviceService): - os.makedirs(serviceService, exist_ok=True) - - if os.path.exists(buildSettings): - # Password randomisation - with open(r'%s' % buildSettings) as objBuildSettingsFile: - piHoleYamlBuildOptions = yaml.load(objBuildSettingsFile) - if ( - piHoleYamlBuildOptions["databasePasswordOption"] == "Randomise password for this build" - or piHoleYamlBuildOptions["databasePasswordOption"] == "Randomise database password every build" - or piHoleYamlBuildOptions["databasePasswordOption"] == "Use default password for this build" - ): - if piHoleYamlBuildOptions["databasePasswordOption"] == "Use default password for this build": - newAdminPassword = "IOtSt4ckP1Hol3" - else: - newAdminPassword = generateRandomString() - - for (index, serviceName) in enumerate(serviceYamlTemplate): - dockerComposeServicesYaml[serviceName] = serviceYamlTemplate[serviceName] - if "environment" in serviceYamlTemplate[serviceName]: - for (envIndex, envName) in enumerate(serviceYamlTemplate[serviceName]["environment"]): - envName = envName.replace("%randomAdminPassword%", newAdminPassword) - dockerComposeServicesYaml[serviceName]["environment"][envIndex] = envName - - # Ensure you update the "Do nothing" and other 2 strings used for password settings in 'passwords.py' - if (piHoleYamlBuildOptions["databasePasswordOption"] == "Randomise password for this build"): - piHoleYamlBuildOptions["databasePasswordOption"] = "Do nothing" - with open(buildSettings, 'w') as outputFile: - yaml.dump(piHoleYamlBuildOptions, outputFile) - else: # Do nothing - don't change password - for (index, serviceName) in enumerate(buildCacheServices): - if serviceName in buildCacheServices: # Load service from cache if exists (to maintain password) - dockerComposeServicesYaml[serviceName] = buildCacheServices[serviceName] - else: - dockerComposeServicesYaml[serviceName] = serviceYamlTemplate[serviceName] - - else: - print("PiHole Warning: Build settings file not found, using default password") - time.sleep(1) - newAdminPassword = "IOtSt4ckP1Hol3" - for (index, serviceName) in enumerate(serviceYamlTemplate): - dockerComposeServicesYaml[serviceName] = serviceYamlTemplate[serviceName] - if "environment" in serviceYamlTemplate[serviceName]: - for (envIndex, envName) in enumerate(serviceYamlTemplate[serviceName]["environment"]): - envName = envName.replace("%randomAdminPassword%", newAdminPassword) - dockerComposeServicesYaml[serviceName]["environment"][envIndex] = envName - piHoleYamlBuildOptions = { - "version": "1", - "application": "IOTstack", - "service": "PiHole", - "comment": "PiHole Build Options" - } - - piHoleYamlBuildOptions["databasePasswordOption"] = "Do nothing" - with open(buildSettings, 'w') as outputFile: - yaml.dump(piHoleYamlBuildOptions, outputFile) - - return True - - - # ##################################### - # Supporting functions below - # ##################################### - - def checkForIssues(): - for (index, serviceName) in enumerate(dockerComposeServicesYaml): - if not currentServiceName == serviceName: # Skip self - currentServicePorts = getExternalPorts(currentServiceName, dockerComposeServicesYaml) - portConflicts = checkPortConflicts(serviceName, currentServicePorts, dockerComposeServicesYaml) - if (len(portConflicts) > 0): - issues["portConflicts"] = portConflicts - - ############################ - # Menu Logic - ############################ - - global currentMenuItemIndex - global selectionInProgress - global menuNavigateDirection - global needsRender - - selectionInProgress = True - currentMenuItemIndex = 0 - menuNavigateDirection = 0 - needsRender = 1 - term = Terminal() - hotzoneLocation = [((term.height // 16) + 6), 0] - - def goBack(): - global selectionInProgress - global needsRender - selectionInProgress = False - needsRender = 1 - return True - - def setPasswordOptions(): - global needsRender - global hasRebuiltAddons - passwordOptionsMenuFilePath = "./.templates/{currentService}/passwords.py".format(currentService=currentServiceName) - with open(passwordOptionsMenuFilePath, "rb") as pythonDynamicImportFile: - code = compile(pythonDynamicImportFile.read(), passwordOptionsMenuFilePath, "exec") - execGlobals = { - "currentServiceName": currentServiceName, - "renderMode": renderMode - } - execLocals = {} - screenActive = False - exec(code, execGlobals, execLocals) - signal.signal(signal.SIGWINCH, onResize) - screenActive = True - needsRender = 1 - - def enterPortNumberExec(): - # global term - global needsRender - global dockerComposeServicesYaml - externalPort = getExternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - internalPort = getInternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - newPortNumber = enterPortNumberWithWhiptail(term, dockerComposeServicesYaml, currentServiceName, hotzoneLocation, externalPort) - - if newPortNumber > 0: - dockerComposeServicesYaml[currentServiceName]["ports"][0] = "{newExtPort}:{oldIntPort}".format( - newExtPort = newPortNumber, - oldIntPort = internalPort - ) - createMenu() - needsRender = 1 - - def onResize(sig, action): - global piHoleBuildOptions - global currentMenuItemIndex - mainRender(1, piHoleBuildOptions, currentMenuItemIndex) - - piHoleBuildOptions = [] - - def createMenu(): - global piHoleBuildOptions - try: - piHoleBuildOptions = [] - portNumber = getExternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - piHoleBuildOptions.append([ - "Change external WUI Port Number from: {port}".format(port=portNumber), - enterPortNumberExec - ]) - except: # Error getting port - pass - - piHoleBuildOptions.append([ - "PiHole Password Options", - setPasswordOptions - ]) - - piHoleBuildOptions.append(["Go back", goBack]) - - def runOptionsMenu(): - createMenu() - menuEntryPoint() - return True - - def renderHotZone(term, menu, selection, hotzoneLocation): - lineLengthAtTextStart = 71 - print(term.move(hotzoneLocation[0], hotzoneLocation[1])) - for (index, menuItem) in enumerate(menu): - toPrint = "" - if index == selection: - toPrint += ('{bv} -> {t.blue_on_green} {title} {t.normal} <-'.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) - else: - toPrint += ('{bv} {t.normal} {title} '.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) - - for i in range(lineLengthAtTextStart - len(menuItem[0])): - toPrint += " " - - toPrint += "{bv}".format(bv=specialChars[renderMode]["borderVertical"]) - - toPrint = term.center(toPrint) - - print(toPrint) - - def mainRender(needsRender, menu, selection): - term = Terminal() - - if needsRender == 1: - print(term.clear()) - print(term.move_y(term.height // 16)) - print(term.black_on_cornsilk4(term.center('IOTstack PiHole Options'))) - print("") - print(term.center(commonTopBorder(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Select Option to configure {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - - if needsRender >= 1: - renderHotZone(term, menu, selection, hotzoneLocation) - - if needsRender == 1: - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - if not hideHelpText: - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Enter] to run command or save input {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Escape] to go back to build stack menu {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - if len(documentationHint) > 1: - if len(documentationHint) > 56: - documentationAndPadding = padText(documentationHint, 71) - print(term.center("{bv} Documentation: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} {dap} {bv}".format(bv=specialChars[renderMode]["borderVertical"], dap=documentationAndPadding))) - else: - documentationAndPadding = padText(documentationHint, 56) - print(term.center("{bv} Documentation: {dap} {bv}".format(bv=specialChars[renderMode]["borderVertical"], dap=documentationAndPadding))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonBottomBorder(renderMode))) - - def runSelection(selection): - import types - global piHoleBuildOptions - if len(piHoleBuildOptions[selection]) > 1 and isinstance(piHoleBuildOptions[selection][1], types.FunctionType): - piHoleBuildOptions[selection][1]() - else: - print(term.green_reverse('IOTstack Error: No function assigned to menu item: "{}"'.format(nodeRedBuildOptions[selection][0]))) - - def isMenuItemSelectable(menu, index): - if len(menu) > index: - if len(menu[index]) > 2: - if menu[index][2]["skip"] == True: - return False - return True - - def menuEntryPoint(): - # These need to be reglobalised due to eval() - global currentMenuItemIndex - global selectionInProgress - global menuNavigateDirection - global needsRender - global hideHelpText - global piHoleBuildOptions - term = Terminal() - with term.fullscreen(): - menuNavigateDirection = 0 - mainRender(needsRender, piHoleBuildOptions, currentMenuItemIndex) - selectionInProgress = True - with term.cbreak(): - while selectionInProgress: - menuNavigateDirection = 0 - - if needsRender: # Only rerender when changed to prevent flickering - mainRender(needsRender, piHoleBuildOptions, currentMenuItemIndex) - needsRender = 0 - - key = term.inkey(esc_delay=0.05) - if key.is_sequence: - if key.name == 'KEY_TAB': - menuNavigateDirection += 1 - if key.name == 'KEY_DOWN': - menuNavigateDirection += 1 - if key.name == 'KEY_UP': - menuNavigateDirection -= 1 - if key.name == 'KEY_LEFT': - goBack() - if key.name == 'KEY_ENTER': - runSelection(currentMenuItemIndex) - if key.name == 'KEY_ESCAPE': - return True - elif key: - if key == 'h': # H pressed - if hideHelpText: - hideHelpText = False - else: - hideHelpText = True - mainRender(1, piHoleBuildOptions, currentMenuItemIndex) - - if menuNavigateDirection != 0: # If a direction was pressed, find next selectable item - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(piHoleBuildOptions) - needsRender = 2 - - while not isMenuItemSelectable(piHoleBuildOptions, currentMenuItemIndex): - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(piHoleBuildOptions) - return True - - #################### - # End menu section - #################### - - if haltOnErrors: - eval(toRun)() - else: - try: - eval(toRun)() - except: - pass - -# This check isn't required, but placed here for debugging purposes -global currentServiceName # Name of the current service -if currentServiceName == 'pihole': - main() -else: - print("Error. '{}' Tried to run 'pihole' config".format(currentServiceName)) diff --git a/.templates/pihole/passwords.py b/.templates/pihole/passwords.py deleted file mode 100755 index 6a24483c3..000000000 --- a/.templates/pihole/passwords.py +++ /dev/null @@ -1,326 +0,0 @@ -#!/usr/bin/env python3 - -import signal - -def main(): - from blessed import Terminal - from deps.chars import specialChars, commonTopBorder, commonBottomBorder, commonEmptyLine - from deps.consts import servicesDirectory, templatesDirectory, buildSettingsFileName - import time - import subprocess - import ruamel.yaml - import os - - global signal - global currentServiceName - global menuSelectionInProgress - global mainMenuList - global currentMenuItemIndex - global renderMode - global paginationSize - global paginationStartIndex - global hideHelpText - - yaml = ruamel.yaml.YAML() - yaml.preserve_quotes = True - - try: # If not already set, then set it. - hideHelpText = hideHelpText - except: - hideHelpText = False - - term = Terminal() - hotzoneLocation = [((term.height // 16) + 6), 0] - paginationToggle = [10, term.height - 25] - paginationStartIndex = 0 - paginationSize = paginationToggle[0] - - serviceService = servicesDirectory + currentServiceName - serviceTemplate = templatesDirectory + currentServiceName - buildSettings = serviceService + buildSettingsFileName - - def goBack(): - global menuSelectionInProgress - global needsRender - menuSelectionInProgress = False - needsRender = 1 - return True - - mainMenuList = [] - - hotzoneLocation = [((term.height // 16) + 6), 0] - - menuSelectionInProgress = True - currentMenuItemIndex = 0 - menuNavigateDirection = 0 - - # Render Modes: - # 0 = No render needed - # 1 = Full render - # 2 = Hotzone only - needsRender = 1 - - def onResize(sig, action): - global mainMenuList - global currentMenuItemIndex - mainRender(1, mainMenuList, currentMenuItemIndex) - - def generateLineText(text, textLength=None, paddingBefore=0, lineLength=64): - result = "" - for i in range(paddingBefore): - result += " " - - textPrintableCharactersLength = textLength - - if (textPrintableCharactersLength) == None: - textPrintableCharactersLength = len(text) - - result += text - remainingSpace = lineLength - textPrintableCharactersLength - - for i in range(remainingSpace): - result += " " - - return result - - def renderHotZone(term, renderType, menu, selection, hotzoneLocation, paddingBefore = 4): - global paginationSize - selectedTextLength = len("-> ") - - print(term.move(hotzoneLocation[0], hotzoneLocation[1])) - - if paginationStartIndex >= 1: - print(term.center("{b} {uaf} {uaf}{uaf}{uaf} {ual} {b}".format( - b=specialChars[renderMode]["borderVertical"], - uaf=specialChars[renderMode]["upArrowFull"], - ual=specialChars[renderMode]["upArrowLine"] - ))) - else: - print(term.center(commonEmptyLine(renderMode))) - - for (index, menuItem) in enumerate(menu): # Menu loop - if index >= paginationStartIndex and index < paginationStartIndex + paginationSize: - lineText = generateLineText(menuItem[0], paddingBefore=paddingBefore) - - # Menu highlight logic - if index == selection: - formattedLineText = '-> {t.blue_on_green}{title}{t.normal} <-'.format(t=term, title=menuItem[0]) - paddedLineText = generateLineText(formattedLineText, textLength=len(menuItem[0]) + selectedTextLength, paddingBefore=paddingBefore - selectedTextLength) - toPrint = paddedLineText - else: - toPrint = '{title}{t.normal}'.format(t=term, title=lineText) - # ##### - - # Menu check render logic - if menuItem[1]["checked"]: - toPrint = " (X) " + toPrint - else: - toPrint = " ( ) " + toPrint - - toPrint = "{bv} {toPrint} {bv}".format(bv=specialChars[renderMode]["borderVertical"], toPrint=toPrint) # Generate border - toPrint = term.center(toPrint) # Center Text (All lines should have the same amount of printable characters) - # ##### - print(toPrint) - - if paginationStartIndex + paginationSize < len(menu): - print(term.center("{b} {daf} {daf}{daf}{daf} {dal} {b}".format( - b=specialChars[renderMode]["borderVertical"], - daf=specialChars[renderMode]["downArrowFull"], - dal=specialChars[renderMode]["downArrowLine"] - ))) - else: - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - - - def mainRender(needsRender, menu, selection): - global paginationStartIndex - global paginationSize - term = Terminal() - - if selection >= paginationStartIndex + paginationSize: - paginationStartIndex = selection - (paginationSize - 1) + 1 - needsRender = 1 - - if selection <= paginationStartIndex - 1: - paginationStartIndex = selection - needsRender = 1 - - if needsRender == 1: - print(term.clear()) - print(term.move_y(term.height // 16)) - print(term.black_on_cornsilk4(term.center('IOTstack PiHole Password Options'))) - print("") - print(term.center(commonTopBorder(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Select Password Option {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - - if needsRender >= 1: - renderHotZone(term, needsRender, menu, selection, hotzoneLocation) - - if needsRender == 1: - print(term.center(commonEmptyLine(renderMode))) - if not hideHelpText: - if term.height < 32: - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Not enough vertical room to render controls help text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - else: - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Space] to select option {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Enter] to build and save option {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Escape] to cancel changes {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonBottomBorder(renderMode))) - - def runSelection(selection): - import types - if len(mainMenuList[selection]) > 1 and isinstance(mainMenuList[selection][1], types.FunctionType): - mainMenuList[selection][1]() - else: - print(term.green_reverse('IOTstack Error: No function assigned to menu item: "{}"'.format(mainMenuList[selection][0]))) - - def isMenuItemSelectable(menu, index): - if len(menu) > index: - if len(menu[index]) > 1: - if "skip" in menu[index][1] and menu[index][1]["skip"] == True: - return False - return True - - def loadOptionsMenu(): - global mainMenuList - mainMenuList.append(["Use default password for this build", { "checked": True }]) - mainMenuList.append(["Randomise password for this build", { "checked": False }]) - # mainMenuList.append(["Randomise database password every build", { "checked": False }]) - mainMenuList.append(["Do nothing", { "checked": False }]) - - def checkMenuItem(selection): - global mainMenuList - for (index, menuItem) in enumerate(mainMenuList): - mainMenuList[index][1]["checked"] = False - - mainMenuList[selection][1]["checked"] = True - - def saveOptions(): - try: - if not os.path.exists(serviceService): - os.makedirs(serviceService, exist_ok=True) - - if os.path.exists(buildSettings): - with open(r'%s' % buildSettings) as objBuildSettingsFile: - piHoleYamlBuildOptions = yaml.load(objBuildSettingsFile) - else: - piHoleYamlBuildOptions = { - "version": "1", - "application": "IOTstack", - "service": "PiHole", - "comment": "Build Settings", - } - - piHoleYamlBuildOptions["databasePasswordOption"] = "" - - for (index, menuOption) in enumerate(mainMenuList): - if menuOption[1]["checked"]: - piHoleYamlBuildOptions["databasePasswordOption"] = menuOption[0] - break - - with open(buildSettings, 'w') as outputFile: - yaml.dump(piHoleYamlBuildOptions, outputFile) - - except Exception as err: - print("Error saving PiHole Password options", currentServiceName) - print(err) - return False - return True - - def loadOptions(): - try: - if not os.path.exists(serviceService): - os.makedirs(serviceService, exist_ok=True) - - if os.path.exists(buildSettings): - with open(r'%s' % buildSettings) as objBuildSettingsFile: - piHoleYamlBuildOptions = yaml.load(objBuildSettingsFile) - - for (index, menuOption) in enumerate(mainMenuList): - if menuOption[0] == piHoleYamlBuildOptions["databasePasswordOption"]: - checkMenuItem(index) - break - - except Exception as err: - print("Error loading PiHole Password options", currentServiceName) - print(err) - return False - return True - - - if __name__ == 'builtins': - global signal - term = Terminal() - signal.signal(signal.SIGWINCH, onResize) - loadOptionsMenu() - loadOptions() - with term.fullscreen(): - menuNavigateDirection = 0 - mainRender(needsRender, mainMenuList, currentMenuItemIndex) - menuSelectionInProgress = True - with term.cbreak(): - while menuSelectionInProgress: - menuNavigateDirection = 0 - - if not needsRender == 0: # Only rerender when changed to prevent flickering - mainRender(needsRender, mainMenuList, currentMenuItemIndex) - needsRender = 0 - - key = term.inkey(esc_delay=0.05) - if key.is_sequence: - if key.name == 'KEY_TAB': - if paginationSize == paginationToggle[0]: - paginationSize = paginationToggle[1] - else: - paginationSize = paginationToggle[0] - mainRender(1, mainMenuList, currentMenuItemIndex) - if key.name == 'KEY_DOWN': - menuNavigateDirection += 1 - if key.name == 'KEY_UP': - menuNavigateDirection -= 1 - if key.name == 'KEY_ENTER': - if saveOptions(): - return True - else: - print("Something went wrong. Try saving the list again.") - if key.name == 'KEY_ESCAPE': - menuSelectionInProgress = False - return True - elif key: - if key == ' ': # Space pressed - checkMenuItem(currentMenuItemIndex) # Update checked list - needsRender = 2 - elif key == 'h': # H pressed - if hideHelpText: - hideHelpText = False - else: - hideHelpText = True - mainRender(1, mainMenuList, currentMenuItemIndex) - - if menuNavigateDirection != 0: # If a direction was pressed, find next selectable item - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(mainMenuList) - needsRender = 2 - - while not isMenuItemSelectable(mainMenuList, currentMenuItemIndex): - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(mainMenuList) - return True - - return True - -originalSignalHandler = signal.getsignal(signal.SIGINT) -main() -signal.signal(signal.SIGWINCH, originalSignalHandler) diff --git a/.templates/pihole/service.yml b/.templates/pihole/service.yml deleted file mode 100644 index 26258e41f..000000000 --- a/.templates/pihole/service.yml +++ /dev/null @@ -1,26 +0,0 @@ -pihole: - container_name: pihole - image: pihole/pihole:latest - ports: - - "8089:80/tcp" - - "53:53/tcp" - - "53:53/udp" - - "67:67/udp" - environment: - - WEBPASSWORD=%randomAdminPassword% - - INTERFACE=eth0 - volumes: - - ./volumes/pihole/etc-pihole/:/etc/pihole/ - - ./volumes/pihole/etc-dnsmasq.d/:/etc/dnsmasq.d/ - dns: - - 127.0.0.1 - - 1.1.1.1 - cap_add: - - NET_ADMIN - restart: unless-stopped - networks: - - iotstack_nw - - vpn_nw - -# Recommended but not required (DHCP needs NET_ADMIN) -# https://github.com/pi-hole/docker-pi-hole#note-on-capabilities \ No newline at end of file diff --git a/.templates/plex/build.py b/.templates/plex/build.py deleted file mode 100755 index 1ea53f3a8..000000000 --- a/.templates/plex/build.py +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env python3 - -issues = {} # Returned issues dict -buildHooks = {} # Options, and others hooks -haltOnErrors = True - -# Main wrapper function. Required to make local vars work correctly -def main(): - global dockerComposeServicesYaml # The loaded memory YAML of all checked services - global toRun # Switch for which function to run when executed - global buildHooks # Where to place the options menu result - global currentServiceName # Name of the current service - global issues # Returned issues dict - global haltOnErrors # Turn on to allow erroring - - # runtime vars - portConflicts = [] - - # This lets the menu know whether to put " >> Options " or not - # This function is REQUIRED. - def checkForOptionsHook(): - try: - buildHooks["options"] = callable(runOptionsMenu) - except: - buildHooks["options"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPreBuildHook(): - try: - buildHooks["preBuildHook"] = callable(preBuild) - except: - buildHooks["preBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPostBuildHook(): - try: - buildHooks["postBuildHook"] = callable(postBuild) - except: - buildHooks["postBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForRunChecksHook(): - try: - buildHooks["runChecksHook"] = callable(runChecks) - except: - buildHooks["runChecksHook"] = False - return buildHooks - return buildHooks - - # This service will not check anything unless this is set - # This function is optional, and will run each time the menu is rendered - def runChecks(): - checkForIssues() - return [] - - # This function is optional, and will run after the docker-compose.yml file is written to disk. - def postBuild(): - return True - - # This function is optional, and will run just before the build docker-compose.yml code. - def preBuild(): - return True - - # ##################################### - # Supporting functions below - # ##################################### - - def checkForIssues(): - return True - - if haltOnErrors: - eval(toRun)() - else: - try: - eval(toRun)() - except: - pass - -# This check isn't required, but placed here for debugging purposes -global currentServiceName # Name of the current service -if currentServiceName == 'plex': - main() -else: - print("Error. '{}' Tried to run 'plex' config".format(currentServiceName)) diff --git a/.templates/plex/service.yml b/.templates/plex/service.yml deleted file mode 100644 index cc1716476..000000000 --- a/.templates/plex/service.yml +++ /dev/null @@ -1,12 +0,0 @@ -plex: - image: linuxserver/plex - container_name: plex - network_mode: host - environment: - - PUID=1000 - - PGID=1000 - - VERSION=docker - volumes: - - ./volumes/plex/config:/config - - ./volumes/plex/transcode:/transcode - restart: unless-stopped diff --git a/.templates/portainer-ce/build.py b/.templates/portainer-ce/build.py deleted file mode 100755 index 475562973..000000000 --- a/.templates/portainer-ce/build.py +++ /dev/null @@ -1,316 +0,0 @@ -#!/usr/bin/env python3 - -issues = {} # Returned issues dict -buildHooks = {} # Options, and others hooks -haltOnErrors = True - -# Main wrapper function. Required to make local vars work correctly -def main(): - import os - import time - import ruamel.yaml - import signal - import sys - from blessed import Terminal - - from deps.chars import specialChars, commonTopBorder, commonBottomBorder, commonEmptyLine, padText - from deps.consts import servicesDirectory, templatesDirectory - from deps.common_functions import getExternalPorts, getInternalPorts, checkPortConflicts, enterPortNumberWithWhiptail - - yaml = ruamel.yaml.YAML() - yaml.preserve_quotes = True - - global dockerComposeServicesYaml # The loaded memory YAML of all checked services - global toRun # Switch for which function to run when executed - global buildHooks # Where to place the options menu result - global currentServiceName # Name of the current service - global issues # Returned issues dict - global haltOnErrors # Turn on to allow erroring - global hideHelpText # Showing and hiding the help controls text - - try: # If not already set, then set it. - hideHelpText = hideHelpText - except: - hideHelpText = False - - documentationHint = 'https://sensorsiot.github.io/IOTstack/Containers/Portainer-ce' - - # runtime vars - portConflicts = [] - - # This lets the menu know whether to put " >> Options " or not - # This function is REQUIRED. - def checkForOptionsHook(): - try: - buildHooks["options"] = callable(runOptionsMenu) - except: - buildHooks["options"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPreBuildHook(): - try: - buildHooks["preBuildHook"] = callable(preBuild) - except: - buildHooks["preBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPostBuildHook(): - try: - buildHooks["postBuildHook"] = callable(postBuild) - except: - buildHooks["postBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForRunChecksHook(): - try: - buildHooks["runChecksHook"] = callable(runChecks) - except: - buildHooks["runChecksHook"] = False - return buildHooks - return buildHooks - - # This service will not check anything unless this is set - # This function is optional, and will run each time the menu is rendered - def runChecks(): - checkForIssues() - return [] - - # This function is optional, and will run after the docker-compose.yml file is written to disk. - def postBuild(): - return True - - # This function is optional, and will run just before the build docker-compose.yml code. - def preBuild(): - return True - - # ##################################### - # Supporting functions below - # ##################################### - - def checkForIssues(): - for (index, serviceName) in enumerate(dockerComposeServicesYaml): - if not currentServiceName == serviceName: # Skip self - currentServicePorts = getExternalPorts(currentServiceName, dockerComposeServicesYaml) - portConflicts = checkPortConflicts(serviceName, currentServicePorts, dockerComposeServicesYaml) - if (len(portConflicts) > 0): - issues["portConflicts"] = portConflicts - - # ##################################### - # End Supporting functions - # ##################################### - - ############################ - # Menu Logic - ############################ - - global currentMenuItemIndex - global selectionInProgress - global menuNavigateDirection - global needsRender - - selectionInProgress = True - currentMenuItemIndex = 0 - menuNavigateDirection = 0 - needsRender = 1 - term = Terminal() - hotzoneLocation = [((term.height // 16) + 6), 0] - - def goBack(): - global selectionInProgress - global needsRender - selectionInProgress = False - needsRender = 1 - return True - - def enterPortNumberExec(): - # global term - global needsRender - global dockerComposeServicesYaml - externalPort = getExternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - internalPort = getInternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - newPortNumber = enterPortNumberWithWhiptail(term, dockerComposeServicesYaml, currentServiceName, hotzoneLocation, externalPort) - - if newPortNumber > 0: - dockerComposeServicesYaml[currentServiceName]["ports"][0] = "{newExtPort}:{oldIntPort}".format( - newExtPort = newPortNumber, - oldIntPort = internalPort - ) - createMenu() - needsRender = 1 - - def onResize(sig, action): - global portainerCeBuildOptions - global currentMenuItemIndex - mainRender(1, portainerCeBuildOptions, currentMenuItemIndex) - - portainerCeBuildOptions = [] - - def createMenu(): - global portainerCeBuildOptions - try: - portainerCeBuildOptions = [] - portNumber = getExternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - portainerCeBuildOptions.append([ - "Change external WUI Port Number from: {port}".format(port=portNumber), - enterPortNumberExec - ]) - except: # Error getting port - pass - portainerCeBuildOptions.append(["Go back", goBack]) - - def runOptionsMenu(): - createMenu() - menuEntryPoint() - return True - - def renderHotZone(term, menu, selection, hotzoneLocation): - lineLengthAtTextStart = 71 - print(term.move(hotzoneLocation[0], hotzoneLocation[1])) - for (index, menuItem) in enumerate(menu): - toPrint = "" - if index == selection: - toPrint += ('{bv} -> {t.blue_on_green} {title} {t.normal} <-'.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) - else: - toPrint += ('{bv} {t.normal} {title} '.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) - - for i in range(lineLengthAtTextStart - len(menuItem[0])): - toPrint += " " - - toPrint += "{bv}".format(bv=specialChars[renderMode]["borderVertical"]) - - toPrint = term.center(toPrint) - - print(toPrint) - - def mainRender(needsRender, menu, selection): - term = Terminal() - - if needsRender == 1: - print(term.clear()) - print(term.move_y(term.height // 16)) - print(term.black_on_cornsilk4(term.center('IOTstack Portainer-CE Options'))) - print("") - print(term.center(commonTopBorder(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Select Option to configure {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - - if needsRender >= 1: - renderHotZone(term, menu, selection, hotzoneLocation) - - if needsRender == 1: - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - if not hideHelpText: - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Enter] to run command or save input {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Escape] to go back to build stack menu {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - if len(documentationHint) > 1: - if len(documentationHint) > 56: - documentationAndPadding = padText(documentationHint, 71) - print(term.center("{bv} Documentation: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} {dap} {bv}".format(bv=specialChars[renderMode]["borderVertical"], dap=documentationAndPadding))) - else: - documentationAndPadding = padText(documentationHint, 56) - print(term.center("{bv} Documentation: {dap} {bv}".format(bv=specialChars[renderMode]["borderVertical"], dap=documentationAndPadding))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonBottomBorder(renderMode))) - - def runSelection(selection): - import types - global portainerCeBuildOptions - if len(portainerCeBuildOptions[selection]) > 1 and isinstance(portainerCeBuildOptions[selection][1], types.FunctionType): - portainerCeBuildOptions[selection][1]() - else: - print(term.green_reverse('IOTstack Error: No function assigned to menu item: "{}"'.format(nodeRedBuildOptions[selection][0]))) - - def isMenuItemSelectable(menu, index): - if len(menu) > index: - if len(menu[index]) > 2: - if menu[index][2]["skip"] == True: - return False - return True - - def menuEntryPoint(): - # These need to be reglobalised due to eval() - global currentMenuItemIndex - global selectionInProgress - global menuNavigateDirection - global needsRender - global hideHelpText - global portainerCeBuildOptions - term = Terminal() - with term.fullscreen(): - menuNavigateDirection = 0 - mainRender(needsRender, portainerCeBuildOptions, currentMenuItemIndex) - selectionInProgress = True - with term.cbreak(): - while selectionInProgress: - menuNavigateDirection = 0 - - if needsRender: # Only rerender when changed to prevent flickering - mainRender(needsRender, portainerCeBuildOptions, currentMenuItemIndex) - needsRender = 0 - - key = term.inkey(esc_delay=0.05) - if key.is_sequence: - if key.name == 'KEY_TAB': - menuNavigateDirection += 1 - if key.name == 'KEY_DOWN': - menuNavigateDirection += 1 - if key.name == 'KEY_UP': - menuNavigateDirection -= 1 - if key.name == 'KEY_LEFT': - goBack() - if key.name == 'KEY_ENTER': - runSelection(currentMenuItemIndex) - if key.name == 'KEY_ESCAPE': - return True - elif key: - if key == 'h': # H pressed - if hideHelpText: - hideHelpText = False - else: - hideHelpText = True - mainRender(1, portainerCeBuildOptions, currentMenuItemIndex) - - if menuNavigateDirection != 0: # If a direction was pressed, find next selectable item - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(portainerCeBuildOptions) - needsRender = 2 - - while not isMenuItemSelectable(portainerCeBuildOptions, currentMenuItemIndex): - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(portainerCeBuildOptions) - return True - - #################### - # End menu section - #################### - - - if haltOnErrors: - eval(toRun)() - else: - try: - eval(toRun)() - except: - pass - -# This check isn't required, but placed here for debugging purposes -global currentServiceName # Name of the current service -if currentServiceName == 'portainer-ce': - main() -else: - print("Error. '{}' Tried to run 'portainer-ce' config".format(currentServiceName)) diff --git a/.templates/portainer/build.py b/.templates/portainer/build.py deleted file mode 100755 index 6c692b15f..000000000 --- a/.templates/portainer/build.py +++ /dev/null @@ -1,317 +0,0 @@ -#!/usr/bin/env python3 - -issues = {} # Returned issues dict -buildHooks = {} # Options, and others hooks -haltOnErrors = True - -# Main wrapper function. Required to make local vars work correctly -def main(): - import os - import time - import ruamel.yaml - import signal - import sys - from blessed import Terminal - - from deps.chars import specialChars, commonTopBorder, commonBottomBorder, commonEmptyLine, padText - from deps.consts import servicesDirectory, templatesDirectory - from deps.common_functions import getExternalPorts, getInternalPorts, checkPortConflicts, enterPortNumberWithWhiptail - - global dockerComposeServicesYaml # The loaded memory YAML of all checked services - global toRun # Switch for which function to run when executed - global buildHooks # Where to place the options menu result - global currentServiceName # Name of the current service - global issues # Returned issues dict - global haltOnErrors # Turn on to allow erroring - global hideHelpText # Showing and hiding the help controls text - - yaml = ruamel.yaml.YAML() - yaml.preserve_quotes = True - - try: # If not already set, then set it. - hideHelpText = hideHelpText - except: - hideHelpText = False - - documentationHint = 'https://sensorsiot.github.io/IOTstack/Containers/Portainer' - - # runtime vars - portConflicts = [] - - # This lets the menu know whether to put " >> Options " or not - # This function is REQUIRED. - def checkForOptionsHook(): - try: - buildHooks["options"] = callable(runOptionsMenu) - except: - buildHooks["options"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPreBuildHook(): - try: - buildHooks["preBuildHook"] = callable(preBuild) - except: - buildHooks["preBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPostBuildHook(): - try: - buildHooks["postBuildHook"] = callable(postBuild) - except: - buildHooks["postBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForRunChecksHook(): - try: - buildHooks["runChecksHook"] = callable(runChecks) - except: - buildHooks["runChecksHook"] = False - return buildHooks - return buildHooks - - # This service will not check anything unless this is set - # This function is optional, and will run each time the menu is rendered - def runChecks(): - checkForIssues() - return [] - - # This function is optional, and will run after the docker-compose.yml file is written to disk. - def postBuild(): - return True - - # This function is optional, and will run just before the build docker-compose.yml code. - def preBuild(): - return True - - # ##################################### - # Supporting functions below - # ##################################### - - def checkForIssues(): - issues["deprecation"] = 'Portainer is deprecated and may be removed from IOTstack at any time. Use Portainer-CE instead.' - for (index, serviceName) in enumerate(dockerComposeServicesYaml): - if not currentServiceName == serviceName: # Skip self - currentServicePorts = getExternalPorts(currentServiceName, dockerComposeServicesYaml) - portConflicts = checkPortConflicts(serviceName, currentServicePorts, dockerComposeServicesYaml) - if (len(portConflicts) > 0): - issues["portConflicts"] = portConflicts - - # ##################################### - # End Supporting functions - # ##################################### - - ############################ - # Menu Logic - ############################ - - global currentMenuItemIndex - global selectionInProgress - global menuNavigateDirection - global needsRender - - selectionInProgress = True - currentMenuItemIndex = 0 - menuNavigateDirection = 0 - needsRender = 1 - term = Terminal() - hotzoneLocation = [((term.height // 16) + 6), 0] - - def goBack(): - global selectionInProgress - global needsRender - selectionInProgress = False - needsRender = 1 - return True - - def enterPortNumberExec(): - # global term - global needsRender - global dockerComposeServicesYaml - externalPort = getExternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - internalPort = getInternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - newPortNumber = enterPortNumberWithWhiptail(term, dockerComposeServicesYaml, currentServiceName, hotzoneLocation, externalPort) - - if newPortNumber > 0: - dockerComposeServicesYaml[currentServiceName]["ports"][0] = "{newExtPort}:{oldIntPort}".format( - newExtPort = newPortNumber, - oldIntPort = internalPort - ) - createMenu() - needsRender = 1 - - def onResize(sig, action): - global portainerBuildOptions - global currentMenuItemIndex - mainRender(1, portainerBuildOptions, currentMenuItemIndex) - - portainerBuildOptions = [] - - def createMenu(): - global portainerBuildOptions - try: - portainerBuildOptions = [] - portNumber = getExternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - portainerBuildOptions.append([ - "Change external WUI Port Number from: {port}".format(port=portNumber), - enterPortNumberExec - ]) - except: # Error getting port - pass - portainerBuildOptions.append(["Go back", goBack]) - - def runOptionsMenu(): - createMenu() - menuEntryPoint() - return True - - def renderHotZone(term, menu, selection, hotzoneLocation): - lineLengthAtTextStart = 71 - print(term.move(hotzoneLocation[0], hotzoneLocation[1])) - for (index, menuItem) in enumerate(menu): - toPrint = "" - if index == selection: - toPrint += ('{bv} -> {t.blue_on_green} {title} {t.normal} <-'.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) - else: - toPrint += ('{bv} {t.normal} {title} '.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) - - for i in range(lineLengthAtTextStart - len(menuItem[0])): - toPrint += " " - - toPrint += "{bv}".format(bv=specialChars[renderMode]["borderVertical"]) - - toPrint = term.center(toPrint) - - print(toPrint) - - def mainRender(needsRender, menu, selection): - term = Terminal() - - if needsRender == 1: - print(term.clear()) - print(term.move_y(term.height // 16)) - print(term.black_on_cornsilk4(term.center('IOTstack Portainer Options'))) - print("") - print(term.center(commonTopBorder(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Select Option to configure {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - - if needsRender >= 1: - renderHotZone(term, menu, selection, hotzoneLocation) - - if needsRender == 1: - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - if not hideHelpText: - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Enter] to run command or save input {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Escape] to go back to build stack menu {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - if len(documentationHint) > 1: - if len(documentationHint) > 56: - documentationAndPadding = padText(documentationHint, 71) - print(term.center("{bv} Documentation: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} {dap} {bv}".format(bv=specialChars[renderMode]["borderVertical"], dap=documentationAndPadding))) - else: - documentationAndPadding = padText(documentationHint, 56) - print(term.center("{bv} Documentation: {dap} {bv}".format(bv=specialChars[renderMode]["borderVertical"], dap=documentationAndPadding))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonBottomBorder(renderMode))) - - def runSelection(selection): - import types - global portainerBuildOptions - if len(portainerBuildOptions[selection]) > 1 and isinstance(portainerBuildOptions[selection][1], types.FunctionType): - portainerBuildOptions[selection][1]() - else: - print(term.green_reverse('IOTstack Error: No function assigned to menu item: "{}"'.format(nodeRedBuildOptions[selection][0]))) - - def isMenuItemSelectable(menu, index): - if len(menu) > index: - if len(menu[index]) > 2: - if menu[index][2]["skip"] == True: - return False - return True - - def menuEntryPoint(): - # These need to be reglobalised due to eval() - global currentMenuItemIndex - global selectionInProgress - global menuNavigateDirection - global needsRender - global hideHelpText - global portainerBuildOptions - term = Terminal() - with term.fullscreen(): - menuNavigateDirection = 0 - mainRender(needsRender, portainerBuildOptions, currentMenuItemIndex) - selectionInProgress = True - with term.cbreak(): - while selectionInProgress: - menuNavigateDirection = 0 - - if needsRender: # Only rerender when changed to prevent flickering - mainRender(needsRender, portainerBuildOptions, currentMenuItemIndex) - needsRender = 0 - - key = term.inkey(esc_delay=0.05) - if key.is_sequence: - if key.name == 'KEY_TAB': - menuNavigateDirection += 1 - if key.name == 'KEY_DOWN': - menuNavigateDirection += 1 - if key.name == 'KEY_UP': - menuNavigateDirection -= 1 - if key.name == 'KEY_LEFT': - goBack() - if key.name == 'KEY_ENTER': - runSelection(currentMenuItemIndex) - if key.name == 'KEY_ESCAPE': - return True - elif key: - if key == 'h': # H pressed - if hideHelpText: - hideHelpText = False - else: - hideHelpText = True - mainRender(1, portainerBuildOptions, currentMenuItemIndex) - - if menuNavigateDirection != 0: # If a direction was pressed, find next selectable item - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(portainerBuildOptions) - needsRender = 2 - - while not isMenuItemSelectable(portainerBuildOptions, currentMenuItemIndex): - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(portainerBuildOptions) - return True - - #################### - # End menu section - #################### - - - if haltOnErrors: - eval(toRun)() - else: - try: - eval(toRun)() - except: - pass - -# This check isn't required, but placed here for debugging purposes -global currentServiceName # Name of the current service -if currentServiceName == 'portainer': - main() -else: - print("Error. '{}' Tried to run 'portainer' config".format(currentServiceName)) diff --git a/.templates/portainer/service.yml b/.templates/portainer/service.yml deleted file mode 100644 index 23f99e8a2..000000000 --- a/.templates/portainer/service.yml +++ /dev/null @@ -1,11 +0,0 @@ -portainer: - container_name: portainer - image: portainer/portainer - restart: unless-stopped - ports: - - "9002:9000" - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - ./volumes/portainer/data:/data - networks: - - iotstack_nw diff --git a/.templates/portainer_agent/service.yml b/.templates/portainer_agent/service.yml deleted file mode 100644 index d0315469a..000000000 --- a/.templates/portainer_agent/service.yml +++ /dev/null @@ -1,9 +0,0 @@ - portainer_agent: - image: portainer/agent - container_name: portainer-agent - ports: - - "9001:9001" - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - /var/lib/docker/volumes:/var/lib/docker/volumes - restart: unless-stopped diff --git a/.templates/postgres/build.py b/.templates/postgres/build.py deleted file mode 100755 index d2d589190..000000000 --- a/.templates/postgres/build.py +++ /dev/null @@ -1,399 +0,0 @@ -#!/usr/bin/env python3 - -issues = {} # Returned issues dict -buildHooks = {} # Options, and others hooks -haltOnErrors = True - -# Main wrapper function. Required to make local vars work correctly -def main(): - import os - import time - import sys - import ruamel.yaml - import signal - import subprocess - from blessed import Terminal - - from deps.chars import specialChars, commonTopBorder, commonBottomBorder, commonEmptyLine, padText - from deps.consts import servicesDirectory, templatesDirectory, buildSettingsFileName, servicesFileName - from deps.common_functions import getExternalPorts, getInternalPorts, checkPortConflicts - - yaml = ruamel.yaml.YAML() - yaml.preserve_quotes = True - - global dockerComposeServicesYaml # The loaded memory YAML of all checked services - global toRun # Switch for which function to run when executed - global buildHooks # Where to place the options menu result - global currentServiceName # Name of the current service - global issues # Returned issues dict - global haltOnErrors # Turn on to allow erroring - global hideHelpText # Showing and hiding the help controls text - global serviceService - - serviceService = servicesDirectory + currentServiceName - serviceTemplate = templatesDirectory + currentServiceName - buildSettings = serviceService + buildSettingsFileName - - try: # If not already set, then set it. - hideHelpText = hideHelpText - except: - hideHelpText = False - - documentationHint = 'https://sensorsiot.github.io/IOTstack/Containers/PostgreSQL' - - # runtime vars - portConflicts = [] - - # This lets the menu know whether to put " >> Options " or not - # This function is REQUIRED. - def checkForOptionsHook(): - try: - buildHooks["options"] = callable(runOptionsMenu) - except: - buildHooks["options"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPreBuildHook(): - try: - buildHooks["preBuildHook"] = callable(preBuild) - except: - buildHooks["preBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPostBuildHook(): - try: - buildHooks["postBuildHook"] = callable(postBuild) - except: - buildHooks["postBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForRunChecksHook(): - try: - buildHooks["runChecksHook"] = callable(runChecks) - except: - buildHooks["runChecksHook"] = False - return buildHooks - return buildHooks - - # This service will not check anything unless this is set - # This function is optional, and will run each time the menu is rendered - def runChecks(): - checkForIssues() - return [] - - # This function is optional, and will run after the docker-compose.yml file is written to disk. - def postBuild(): - return True - - # This function is optional, and will run just before the build docker-compose.yml code. - def preBuild(): - # Multi-service: - with open((r'%s/' % serviceTemplate) + servicesFileName) as objServiceFile: - serviceYamlTemplate = yaml.load(objServiceFile) - - oldBuildCache = {} - try: - with open(r'%s' % buildCache) as objBuildCache: - oldBuildCache = yaml.load(objBuildCache) - except: - pass - - buildCacheServices = {} - if "services" in oldBuildCache: - buildCacheServices = oldBuildCache["services"] - - if not os.path.exists(serviceService): - os.makedirs(serviceService, exist_ok=True) - - if os.path.exists(buildSettings): - # Password randomisation - with open(r'%s' % buildSettings) as objBuildSettingsFile: - postgresDbYamlBuildOptions = yaml.load(objBuildSettingsFile) - if ( - postgresDbYamlBuildOptions["databasePasswordOption"] == "Randomise database password for this build" - or postgresDbYamlBuildOptions["databasePasswordOption"] == "Randomise database password every build" - or postgresDbYamlBuildOptions["databasePasswordOption"] == "Use default password for this build" - ): - if postgresDbYamlBuildOptions["databasePasswordOption"] == "Use default password for this build": - newAdminPassword = "IOtSt4ckToorPostgr3sDb" - newPassword = "IOtSt4ckpostgresDbPw" - else: - newAdminPassword = generateRandomString() - newPassword = generateRandomString() - for (index, serviceName) in enumerate(serviceYamlTemplate): - dockerComposeServicesYaml[serviceName] = serviceYamlTemplate[serviceName] - if "environment" in serviceYamlTemplate[serviceName]: - for (envIndex, envName) in enumerate(serviceYamlTemplate[serviceName]["environment"]): - envName = envName.replace("%randomAdminPassword%", newAdminPassword) - envName = envName.replace("%randomPassword%", newPassword) - dockerComposeServicesYaml[serviceName]["environment"][envIndex] = envName - - # Ensure you update the "Do nothing" and other 2 strings used for password settings in 'passwords.py' - if (postgresDbYamlBuildOptions["databasePasswordOption"] == "Randomise database password for this build"): - postgresDbYamlBuildOptions["databasePasswordOption"] = "Do nothing" - with open(buildSettings, 'w') as outputFile: - yaml.dump(postgresDbYamlBuildOptions, outputFile) - else: # Do nothing - don't change password - for (index, serviceName) in enumerate(buildCacheServices): - if serviceName in buildCacheServices: # Load service from cache if exists (to maintain password) - dockerComposeServicesYaml[serviceName] = buildCacheServices[serviceName] - else: - dockerComposeServicesYaml[serviceName] = serviceYamlTemplate[serviceName] - - else: - print("Postgres Warning: Build settings file not found, using default password") - time.sleep(1) - newAdminPassword = "IOtSt4ckToorPostgr3sDb" - newPassword = "IOtSt4ckpostgresDbPw" - for (index, serviceName) in enumerate(serviceYamlTemplate): - dockerComposeServicesYaml[serviceName] = serviceYamlTemplate[serviceName] - if "environment" in serviceYamlTemplate[serviceName]: - for (envIndex, envName) in enumerate(serviceYamlTemplate[serviceName]["environment"]): - envName = envName.replace("%randomAdminPassword%", newAdminPassword) - envName = envName.replace("%randomPassword%", newPassword) - dockerComposeServicesYaml[serviceName]["environment"][envIndex] = envName - postgresDbYamlBuildOptions = { - "version": "1", - "application": "IOTstack", - "service": "Postgres", - "comment": "Postgres Build Options" - } - - postgresDbYamlBuildOptions["databasePasswordOption"] = "Do nothing" - with open(buildSettings, 'w') as outputFile: - yaml.dump(postgresDbYamlBuildOptions, outputFile) - - return True - - - # ##################################### - # Supporting functions below - # ##################################### - - def checkForIssues(): - for (index, serviceName) in enumerate(dockerComposeServicesYaml): - if not currentServiceName == serviceName: # Skip self - currentServicePorts = getExternalPorts(currentServiceName, dockerComposeServicesYaml) - portConflicts = checkPortConflicts(serviceName, currentServicePorts, dockerComposeServicesYaml) - if (len(portConflicts) > 0): - issues["portConflicts"] = portConflicts - - # ##################################### - # End Supporting functions - # ##################################### - - ############################ - # Menu Logic - ############################ - - global currentMenuItemIndex - global selectionInProgress - global menuNavigateDirection - global needsRender - - selectionInProgress = True - currentMenuItemIndex = 0 - menuNavigateDirection = 0 - needsRender = 1 - term = Terminal() - hotzoneLocation = [((term.height // 16) + 6), 0] - - def goBack(): - global selectionInProgress - global needsRender - selectionInProgress = False - needsRender = 1 - return True - - def setPasswordOptions(): - global needsRender - global hasRebuiltAddons - passwordOptionsMenuFilePath = "./.templates/{currentService}/passwords.py".format(currentService=currentServiceName) - with open(passwordOptionsMenuFilePath, "rb") as pythonDynamicImportFile: - code = compile(pythonDynamicImportFile.read(), passwordOptionsMenuFilePath, "exec") - execGlobals = { - "currentServiceName": currentServiceName, - "renderMode": renderMode - } - execLocals = {} - screenActive = False - exec(code, execGlobals, execLocals) - signal.signal(signal.SIGWINCH, onResize) - screenActive = True - needsRender = 1 - - def onResize(sig, action): - global postgresDbBuildOptions - global currentMenuItemIndex - mainRender(1, postgresDbBuildOptions, currentMenuItemIndex) - - postgresDbBuildOptions = [] - - def createMenu(): - global postgresDbBuildOptions - global serviceService - - postgresDbBuildOptions = [] - postgresDbBuildOptions.append([ - "Postgres Password Options", - setPasswordOptions - ]) - - postgresDbBuildOptions.append(["Go back", goBack]) - - def runOptionsMenu(): - createMenu() - menuEntryPoint() - return True - - def renderHotZone(term, menu, selection, hotzoneLocation): - lineLengthAtTextStart = 71 - print(term.move(hotzoneLocation[0], hotzoneLocation[1])) - for (index, menuItem) in enumerate(menu): - toPrint = "" - if index == selection: - toPrint += ('{bv} -> {t.blue_on_green} {title} {t.normal} <-'.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) - else: - toPrint += ('{bv} {t.normal} {title} '.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) - - for i in range(lineLengthAtTextStart - len(menuItem[0])): - toPrint += " " - - toPrint += "{bv}".format(bv=specialChars[renderMode]["borderVertical"]) - - toPrint = term.center(toPrint) - - print(toPrint) - - def mainRender(needsRender, menu, selection): - term = Terminal() - - if needsRender == 1: - print(term.clear()) - print(term.move_y(term.height // 16)) - print(term.black_on_cornsilk4(term.center('IOTstack Postgres Options'))) - print("") - print(term.center(commonTopBorder(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Select Option to configure {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - - if needsRender >= 1: - renderHotZone(term, menu, selection, hotzoneLocation) - - if needsRender == 1: - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - if not hideHelpText: - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Enter] to run command or save input {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Escape] to go back to build stack menu {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - if len(documentationHint) > 1: - if len(documentationHint) > 56: - documentationAndPadding = padText(documentationHint, 71) - print(term.center("{bv} Documentation: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} {dap} {bv}".format(bv=specialChars[renderMode]["borderVertical"], dap=documentationAndPadding))) - else: - documentationAndPadding = padText(documentationHint, 56) - print(term.center("{bv} Documentation: {dap} {bv}".format(bv=specialChars[renderMode]["borderVertical"], dap=documentationAndPadding))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonBottomBorder(renderMode))) - - def runSelection(selection): - import types - global postgresDbBuildOptions - if len(postgresDbBuildOptions[selection]) > 1 and isinstance(postgresDbBuildOptions[selection][1], types.FunctionType): - postgresDbBuildOptions[selection][1]() - else: - print(term.green_reverse('IOTstack Error: No function assigned to menu item: "{}"'.format(nodeRedBuildOptions[selection][0]))) - - def isMenuItemSelectable(menu, index): - if len(menu) > index: - if len(menu[index]) > 2: - if menu[index][2]["skip"] == True: - return False - return True - - def menuEntryPoint(): - # These need to be reglobalised due to eval() - global currentMenuItemIndex - global selectionInProgress - global menuNavigateDirection - global needsRender - global hideHelpText - global postgresDbBuildOptions - term = Terminal() - with term.fullscreen(): - menuNavigateDirection = 0 - mainRender(needsRender, postgresDbBuildOptions, currentMenuItemIndex) - selectionInProgress = True - with term.cbreak(): - while selectionInProgress: - menuNavigateDirection = 0 - - if needsRender: # Only rerender when changed to prevent flickering - mainRender(needsRender, postgresDbBuildOptions, currentMenuItemIndex) - needsRender = 0 - - key = term.inkey(esc_delay=0.05) - if key.is_sequence: - if key.name == 'KEY_TAB': - menuNavigateDirection += 1 - if key.name == 'KEY_DOWN': - menuNavigateDirection += 1 - if key.name == 'KEY_UP': - menuNavigateDirection -= 1 - if key.name == 'KEY_LEFT': - goBack() - if key.name == 'KEY_ENTER': - runSelection(currentMenuItemIndex) - if key.name == 'KEY_ESCAPE': - return True - elif key: - if key == 'h': # H pressed - if hideHelpText: - hideHelpText = False - else: - hideHelpText = True - mainRender(1, postgresDbBuildOptions, currentMenuItemIndex) - - if menuNavigateDirection != 0: # If a direction was pressed, find next selectable item - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(postgresDbBuildOptions) - needsRender = 2 - - while not isMenuItemSelectable(postgresDbBuildOptions, currentMenuItemIndex): - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(postgresDbBuildOptions) - return True - - #################### - # End menu section - #################### - - - if haltOnErrors: - eval(toRun)() - else: - try: - eval(toRun)() - except: - pass - -# This check isn't required, but placed here for debugging purposes -global currentServiceName # Name of the current service -if currentServiceName == 'postgres': - main() -else: - print("Error. '{}' Tried to run 'postgres' config".format(currentServiceName)) diff --git a/.templates/postgres/service.yml b/.templates/postgres/service.yml deleted file mode 100644 index 76bbe7179..000000000 --- a/.templates/postgres/service.yml +++ /dev/null @@ -1,14 +0,0 @@ -postgres: - container_name: postgres - image: postgres - restart: unless-stopped - environment: - - POSTGRES_USER=postuser - - POSTGRES_PASSWORD=%randomPassword% - - POSTGRES_DB=postdb - ports: - - "5432:5432" - volumes: - - ./volumes/postgres/data:/var/lib/postgresql/data - networks: - - iotstack_nw diff --git a/.templates/postgres/terminal.sh b/.templates/postgres/terminal.sh deleted file mode 100644 index 82c5a3246..000000000 --- a/.templates/postgres/terminal.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -echo 'Use the command "psql DATABASE USER" to enter your database, replace DATABASE and USER with your values' -echo "Remember to end queries with a semicolon ;" -echo "" -echo "IOTstack postgres Documentation: https://sensorsiot.github.io/IOTstack/Containers/PostgreSQL/" -echo "" -echo "docker exec -it postgres bash" - -docker exec -it postgres bash diff --git a/.templates/prometheus/build.sh b/.templates/prometheus/build.sh deleted file mode 100755 index 44841aee2..000000000 --- a/.templates/prometheus/build.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/bash - -DOCKER_COMPOSE_PATH=./.tmp/docker-compose.tmp.yml -TEMPLATE_PATH=./.templates/prometheus - -# TODO: Prometheus needs to have a build.py file created before this bash script is executed. - -if [[ ! -f $DOCKER_COMPOSE_PATH ]]; then - echo "[Prometheus] Warning: $DOCKER_COMPOSE_PATH does not exist." -fi - -# Configure Prometheus Add-ons - -option_selection=$(whiptail --title "Select Prometheus Options" --checklist --separate-output \ - "Use the [SPACEBAR] to select add-on containers from the list below." 20 78 12 -- \ - "node-exporter" "monitor this computer " "ON" \ - "cadvisor-arm" "monitor full container stack " "ON" \ - 3>&1 1>&2 2>&3) - -mapfile -t selected_options <<< "$option_selection" - -# (cat $TEMPLATE_PATH/service.yml; echo) >> $DOCKER_COMPOSE_PATH - -for option in "${selected_options[@]}"; do - # insert add-on service - (cat $TEMPLATE_PATH/service_${option}.yml; echo) >> $DOCKER_COMPOSE_PATH - - # include add-on in depends_on - sed -i.bak -e "/depends_on:/a\\ - \\ \\ \\ \\ \\ \\ - ${option}" $DOCKER_COMPOSE_PATH -done - -# clean up -rm ${DOCKER_COMPOSE_PATH}.bak - -# TODO: need build.py \ No newline at end of file diff --git a/.templates/prometheus/config.yml b/.templates/prometheus/config.yml deleted file mode 100644 index c82004b60..000000000 --- a/.templates/prometheus/config.yml +++ /dev/null @@ -1,10 +0,0 @@ -global: - scrape_interval: 10s - evaluation_interval: 10s - -# scrape_configs: -# - job_name: iotstack -# static_configs: -# - targets: -# - cadvisor:8080 -# - node-exporter:9100 diff --git a/.templates/prometheus/service.yml b/.templates/prometheus/service.yml deleted file mode 100644 index d2c0d54c7..000000000 --- a/.templates/prometheus/service.yml +++ /dev/null @@ -1,15 +0,0 @@ -prometheus: - container_name: prometheus - image: prom/prometheus:latest - restart: unless-stopped - user: "0" - ports: - - "9090:9090" - volumes: - - ./services/prometheus/config.yml:/etc/prometheus/config.yml - - ./volumes/prometheus/data:/data - command: - - '--config.file=/etc/prometheus/config.yml' - - '--storage.tsdb.path=/data' - networks: - - iotstack_nw diff --git a/.templates/prometheus/service_cadvisor-arm.yml b/.templates/prometheus/service_cadvisor-arm.yml deleted file mode 100644 index 9099e999e..000000000 --- a/.templates/prometheus/service_cadvisor-arm.yml +++ /dev/null @@ -1,15 +0,0 @@ - cadvisor-arm: - container_name: cadvisor - image: budry/cadvisor-arm:latest - restart: unless-stopped - user: "0" - privileged: true - ports: - - 8080:8080 - volumes: - - /:/rootfs:ro - - /var/run:/var/run:rw - - /sys:/sys:ro - - /var/lib/docker/:/var/lib/docker:ro - networks: - - iotstack_nw diff --git a/.templates/prometheus/service_node-exporter.yml b/.templates/prometheus/service_node-exporter.yml deleted file mode 100644 index 97fe7d751..000000000 --- a/.templates/prometheus/service_node-exporter.yml +++ /dev/null @@ -1,8 +0,0 @@ - node-exporter: - image: prom/node-exporter:latest - container_name: node_exporter - restart: unless-stopped - expose: - - 9100 - networks: - - iotstack_nw diff --git a/.templates/python/Dockerfile b/.templates/python/Dockerfile deleted file mode 100644 index f06fda813..000000000 --- a/.templates/python/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM python:3 - -WORKDIR /usr/src/app - -COPY requirements.txt ./ - -RUN pip install --no-cache-dir -r requirements.txt - -CMD [ "python", "./app.py" ] \ No newline at end of file diff --git a/.templates/python/app/app.py b/.templates/python/app/app.py deleted file mode 100755 index e75154b7c..000000000 --- a/.templates/python/app/app.py +++ /dev/null @@ -1 +0,0 @@ -print("hello world") \ No newline at end of file diff --git a/.templates/python/build.py b/.templates/python/build.py deleted file mode 100755 index 132deb517..000000000 --- a/.templates/python/build.py +++ /dev/null @@ -1,142 +0,0 @@ -#!/usr/bin/env python3 - -issues = {} # Returned issues dict -buildHooks = {} # Options, and others hooks -haltOnErrors = True - -# Main wrapper function. Required to make local vars work correctly -def main(): - import os - import time - import shutil - import subprocess - import sys - - from deps.consts import servicesDirectory, templatesDirectory, volumesDirectory - from deps.common_functions import getExternalPorts, getInternalPorts, checkPortConflicts - - global dockerComposeServicesYaml # The loaded memory YAML of all checked services - global toRun # Switch for which function to run when executed - global buildHooks # Where to place the options menu result - global currentServiceName # Name of the current service - global issues # Returned issues dict - global haltOnErrors # Turn on to allow erroring - global hideHelpText # Showing and hiding the help controls text - global serviceService - - serviceService = servicesDirectory + currentServiceName - serviceTemplate = templatesDirectory + currentServiceName - serviceVolume = volumesDirectory + currentServiceName - - try: # If not already set, then set it. - hideHelpText = hideHelpText - except: - hideHelpText = False - - # runtime vars - portConflicts = [] - - # This lets the menu know whether to put " >> Options " or not - # This function is REQUIRED. - def checkForOptionsHook(): - try: - buildHooks["options"] = callable(runOptionsMenu) - except: - buildHooks["options"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPreBuildHook(): - try: - buildHooks["preBuildHook"] = callable(preBuild) - except: - buildHooks["preBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPostBuildHook(): - try: - buildHooks["postBuildHook"] = callable(postBuild) - except: - buildHooks["postBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForRunChecksHook(): - try: - buildHooks["runChecksHook"] = callable(runChecks) - except: - buildHooks["runChecksHook"] = False - return buildHooks - return buildHooks - - # This service will not check anything unless this is set - # This function is optional, and will run each time the menu is rendered - def runChecks(): - checkForIssues() - return [] - - # This function is optional, and will run after the docker-compose.yml file is written to disk. - def postBuild(): - return True - - # This function is optional, and will run just before the build docker-compose.yml code. - def preBuild(): - # Setup service directory - if not os.path.exists(serviceService): - os.makedirs(serviceService, exist_ok=True) - - # Files copy - shutil.copy(r'%s/Dockerfile' % serviceTemplate, r'%s/Dockerfile' % serviceService) - - print("sudo mkdir -p " + serviceVolume + "/app ") - subprocess.call("user=$(whoami) && sudo mkdir -p " + serviceVolume + "/app && sudo chown -R $user:$user " + serviceVolume, shell=True) - print("sudo chown -R $user:$user ./volumes/python") - shutil.copy(r'%s/app/requirements.txt' % serviceTemplate, r'%s/app/requirements.txt' % serviceVolume) - shutil.copy(r'%s/app/app.py' % serviceTemplate, r'%s/app/app.py' % serviceVolume) - return True - - # ##################################### - # Supporting functions below - # ##################################### - - def checkForIssues(): - envFileIssues = checkEnvFiles() - if (len(envFileIssues) > 0): - issues["envFileIssues"] = envFileIssues - for (index, serviceName) in enumerate(dockerComposeServicesYaml): - if not currentServiceName == serviceName: # Skip self - currentServicePorts = getExternalPorts(currentServiceName, dockerComposeServicesYaml) - portConflicts = checkPortConflicts(serviceName, currentServicePorts, dockerComposeServicesYaml) - if (len(portConflicts) > 0): - issues["portConflicts"] = portConflicts - - def checkEnvFiles(): - envFileIssues = [] - if not os.path.exists(serviceTemplate + '/app/requirements.txt'): - envFileIssues.append(serviceTemplate + '/app/requirements.txt does not exist') - if not os.path.exists(serviceTemplate + '/app/app.py'): - envFileIssues.append(serviceTemplate + '/app/app.py does not exist') - return envFileIssues - - # ##################################### - # End Supporting functions - # ##################################### - - if haltOnErrors: - eval(toRun)() - else: - try: - eval(toRun)() - except: - pass - -# This check isn't required, but placed here for debugging purposes -global currentServiceName # Name of the current service -if currentServiceName == 'python': - main() -else: - print("Error. '{}' Tried to run 'python' config".format(currentServiceName)) diff --git a/.templates/python/service.yml b/.templates/python/service.yml deleted file mode 100644 index 658462e65..000000000 --- a/.templates/python/service.yml +++ /dev/null @@ -1,7 +0,0 @@ -python: - container_name: python - build: ./services/python/. - restart: unless-stopped - network_mode: host - volumes: - - ./volumes/python/app:/usr/src/app diff --git a/.templates/qbittorrent/service.yml b/.templates/qbittorrent/service.yml deleted file mode 100644 index 4fd4cbed4..000000000 --- a/.templates/qbittorrent/service.yml +++ /dev/null @@ -1,16 +0,0 @@ - qbittorrent: - image: linuxserver/qbittorrent - container_name: qbittorrent - environment: - - PUID=1000 - - PGID=1000 - - UMASK_SET=022 - - WEBUI_PORT=15080 - volumes: - - ./volumes/qbittorrent/config:/config - - ./volumes/qbittorrent/downloads:/downloads - ports: - - "6881:6881" - - "6881:6881/udp" - - "15080:15080" - - "1080:1080" diff --git a/.templates/rtl_433/build.py b/.templates/rtl_433/build.py deleted file mode 100755 index 70cc4ee75..000000000 --- a/.templates/rtl_433/build.py +++ /dev/null @@ -1,126 +0,0 @@ -#!/usr/bin/env python3 - -issues = {} # Returned issues dict -buildHooks = {} # Options, and others hooks -haltOnErrors = True - -# Main wrapper function. Required to make local vars work correctly -def main(): - import os - import time - import shutil - import sys - - from deps.consts import servicesDirectory, templatesDirectory - from deps.common_functions import getExternalPorts, getInternalPorts, checkPortConflicts, checkDependsOn - - global dockerComposeServicesYaml # The loaded memory YAML of all checked services - global toRun # Switch for which function to run when executed - global buildHooks # Where to place the options menu result - global currentServiceName # Name of the current service - global issues # Returned issues dict - global haltOnErrors # Turn on to allow erroring - global hideHelpText # Showing and hiding the help controls text - global serviceService - - serviceService = servicesDirectory + currentServiceName - serviceTemplate = templatesDirectory + currentServiceName - - try: # If not already set, then set it. - hideHelpText = hideHelpText - except: - hideHelpText = False - - # runtime vars - portConflicts = [] - - # This lets the menu know whether to put " >> Options " or not - # This function is REQUIRED. - def checkForOptionsHook(): - try: - buildHooks["options"] = callable(runOptionsMenu) - except: - buildHooks["options"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPreBuildHook(): - try: - buildHooks["preBuildHook"] = callable(preBuild) - except: - buildHooks["preBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPostBuildHook(): - try: - buildHooks["postBuildHook"] = callable(postBuild) - except: - buildHooks["postBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForRunChecksHook(): - try: - buildHooks["runChecksHook"] = callable(runChecks) - except: - buildHooks["runChecksHook"] = False - return buildHooks - return buildHooks - - # This service will not check anything unless this is set - # This function is optional, and will run each time the menu is rendered - def runChecks(): - checkForIssues() - return [] - - # This function is optional, and will run after the docker-compose.yml file is written to disk. - def postBuild(): - return True - - # This function is optional, and will run just before the build docker-compose.yml code. - def preBuild(): - # Setup service directory - if not os.path.exists(serviceService): - os.makedirs(serviceService, exist_ok=True) - - # Files copy - shutil.copy(r'%s/Dockerfile' % serviceTemplate, r'%s/Dockerfile' % serviceService) - return True - - # ##################################### - # Supporting functions below - # ##################################### - - def checkForIssues(): - dependsOnListMissing = checkDependsOn(currentServiceName, dockerComposeServicesYaml) - if (len(dependsOnListMissing) > 0): - issues["dependsOn"] = dependsOnListMissing - for (index, serviceName) in enumerate(dockerComposeServicesYaml): - if not currentServiceName == serviceName: # Skip self - currentServicePorts = getExternalPorts(currentServiceName, dockerComposeServicesYaml) - portConflicts = checkPortConflicts(serviceName, currentServicePorts, dockerComposeServicesYaml) - if (len(portConflicts) > 0): - issues["portConflicts"] = portConflicts - - # ##################################### - # End Supporting functions - # ##################################### - - if haltOnErrors: - eval(toRun)() - else: - try: - eval(toRun)() - except: - pass - -# This check isn't required, but placed here for debugging purposes -global currentServiceName # Name of the current service -if currentServiceName == 'rtl_433': - main() -else: - print("Error. '{}' Tried to run 'rtl_433' config".format(currentServiceName)) diff --git a/.templates/tasmoadmin/build.py b/.templates/tasmoadmin/build.py deleted file mode 100755 index b1cc6c9ab..000000000 --- a/.templates/tasmoadmin/build.py +++ /dev/null @@ -1,319 +0,0 @@ -#!/usr/bin/env python3 - -issues = {} # Returned issues dict -buildHooks = {} # Options, and others hooks -haltOnErrors = True - -# Main wrapper function. Required to make local vars work correctly -def main(): - import os - import time - import ruamel.yaml - import signal - import sys - from blessed import Terminal - - from deps.chars import specialChars, commonTopBorder, commonBottomBorder, commonEmptyLine, padText - from deps.consts import servicesDirectory, templatesDirectory - from deps.common_functions import getExternalPorts, getInternalPorts, checkPortConflicts, enterPortNumberWithWhiptail - - yaml = ruamel.yaml.YAML() - yaml.preserve_quotes = True - - global dockerComposeServicesYaml # The loaded memory YAML of all checked services - global toRun # Switch for which function to run when executed - global buildHooks # Where to place the options menu result - global currentServiceName # Name of the current service - global issues # Returned issues dict - global haltOnErrors # Turn on to allow erroring - global hideHelpText # Showing and hiding the help controls text - global serviceService - - serviceService = servicesDirectory + currentServiceName - - try: # If not already set, then set it. - hideHelpText = hideHelpText - except: - hideHelpText = False - - documentationHint = 'https://sensorsiot.github.io/IOTstack/Containers/TasmoAdmin' - - # runtime vars - portConflicts = [] - - # This lets the menu know whether to put " >> Options " or not - # This function is REQUIRED. - def checkForOptionsHook(): - try: - buildHooks["options"] = callable(runOptionsMenu) - except: - buildHooks["options"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPreBuildHook(): - try: - buildHooks["preBuildHook"] = callable(preBuild) - except: - buildHooks["preBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPostBuildHook(): - try: - buildHooks["postBuildHook"] = callable(postBuild) - except: - buildHooks["postBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForRunChecksHook(): - try: - buildHooks["runChecksHook"] = callable(runChecks) - except: - buildHooks["runChecksHook"] = False - return buildHooks - return buildHooks - - # This service will not check anything unless this is set - # This function is optional, and will run each time the menu is rendered - def runChecks(): - checkForIssues() - return [] - - # This function is optional, and will run after the docker-compose.yml file is written to disk. - def postBuild(): - return True - - # This function is optional, and will run just before the build docker-compose.yml code. - def preBuild(): - return True - - # ##################################### - # Supporting functions below - # ##################################### - - def checkForIssues(): - for (index, serviceName) in enumerate(dockerComposeServicesYaml): - if not currentServiceName == serviceName: # Skip self - currentServicePorts = getExternalPorts(currentServiceName, dockerComposeServicesYaml) - portConflicts = checkPortConflicts(serviceName, currentServicePorts, dockerComposeServicesYaml) - if (len(portConflicts) > 0): - issues["portConflicts"] = portConflicts - - # ##################################### - # End Supporting functions - # ##################################### - - ############################ - # Menu Logic - ############################ - - global currentMenuItemIndex - global selectionInProgress - global menuNavigateDirection - global needsRender - - selectionInProgress = True - currentMenuItemIndex = 0 - menuNavigateDirection = 0 - needsRender = 1 - term = Terminal() - hotzoneLocation = [((term.height // 16) + 6), 0] - - def goBack(): - global selectionInProgress - global needsRender - selectionInProgress = False - needsRender = 1 - return True - - def enterPortNumberExec(): - # global term - global needsRender - global dockerComposeServicesYaml - externalPort = getExternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - internalPort = getInternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - newPortNumber = enterPortNumberWithWhiptail(term, dockerComposeServicesYaml, currentServiceName, hotzoneLocation, externalPort) - - if newPortNumber > 0: - dockerComposeServicesYaml[currentServiceName]["ports"][0] = "{newExtPort}:{oldIntPort}".format( - newExtPort = newPortNumber, - oldIntPort = internalPort - ) - createMenu() - needsRender = 1 - - def onResize(sig, action): - global tasmoAdminBuildOptions - global currentMenuItemIndex - mainRender(1, tasmoAdminBuildOptions, currentMenuItemIndex) - - tasmoAdminBuildOptions = [] - - def createMenu(): - global tasmoAdminBuildOptions - try: - tasmoAdminBuildOptions = [] - portNumber = getExternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - tasmoAdminBuildOptions.append([ - "Change external WUI Port Number from: {port}".format(port=portNumber), - enterPortNumberExec - ]) - except: # Error getting port - pass - tasmoAdminBuildOptions.append(["Go back", goBack]) - - def runOptionsMenu(): - createMenu() - menuEntryPoint() - return True - - def renderHotZone(term, menu, selection, hotzoneLocation): - lineLengthAtTextStart = 71 - print(term.move(hotzoneLocation[0], hotzoneLocation[1])) - for (index, menuItem) in enumerate(menu): - toPrint = "" - if index == selection: - toPrint += ('{bv} -> {t.blue_on_green} {title} {t.normal} <-'.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) - else: - toPrint += ('{bv} {t.normal} {title} '.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) - - for i in range(lineLengthAtTextStart - len(menuItem[0])): - toPrint += " " - - toPrint += "{bv}".format(bv=specialChars[renderMode]["borderVertical"]) - - toPrint = term.center(toPrint) - - print(toPrint) - - def mainRender(needsRender, menu, selection): - term = Terminal() - - if needsRender == 1: - print(term.clear()) - print(term.move_y(term.height // 16)) - print(term.black_on_cornsilk4(term.center('IOTstack Tasmo Admin Options'))) - print("") - print(term.center(commonTopBorder(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Select Option to configure {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - - if needsRender >= 1: - renderHotZone(term, menu, selection, hotzoneLocation) - - if needsRender == 1: - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - if not hideHelpText: - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Enter] to run command or save input {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Escape] to go back to build stack menu {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - if len(documentationHint) > 1: - if len(documentationHint) > 56: - documentationAndPadding = padText(documentationHint, 71) - print(term.center("{bv} Documentation: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} {dap} {bv}".format(bv=specialChars[renderMode]["borderVertical"], dap=documentationAndPadding))) - else: - documentationAndPadding = padText(documentationHint, 56) - print(term.center("{bv} Documentation: {dap} {bv}".format(bv=specialChars[renderMode]["borderVertical"], dap=documentationAndPadding))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonBottomBorder(renderMode))) - - def runSelection(selection): - import types - global tasmoAdminBuildOptions - if len(tasmoAdminBuildOptions[selection]) > 1 and isinstance(tasmoAdminBuildOptions[selection][1], types.FunctionType): - tasmoAdminBuildOptions[selection][1]() - else: - print(term.green_reverse('IOTstack Error: No function assigned to menu item: "{}"'.format(nodeRedBuildOptions[selection][0]))) - - def isMenuItemSelectable(menu, index): - if len(menu) > index: - if len(menu[index]) > 2: - if menu[index][2]["skip"] == True: - return False - return True - - def menuEntryPoint(): - # These need to be reglobalised due to eval() - global currentMenuItemIndex - global selectionInProgress - global menuNavigateDirection - global needsRender - global hideHelpText - global tasmoAdminBuildOptions - term = Terminal() - with term.fullscreen(): - menuNavigateDirection = 0 - mainRender(needsRender, tasmoAdminBuildOptions, currentMenuItemIndex) - selectionInProgress = True - with term.cbreak(): - while selectionInProgress: - menuNavigateDirection = 0 - - if needsRender: # Only rerender when changed to prevent flickering - mainRender(needsRender, tasmoAdminBuildOptions, currentMenuItemIndex) - needsRender = 0 - - key = term.inkey(esc_delay=0.05) - if key.is_sequence: - if key.name == 'KEY_TAB': - menuNavigateDirection += 1 - if key.name == 'KEY_DOWN': - menuNavigateDirection += 1 - if key.name == 'KEY_UP': - menuNavigateDirection -= 1 - if key.name == 'KEY_LEFT': - goBack() - if key.name == 'KEY_ENTER': - runSelection(currentMenuItemIndex) - if key.name == 'KEY_ESCAPE': - return True - elif key: - if key == 'h': # H pressed - if hideHelpText: - hideHelpText = False - else: - hideHelpText = True - mainRender(1, tasmoAdminBuildOptions, currentMenuItemIndex) - - if menuNavigateDirection != 0: # If a direction was pressed, find next selectable item - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(tasmoAdminBuildOptions) - needsRender = 2 - - while not isMenuItemSelectable(tasmoAdminBuildOptions, currentMenuItemIndex): - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(tasmoAdminBuildOptions) - return True - - #################### - # End menu section - #################### - - - if haltOnErrors: - eval(toRun)() - else: - try: - eval(toRun)() - except: - pass - -# This check isn't required, but placed here for debugging purposes -global currentServiceName # Name of the current service -if currentServiceName == 'tasmoadmin': - main() -else: - print("Error. '{}' Tried to run 'tasmoadmin' config".format(currentServiceName)) diff --git a/.templates/tasmoadmin/service.yml b/.templates/tasmoadmin/service.yml deleted file mode 100644 index 4c95564c8..000000000 --- a/.templates/tasmoadmin/service.yml +++ /dev/null @@ -1,11 +0,0 @@ -tasmoadmin: - container_name: tasmoadmin - image: raymondmm/tasmoadmin - restart: unless-stopped - ports: - - "8088:80" - volumes: - - ./volumes/tasmoadmin/data:/data - networks: - - iotstack_nw - diff --git a/.templates/telegraf/build.py b/.templates/telegraf/build.py deleted file mode 100755 index 00a8def4f..000000000 --- a/.templates/telegraf/build.py +++ /dev/null @@ -1,153 +0,0 @@ -#!/usr/bin/env python3 - -issues = {} # Returned issues dict -buildHooks = {} # Options, and others hooks -haltOnErrors = True - -# Main wrapper function. Required to make local vars work correctly -def main(): - import os - import time - import shutil - import sys - - from deps.consts import servicesDirectory, templatesDirectory - from deps.common_functions import getExternalPorts, getInternalPorts, checkPortConflicts - - global dockerComposeServicesYaml # The loaded memory YAML of all checked services - global toRun # Switch for which function to run when executed - global buildHooks # Where to place the options menu result - global currentServiceName # Name of the current service - global issues # Returned issues dict - global haltOnErrors # Turn on to allow erroring - global hideHelpText # Showing and hiding the help controls text - global serviceService - - serviceService = servicesDirectory + currentServiceName - serviceTemplate = templatesDirectory + currentServiceName - - try: # If not already set, then set it. - hideHelpText = hideHelpText - except: - hideHelpText = False - - # runtime vars - portConflicts = [] - - # This lets the menu know whether to put " >> Options " or not - # This function is REQUIRED. - def checkForOptionsHook(): - try: - buildHooks["options"] = callable(runOptionsMenu) - except: - buildHooks["options"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPreBuildHook(): - try: - buildHooks["preBuildHook"] = callable(preBuild) - except: - buildHooks["preBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPostBuildHook(): - try: - buildHooks["postBuildHook"] = callable(postBuild) - except: - buildHooks["postBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForRunChecksHook(): - try: - buildHooks["runChecksHook"] = callable(runChecks) - except: - buildHooks["runChecksHook"] = False - return buildHooks - return buildHooks - - # This service will not check anything unless this is set - # This function is optional, and will run each time the menu is rendered - def runChecks(): - checkForIssues() - return [] - - # This function is optional, and will run after the docker-compose.yml file is written to disk. - def postBuild(): - return True - - # This function is optional, and will run just before the build docker-compose.yml code. - def preBuild(): - # Setup service directory - if not os.path.exists(serviceService): - os.makedirs(serviceService, exist_ok=True) - - # Files copy - if not os.path.exists(serviceTemplate + '/telegraf.conf'): - shutil.copy(r'%s/telegraf.conf' % serviceTemplate, r'%s/telegraf.conf' % serviceService) - return True - - # ##################################### - # Supporting functions below - # ##################################### - - def checkForIssues(): - envFileIssues = checkEnvFiles() - if (len(envFileIssues) > 0): - issues["envFileIssues"] = envFileIssues - - envReqServiceIssues = checkReqServices() - if (len(envReqServiceIssues) > 0): - issues["requiredService"] = envReqServiceIssues - - for (index, serviceName) in enumerate(dockerComposeServicesYaml): - if not currentServiceName == serviceName: # Skip self - currentServicePorts = getExternalPorts(currentServiceName, dockerComposeServicesYaml) - portConflicts = checkPortConflicts(serviceName, currentServicePorts, dockerComposeServicesYaml) - if (len(portConflicts) > 0): - issues["portConflicts"] = portConflicts - - def checkEnvFiles(): - envFileIssues = [] - if not os.path.exists(serviceTemplate + '/telegraf.conf'): - envFileIssues.append(serviceTemplate + '/telegraf.conf does not exist') - return envFileIssues - - def checkReqServices(): - try: - envReqServicesIssues = [] - if "depends_on" in dockerComposeServicesYaml[currentServiceName]: - reqServices = dockerComposeServicesYaml[currentServiceName]["depends_on"] - for (index, reqServiceName) in enumerate(reqServices): - if not reqServiceName in dockerComposeServicesYaml: - envReqServicesIssues.append(reqServiceName + ' service required') - - return envReqServicesIssues - except Exception as err: - print(err) - pass - return [] - - # ##################################### - # End Supporting functions - # ##################################### - - if haltOnErrors: - eval(toRun)() - else: - try: - eval(toRun)() - except: - pass - -# This check isn't required, but placed here for debugging purposes -global currentServiceName # Name of the current service -if currentServiceName == 'telegraf': - main() -else: - print("Error. '{}' Tried to run 'telegraf' config".format(currentServiceName)) diff --git a/.templates/telegraf/service.yml b/.templates/telegraf/service.yml deleted file mode 100644 index f16f8e52a..000000000 --- a/.templates/telegraf/service.yml +++ /dev/null @@ -1,11 +0,0 @@ -telegraf: - container_name: telegraf - image: telegraf - restart: unless-stopped - volumes: - - ./services/telegraf/telegraf.conf:/etc/telegraf/telegraf.conf:ro - depends_on: - - influxdb - - mosquitto - networks: - - iotstack_nw diff --git a/.templates/telegraf/telegraf.conf b/.templates/telegraf/telegraf.conf deleted file mode 100644 index f377c5a3f..000000000 --- a/.templates/telegraf/telegraf.conf +++ /dev/null @@ -1,237 +0,0 @@ -# Telegraf Configuration -# -# Telegraf is entirely plugin driven. All metrics are gathered from the -# declared inputs, and sent to the declared outputs. -# -# Plugins must be declared in here to be active. -# To deactivate a plugin, comment out the name and any variables. -# -# Use 'telegraf -config telegraf.conf -test' to see what metrics a config -# file would generate. -# -# Environment variables can be used anywhere in this config file, simply surround -# them with ${}. For strings the variable must be within quotes (ie, "${STR_VAR}"), -# for numbers and booleans they should be plain (ie, ${INT_VAR}, ${BOOL_VAR}) - - -# Global tags can be specified here in key="value" format. -[global_tags] - # dc = "us-east-1" # will tag all metrics with dc=us-east-1 - # rack = "1a" - ## Environment variables can be used as tags, and throughout the config file - # user = "$USER" - - -# Configuration for telegraf agent -[agent] - ## Default data collection interval for all inputs - interval = "10s" - ## Rounds collection interval to 'interval' - ## ie, if interval="10s" then always collect on :00, :10, :20, etc. - round_interval = true - - ## Telegraf will send metrics to outputs in batches of at most - ## metric_batch_size metrics. - ## This controls the size of writes that Telegraf sends to output plugins. - metric_batch_size = 1000 - - ## Maximum number of unwritten metrics per output. - metric_buffer_limit = 10000 - - ## Collection jitter is used to jitter the collection by a random amount. - ## Each plugin will sleep for a random time within jitter before collecting. - ## This can be used to avoid many plugins querying things like sysfs at the - ## same time, which can have a measurable effect on the system. - collection_jitter = "0s" - - ## Default flushing interval for all outputs. Maximum flush_interval will be - ## flush_interval + flush_jitter - flush_interval = "10s" - ## Jitter the flush interval by a random amount. This is primarily to avoid - ## large write spikes for users running a large number of telegraf instances. - ## ie, a jitter of 5s and interval 10s means flushes will happen every 10-15s - flush_jitter = "0s" - - ## By default or when set to "0s", precision will be set to the same - ## timestamp order as the collection interval, with the maximum being 1s. - ## ie, when interval = "10s", precision will be "1s" - ## when interval = "250ms", precision will be "1ms" - ## Precision will NOT be used for service inputs. It is up to each individual - ## service input to set the timestamp at the appropriate precision. - ## Valid time units are "ns", "us" (or "µs"), "ms", "s". - precision = "" - - ## Log at debug level. - # debug = false - ## Log only error level messages. - # quiet = false - - ## Log file name, the empty string means to log to stderr. - # logfile = "" - - ## The logfile will be rotated after the time interval specified. When set - ## to 0 no time based rotation is performed. Logs are rotated only when - ## written to, if there is no log activity rotation may be delayed. - # logfile_rotation_interval = "0d" - - ## The logfile will be rotated when it becomes larger than the specified - ## size. When set to 0 no size based rotation is performed. - # logfile_rotation_max_size = "0MB" - - ## Maximum number of rotated archives to keep, any older logs are deleted. - ## If set to -1, no archives are removed. - # logfile_rotation_max_archives = 5 - - ## Override default hostname, if empty use os.Hostname() - hostname = "" - ## If set to true, do no set the "host" tag in the telegraf agent. - omit_hostname = false - - -############################################################################### -# OUTPUT PLUGINS # -############################################################################### - - -# Configuration for sending metrics to InfluxDB -[[outputs.influxdb]] - # The full HTTP or UDP URL for your InfluxDB instance. - # - # Multiple URLs can be specified for a single cluster, only ONE of the - # urls will be written to each interval. - # urls = ["unix:///var/run/influxdb.sock"] - # urls = ["udp://influxdb:8089"] - urls = ["http://influxdb:8086"] - - # The target database for metrics; will be created as needed. - # For UDP url endpoint database needs to be configured on server side. - database = "telegraf" - - # The value of this tag will be used to determine the database. If this - # tag is not set the 'database' option is used as the default. - database_tag = "" - - # If true, the database tag will not be added to the metric. - exclude_database_tag = false - - # If true, no CREATE DATABASE queries will be sent. Set to true when using - # Telegraf with a user without permissions to create databases or when the - # database already exists. - skip_database_creation = false - - # Name of existing retention policy to write to. Empty string writes to - # the default retention policy. Only takes effect when using HTTP. - retention_policy = "" - - # Write consistency (clusters only), can be: "any", "one", "quorum", "all". - # Only takes effect when using HTTP. - write_consistency = "any" - - # Timeout for HTTP messages. - timeout = "5s" - - # HTTP Basic Auth - username = "telegraf" - password = "metricsmetricsmetricsmetrics" - - # HTTP User-Agent - user_agent = "telegraf" - - # UDP payload size is the maximum packet size to send. - udp_payload = "512B" - - ## Optional TLS Config for use on HTTP connections. - # tls_ca = "/etc/telegraf/ca.pem" - # tls_cert = "/etc/telegraf/cert.pem" - # tls_key = "/etc/telegraf/key.pem" - ## Use TLS but skip chain & host verification - # insecure_skip_verify = false - # - ## HTTP Proxy override, if unset values the standard proxy environment - ## variables are consulted to determine which proxy, if any, should be used. - # http_proxy = "http://corporate.proxy:3128" - - ## Additional HTTP headers - # http_headers = {"X-Special-Header" = "Special-Value"} - - ## HTTP Content-Encoding for write request body, can be set to "gzip" to - ## compress body or "identity" to apply no encoding. - # content_encoding = "identity" - - ## When true, Telegraf will output unsigned integers as unsigned values, - ## i.e.: "42u". You will need a version of InfluxDB supporting unsigned - ## integer values. Enabling this option will result in field type errors if - ## existing data has been written. - # influx_uint_support = false - - # Read metrics from MQTT topic(s) - [[inputs.mqtt_consumer]] - ## MQTT broker URLs to be used. The format should be scheme://host:port, - ## schema can be tcp, ssl, or ws. - servers = ["tcp://mosquitto:1883"] - - ## Topics that will be subscribed to. - topics = [ - "telegraf/host01/cpu", - "telegraf/+/mem", - "sensors/#", - ] - - ## The message topic will be stored in a tag specified by this value. If set - ## to the empty string no topic tag will be created. - # topic_tag = "topic" - - ## QoS policy for messages - ## 0 = at most once - ## 1 = at least once - ## 2 = exactly once - ## - ## When using a QoS of 1 or 2, you should enable persistent_session to allow - ## resuming unacknowledged messages. - # qos = 0 - - ## Connection timeout for initial connection in seconds - # connection_timeout = "30s" - - ## Maximum messages to read from the broker that have not been written by an - ## output. For best throughput set based on the number of metrics within - ## each message and the size of the output's metric_batch_size. - ## - ## For example, if each message from the queue contains 10 metrics and the - ## output metric_batch_size is 1000, setting this to 100 will ensure that a - ## full batch is collected and the write is triggered immediately without - ## waiting until the next flush_interval. - max_undelivered_messages = 1000 - - ## Persistent session disables clearing of the client session on connection. - ## In order for this option to work you must also set client_id to identity - ## the client. To receive messages that arrived while the client is offline, - ## also set the qos option to 1 or 2 and don't forget to also set the QoS when - ## publishing. - # persistent_session = false - - ## If unset, a random client ID will be generated. - # client_id = "" - - ## Username and password to connect MQTT server. - # username = "telegraf" - # password = "metricsmetricsmetricsmetrics" - - ## Optional TLS Config - # tls_ca = "/etc/telegraf/ca.pem" - # tls_cert = "/etc/telegraf/cert.pem" - # tls_key = "/etc/telegraf/key.pem" - ## Use TLS but skip chain & host verification - # insecure_skip_verify = false - - ## Data format to consume. - ## Each data format has its own unique set of configuration options, read - ## more about them here: - ## https://github.com/influxdata/telegraf/blob/master/docs/DATA_FORMATS_INPUT.md - - #data_format = "influx" - data_format = "json" - #tag_keys = [ - # "temperature", - # "humidity" - #] \ No newline at end of file diff --git a/.templates/timescaledb/build.py b/.templates/timescaledb/build.py deleted file mode 100755 index abb3f75da..000000000 --- a/.templates/timescaledb/build.py +++ /dev/null @@ -1,116 +0,0 @@ -#!/usr/bin/env python3 - -issues = {} # Returned issues dict -buildHooks = {} # Options, and others hooks -haltOnErrors = True - -# Main wrapper function. Required to make local vars work correctly -def main(): - import os - import time - import sys - - from deps.consts import servicesDirectory, templatesDirectory - from deps.common_functions import getExternalPorts, getInternalPorts, checkPortConflicts - - global dockerComposeServicesYaml # The loaded memory YAML of all checked services - global toRun # Switch for which function to run when executed - global buildHooks # Where to place the options menu result - global currentServiceName # Name of the current service - global issues # Returned issues dict - global haltOnErrors # Turn on to allow erroring - global hideHelpText # Showing and hiding the help controls text - global serviceService - - serviceService = servicesDirectory + currentServiceName - serviceTemplate = templatesDirectory + currentServiceName - - try: # If not already set, then set it. - hideHelpText = hideHelpText - except: - hideHelpText = False - - # runtime vars - portConflicts = [] - - # This lets the menu know whether to put " >> Options " or not - # This function is REQUIRED. - def checkForOptionsHook(): - try: - buildHooks["options"] = callable(runOptionsMenu) - except: - buildHooks["options"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPreBuildHook(): - try: - buildHooks["preBuildHook"] = callable(preBuild) - except: - buildHooks["preBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPostBuildHook(): - try: - buildHooks["postBuildHook"] = callable(postBuild) - except: - buildHooks["postBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForRunChecksHook(): - try: - buildHooks["runChecksHook"] = callable(runChecks) - except: - buildHooks["runChecksHook"] = False - return buildHooks - return buildHooks - - # This service will not check anything unless this is set - # This function is optional, and will run each time the menu is rendered - def runChecks(): - checkForIssues() - return [] - - # This function is optional, and will run after the docker-compose.yml file is written to disk. - def postBuild(): - return True - - # This function is optional, and will run just before the build docker-compose.yml code. - def preBuild(): - return True - - # ##################################### - # Supporting functions below - # ##################################### - - def checkForIssues(): - for (index, serviceName) in enumerate(dockerComposeServicesYaml): - if not currentServiceName == serviceName: # Skip self - currentServicePorts = getExternalPorts(currentServiceName, dockerComposeServicesYaml) - portConflicts = checkPortConflicts(serviceName, currentServicePorts, dockerComposeServicesYaml) - if (len(portConflicts) > 0): - issues["portConflicts"] = portConflicts - - # ##################################### - # End Supporting functions - # ##################################### - - if haltOnErrors: - eval(toRun)() - else: - try: - eval(toRun)() - except: - pass - -# This check isn't required, but placed here for debugging purposes -global currentServiceName # Name of the current service -if currentServiceName == 'timescaledb': - main() -else: - print("Error. '{}' Tried to run 'timescaledb' config".format(currentServiceName)) diff --git a/.templates/transmission/build.py b/.templates/transmission/build.py deleted file mode 100755 index 74c6d63a6..000000000 --- a/.templates/transmission/build.py +++ /dev/null @@ -1,337 +0,0 @@ -#!/usr/bin/env python3 - -issues = {} # Returned issues dict -buildHooks = {} # Options, and others hooks -haltOnErrors = True - -# Main wrapper function. Required to make local vars work correctly -def main(): - import os - import time - from blessed import Terminal - from deps.chars import specialChars, commonTopBorder, commonBottomBorder, commonEmptyLine, padText - from deps.consts import servicesDirectory, templatesDirectory, volumesDirectory - from deps.common_functions import getExternalPorts, getInternalPorts, checkPortConflicts, enterPortNumberWithWhiptail - - global dockerComposeServicesYaml # The loaded memory YAML of all checked services - global toRun # Switch for which function to run when executed - global buildHooks # Where to place the options menu result - global currentServiceName # Name of the current service - global issues # Returned issues dict - global haltOnErrors # Turn on to allow erroring - - # runtime vars - portConflicts = [] - serviceVolume = volumesDirectory + currentServiceName - serviceService = servicesDirectory + currentServiceName - serviceTemplate = templatesDirectory + currentServiceName - - try: # If not already set, then set it. - hideHelpText = hideHelpText - except: - hideHelpText = False - - documentationHint = 'https://sensorsiot.github.io/IOTstack/' - - # This lets the menu know whether to put " >> Options " or not - # This function is REQUIRED. - def checkForOptionsHook(): - try: - buildHooks["options"] = callable(runOptionsMenu) - except: - buildHooks["options"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPreBuildHook(): - try: - buildHooks["preBuildHook"] = callable(preBuild) - except: - buildHooks["preBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPostBuildHook(): - try: - buildHooks["postBuildHook"] = callable(postBuild) - except: - buildHooks["postBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForRunChecksHook(): - try: - buildHooks["runChecksHook"] = callable(runChecks) - except: - buildHooks["runChecksHook"] = False - return buildHooks - return buildHooks - - # This service will not check anything unless this is set - # This function is optional, and will run each time the menu is rendered - def runChecks(): - checkForIssues() - return [] - - # This function is optional, and will run after the docker-compose.yml file is written to disk. - def postBuild(): - if not os.path.exists(serviceVolume): - try: - os.makedirs(serviceVolume, exist_ok=True) - print("Created", serviceVolume, "for", currentServiceName) - except Exception as err: - print("Error creating directory", currentServiceName) - print(err) - if not os.path.exists(serviceVolume + "/downloads"): - try: - os.mkdir(serviceVolume + "/downloads") - print("Created", serviceVolume + "/downloads", "for", currentServiceName) - except Exception as err: - print("Error creating downloads directory", currentServiceName) - print(err) - - if not os.path.exists(serviceVolume + "/watch"): - try: - os.makedirs(serviceVolume + "/watch", exist_ok=True) - print("Created", serviceVolume + "/watch", "for", currentServiceName) - except Exception as err: - print("Error creating watch directory", currentServiceName) - print(err) - - if not os.path.exists(serviceVolume + "/config"): - try: - os.makedirs(serviceVolume + "/config", exist_ok=True) - print("Created", serviceVolume + "/config", "for", currentServiceName) - except Exception as err: - print("Error creating config directory", currentServiceName) - print(err) - - return True - - # This function is optional, and will run just before the build docker-compose.yml code. - def preBuild(): - return True - - # ##################################### - # Supporting functions below - # ##################################### - - def checkForIssues(): - for (index, serviceName) in enumerate(dockerComposeServicesYaml): - if not currentServiceName == serviceName: # Skip self - currentServicePorts = getExternalPorts(currentServiceName, dockerComposeServicesYaml) - portConflicts = checkPortConflicts(serviceName, currentServicePorts, dockerComposeServicesYaml) - if (len(portConflicts) > 0): - issues["portConflicts"] = portConflicts - - ############################ - # Menu Logic - ############################ - - global currentMenuItemIndex - global selectionInProgress - global menuNavigateDirection - global needsRender - - selectionInProgress = True - currentMenuItemIndex = 0 - menuNavigateDirection = 0 - needsRender = 1 - term = Terminal() - hotzoneLocation = [((term.height // 16) + 6), 0] - - def goBack(): - global selectionInProgress - global needsRender - selectionInProgress = False - needsRender = 1 - return True - - def enterPortNumberExec(): - # global term - global needsRender - global dockerComposeServicesYaml - externalPort = getExternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - internalPort = getInternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - newPortNumber = enterPortNumberWithWhiptail(term, dockerComposeServicesYaml, currentServiceName, hotzoneLocation, externalPort) - - if newPortNumber > 0: - dockerComposeServicesYaml[currentServiceName]["ports"][0] = "{newExtPort}:{oldIntPort}".format( - newExtPort = newPortNumber, - oldIntPort = internalPort - ) - createMenu() - needsRender = 1 - - def onResize(sig, action): - global transmissionBuildOptions - global currentMenuItemIndex - mainRender(1, transmissionBuildOptions, currentMenuItemIndex) - - transmissionBuildOptions = [] - - def createMenu(): - global transmissionBuildOptions - try: - transmissionBuildOptions = [] - portNumber = getExternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - transmissionBuildOptions.append([ - "Change external WUI Port Number from: {port}".format(port=portNumber), - enterPortNumberExec - ]) - except: # Error getting port - pass - transmissionBuildOptions.append(["Go back", goBack]) - - def runOptionsMenu(): - createMenu() - menuEntryPoint() - return True - - def renderHotZone(term, menu, selection, hotzoneLocation): - lineLengthAtTextStart = 71 - print(term.move(hotzoneLocation[0], hotzoneLocation[1])) - for (index, menuItem) in enumerate(menu): - toPrint = "" - if index == selection: - toPrint += ('{bv} -> {t.blue_on_green} {title} {t.normal} <-'.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) - else: - toPrint += ('{bv} {t.normal} {title} '.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) - - for i in range(lineLengthAtTextStart - len(menuItem[0])): - toPrint += " " - - toPrint += "{bv}".format(bv=specialChars[renderMode]["borderVertical"]) - - toPrint = term.center(toPrint) - - print(toPrint) - - def mainRender(needsRender, menu, selection): - term = Terminal() - - if needsRender == 1: - print(term.clear()) - print(term.move_y(term.height // 16)) - print(term.black_on_cornsilk4(term.center('IOTstack Transmission Options'))) - print("") - print(term.center(commonTopBorder(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Select Option to configure {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - - if needsRender >= 1: - renderHotZone(term, menu, selection, hotzoneLocation) - - if needsRender == 1: - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - if not hideHelpText: - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Enter] to run command or save input {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Escape] to go back to build stack menu {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - if len(documentationHint) > 1: - if len(documentationHint) > 56: - documentationAndPadding = padText(documentationHint, 71) - print(term.center("{bv} Documentation: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} {dap} {bv}".format(bv=specialChars[renderMode]["borderVertical"], dap=documentationAndPadding))) - else: - documentationAndPadding = padText(documentationHint, 56) - print(term.center("{bv} Documentation: {dap} {bv}".format(bv=specialChars[renderMode]["borderVertical"], dap=documentationAndPadding))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonBottomBorder(renderMode))) - - def runSelection(selection): - import types - global transmissionBuildOptions - if len(transmissionBuildOptions[selection]) > 1 and isinstance(transmissionBuildOptions[selection][1], types.FunctionType): - transmissionBuildOptions[selection][1]() - else: - print(term.green_reverse('IOTstack Error: No function assigned to menu item: "{}"'.format(nodeRedBuildOptions[selection][0]))) - - def isMenuItemSelectable(menu, index): - if len(menu) > index: - if len(menu[index]) > 2: - if menu[index][2]["skip"] == True: - return False - return True - - def menuEntryPoint(): - # These need to be reglobalised due to eval() - global currentMenuItemIndex - global selectionInProgress - global menuNavigateDirection - global needsRender - global hideHelpText - global transmissionBuildOptions - term = Terminal() - with term.fullscreen(): - menuNavigateDirection = 0 - mainRender(needsRender, transmissionBuildOptions, currentMenuItemIndex) - selectionInProgress = True - with term.cbreak(): - while selectionInProgress: - menuNavigateDirection = 0 - - if needsRender: # Only rerender when changed to prevent flickering - mainRender(needsRender, transmissionBuildOptions, currentMenuItemIndex) - needsRender = 0 - - key = term.inkey(esc_delay=0.05) - if key.is_sequence: - if key.name == 'KEY_TAB': - menuNavigateDirection += 1 - if key.name == 'KEY_DOWN': - menuNavigateDirection += 1 - if key.name == 'KEY_UP': - menuNavigateDirection -= 1 - if key.name == 'KEY_LEFT': - goBack() - if key.name == 'KEY_ENTER': - runSelection(currentMenuItemIndex) - if key.name == 'KEY_ESCAPE': - return True - elif key: - if key == 'h': # H pressed - if hideHelpText: - hideHelpText = False - else: - hideHelpText = True - mainRender(1, transmissionBuildOptions, currentMenuItemIndex) - - if menuNavigateDirection != 0: # If a direction was pressed, find next selectable item - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(transmissionBuildOptions) - needsRender = 2 - - while not isMenuItemSelectable(transmissionBuildOptions, currentMenuItemIndex): - currentMenuItemIndex += menuNavigateDirection - currentMenuItemIndex = currentMenuItemIndex % len(transmissionBuildOptions) - return True - - #################### - # End menu section - #################### - - if haltOnErrors: - eval(toRun)() - else: - try: - eval(toRun)() - except: - pass - -# This check isn't required, but placed here for debugging purposes -global currentServiceName # Name of the current service -if currentServiceName == 'transmission': - main() -else: - print("Error. '{}' Tried to run 'transmission' config".format(currentServiceName)) diff --git a/.templates/webthings_gateway/build.py b/.templates/webthings_gateway/build.py deleted file mode 100755 index 0be4ae8c8..000000000 --- a/.templates/webthings_gateway/build.py +++ /dev/null @@ -1,138 +0,0 @@ -#!/usr/bin/env python3 - -issues = {} # Returned issues dict -buildHooks = {} # Options, and others hooks -haltOnErrors = True - -# Main wrapper function. Required to make local vars work correctly -def main(): - import os - import time - import shutil - import sys - - from deps.consts import servicesDirectory, templatesDirectory, volumesDirectory - from deps.common_functions import getExternalPorts, getInternalPorts, checkPortConflicts - - global dockerComposeServicesYaml # The loaded memory YAML of all checked services - global toRun # Switch for which function to run when executed - global buildHooks # Where to place the options menu result - global currentServiceName # Name of the current service - global issues # Returned issues dict - global haltOnErrors # Turn on to allow erroring - global hideHelpText # Showing and hiding the help controls text - global serviceService - - serviceVolume = volumesDirectory + currentServiceName - serviceService = servicesDirectory + currentServiceName - serviceTemplate = templatesDirectory + currentServiceName - - try: # If not already set, then set it. - hideHelpText = hideHelpText - except: - hideHelpText = False - - # runtime vars - portConflicts = [] - - # This lets the menu know whether to put " >> Options " or not - # This function is REQUIRED. - def checkForOptionsHook(): - try: - buildHooks["options"] = callable(runOptionsMenu) - except: - buildHooks["options"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPreBuildHook(): - try: - buildHooks["preBuildHook"] = callable(preBuild) - except: - buildHooks["preBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPostBuildHook(): - try: - buildHooks["postBuildHook"] = callable(postBuild) - except: - buildHooks["postBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForRunChecksHook(): - try: - buildHooks["runChecksHook"] = callable(runChecks) - except: - buildHooks["runChecksHook"] = False - return buildHooks - return buildHooks - - # This service will not check anything unless this is set - # This function is optional, and will run each time the menu is rendered - def runChecks(): - checkForIssues() - return [] - - # This function is optional, and will run after the docker-compose.yml file is written to disk. - def postBuild(): - return True - - # This function is optional, and will run just before the build docker-compose.yml code. - def preBuild(): - # Setup service directory - if not os.path.exists(serviceVolume): - os.makedirs(serviceVolume, exist_ok=True) - os.makedirs(serviceVolume + '/share', exist_ok=True) - os.makedirs(serviceVolume + '/share/config', exist_ok=True) - - # Files copy - shutil.copy(r'%s/local.json' % serviceTemplate, r'%s/share/config/local.json' % serviceVolume) - return True - - # ##################################### - # Supporting functions below - # ##################################### - - - def checkForIssues(): - envFileIssues = checkEnvFiles() - if (len(envFileIssues) > 0): - issues["envFileIssues"] = envFileIssues - - for (index, serviceName) in enumerate(dockerComposeServicesYaml): - if not currentServiceName == serviceName: # Skip self - currentServicePorts = getExternalPorts(currentServiceName, dockerComposeServicesYaml) - portConflicts = checkPortConflicts(serviceName, currentServicePorts, dockerComposeServicesYaml) - if (len(portConflicts) > 0): - issues["portConflicts"] = portConflicts - - def checkEnvFiles(): - envFileIssues = [] - if not os.path.exists(serviceTemplate + '/local.json'): - envFileIssues.append(serviceTemplate + '/local.json does not exist') - return envFileIssues - - - # ##################################### - # End Supporting functions - # ##################################### - - if haltOnErrors: - eval(toRun)() - else: - try: - eval(toRun)() - except: - pass - -# This check isn't required, but placed here for debugging purposes -global currentServiceName # Name of the current service -if currentServiceName == 'webthingsio_gateway': - main() -else: - print("Error. '{}' Tried to run 'webthingsio_gateway' config".format(currentServiceName)) diff --git a/.templates/webthings_gateway/service.yml b/.templates/webthings_gateway/service.yml deleted file mode 100644 index 5da37ae35..000000000 --- a/.templates/webthings_gateway/service.yml +++ /dev/null @@ -1,10 +0,0 @@ -webthingsio_gateway: - image: webthingsio/gateway:latest - container_name: webthingsio_gateway - network_mode: host - #ports: - #- "4060:4060" - #- "4061:4061" - volumes: - - ./volumes/webthingsio_gateway/share:/home/node/.mozilla-iot - diff --git a/.templates/wireguard/build.py b/.templates/wireguard/build.py deleted file mode 100755 index 76287420d..000000000 --- a/.templates/wireguard/build.py +++ /dev/null @@ -1,85 +0,0 @@ -#!/usr/bin/env python3 - -issues = {} # Returned issues dict -buildHooks = {} # Options, and others hooks -haltOnErrors = True - -# Main wrapper function. Required to make local vars work correctly -def main(): - import subprocess - global dockerComposeServicesYaml # The loaded memory YAML of all checked services - global toRun # Switch for which function to run when executed - global buildHooks # Where to place the options menu result - global currentServiceName # Name of the current service - global issues # Returned issues dict - global haltOnErrors # Turn on to allow erroring - - from deps.consts import servicesDirectory, templatesDirectory, volumesDirectory, servicesFileName - - # runtime vars - serviceVolume = volumesDirectory + currentServiceName # Unused in example - serviceService = servicesDirectory + currentServiceName # Unused in example - serviceTemplate = templatesDirectory + currentServiceName - - # This lets the menu know whether to put " >> Options " or not - # This function is REQUIRED. - def checkForOptionsHook(): - try: - buildHooks["options"] = callable(runOptionsMenu) - except: - buildHooks["options"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPreBuildHook(): - try: - buildHooks["preBuildHook"] = callable(preBuild) - except: - buildHooks["preBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPostBuildHook(): - try: - buildHooks["postBuildHook"] = callable(postBuild) - except: - buildHooks["postBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForRunChecksHook(): - try: - buildHooks["runChecksHook"] = callable(runChecks) - except: - buildHooks["runChecksHook"] = False - return buildHooks - return buildHooks - - # This function is optional, and will run just before the build docker-compose.yml code. - def preBuild(): - execComm = "bash {currentServiceTemplate}/build.sh".format(currentServiceTemplate=serviceTemplate) - print("[Wireguard]: ", execComm) - subprocess.call(execComm, shell=True) - return True - - # ##################################### - # Supporting functions below - # ##################################### - - if haltOnErrors: - eval(toRun)() - else: - try: - eval(toRun)() - except: - pass - -# This check isn't required, but placed here for debugging purposes -global currentServiceName # Name of the current service -if currentServiceName == 'wireguard': - main() -else: - print("Error. '{}' Tried to run 'wireguard' config".format(currentServiceName)) diff --git a/.templates/wireguard/build.sh b/.templates/wireguard/build.sh deleted file mode 100755 index 76d85a28c..000000000 --- a/.templates/wireguard/build.sh +++ /dev/null @@ -1,13 +0,0 @@ -#1/bin/bash - -WG_CONF_TEMPLATE_PATH=./.templates/wireguard/wg0.conf -WG_CONF_DEST_PATH=./services/wireguard/config - -if [[ ! -f $WG_CONF_TEMPLATE_PATH ]]; then - echo "[Wireguard] Warning: $WG_CONF_TEMPLATE_PATH does not exist." -else - if [[ ! -d $WG_CONF_DEST_PATH ]]; then - mkdir -p $WG_CONF_DEST_PATH - cp -r $WG_CONF_TEMPLATE_PATH $WG_CONF_DEST_PATH - fi; -fi diff --git a/.templates/wireguard/service.yml b/.templates/wireguard/service.yml deleted file mode 100644 index 3e1d13397..000000000 --- a/.templates/wireguard/service.yml +++ /dev/null @@ -1,23 +0,0 @@ -wireguard: - image: linuxserver/wireguard - container_name: wireguard - cap_add: - - NET_ADMIN - - SYS_MODULE - environment: - - PUID=1000 - - PGID=1000 - - TZ=Europe/Berlin - - SERVERURL=.duckdns.org - - SERVERPORT=51820 - - PEERS=1 - - PEERDNS=auto - - INTERNAL_SUBNET=100.64.0.0/24 - volumes: - - ./services/wireguard/config:/config - - /lib/modules:/lib/modules - ports: - - "51820:51820/udp" - sysctls: - - net.ipv4.conf.all.src_valid_mark=1 - restart: unless-stopped diff --git a/.templates/zigbee2mqtt/service.yml b/.templates/zigbee2mqtt/service.yml deleted file mode 100644 index 829312310..000000000 --- a/.templates/zigbee2mqtt/service.yml +++ /dev/null @@ -1,16 +0,0 @@ -zigbee2mqtt: - container_name: zigbee2mqtt - build: ./.templates/zigbee2mqtt/. - environment: - - TZ=Etc/UTC - ports: - - "8080:8080" - volumes: - - ./volumes/zigbee2mqtt/data:/app/data - devices: - - /dev/ttyAMA0:/dev/ttyACM0 # should work even if no adapter - #- /dev/ttyACM0:/dev/ttyACM0 # should work if CC2531 connected - #- /dev/ttyUSB0:/dev/ttyACM0 # Electrolama zig-a-zig-ah! (zzh!) maybe other as well - restart: unless-stopped - networks: - - iotstack_nw diff --git a/.templates/zigbee2mqtt_assistant/build.py b/.templates/zigbee2mqtt_assistant/build.py deleted file mode 100755 index b05ddf7e1..000000000 --- a/.templates/zigbee2mqtt_assistant/build.py +++ /dev/null @@ -1,116 +0,0 @@ -#!/usr/bin/env python3 - -issues = {} # Returned issues dict -buildHooks = {} # Options, and others hooks -haltOnErrors = True - -# Main wrapper function. Required to make local vars work correctly -def main(): - import os - import time - import sys - - from deps.consts import servicesDirectory, templatesDirectory - from deps.common_functions import getExternalPorts, getInternalPorts, checkPortConflicts - - global dockerComposeServicesYaml # The loaded memory YAML of all checked services - global toRun # Switch for which function to run when executed - global buildHooks # Where to place the options menu result - global currentServiceName # Name of the current service - global issues # Returned issues dict - global haltOnErrors # Turn on to allow erroring - global hideHelpText # Showing and hiding the help controls text - global serviceService - - serviceService = servicesDirectory + currentServiceName - serviceTemplate = templatesDirectory + currentServiceName - - try: # If not already set, then set it. - hideHelpText = hideHelpText - except: - hideHelpText = False - - # runtime vars - portConflicts = [] - - # This lets the menu know whether to put " >> Options " or not - # This function is REQUIRED. - def checkForOptionsHook(): - try: - buildHooks["options"] = callable(runOptionsMenu) - except: - buildHooks["options"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPreBuildHook(): - try: - buildHooks["preBuildHook"] = callable(preBuild) - except: - buildHooks["preBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForPostBuildHook(): - try: - buildHooks["postBuildHook"] = callable(postBuild) - except: - buildHooks["postBuildHook"] = False - return buildHooks - return buildHooks - - # This function is REQUIRED. - def checkForRunChecksHook(): - try: - buildHooks["runChecksHook"] = callable(runChecks) - except: - buildHooks["runChecksHook"] = False - return buildHooks - return buildHooks - - # This service will not check anything unless this is set - # This function is optional, and will run each time the menu is rendered - def runChecks(): - checkForIssues() - return [] - - # This function is optional, and will run after the docker-compose.yml file is written to disk. - def postBuild(): - return True - - # This function is optional, and will run just before the build docker-compose.yml code. - def preBuild(): - return True - - # ##################################### - # Supporting functions below - # ##################################### - - def checkForIssues(): - for (index, serviceName) in enumerate(dockerComposeServicesYaml): - if not currentServiceName == serviceName: # Skip self - currentServicePorts = getExternalPorts(currentServiceName, dockerComposeServicesYaml) - portConflicts = checkPortConflicts(serviceName, currentServicePorts, dockerComposeServicesYaml) - if (len(portConflicts) > 0): - issues["portConflicts"] = portConflicts - - # ##################################### - # End Supporting functions - # ##################################### - - if haltOnErrors: - eval(toRun)() - else: - try: - eval(toRun)() - except: - pass - -# This check isn't required, but placed here for debugging purposes -global currentServiceName # Name of the current service -if currentServiceName == 'zigbee2mqtt_assistant': - main() -else: - print("Error. '{}' Tried to run 'zigbee2mqtt_assistant' config".format(currentServiceName)) diff --git a/.templates/zigbee2mqtt_assistant/service.yml b/.templates/zigbee2mqtt_assistant/service.yml deleted file mode 100755 index a2b006700..000000000 --- a/.templates/zigbee2mqtt_assistant/service.yml +++ /dev/null @@ -1,13 +0,0 @@ -zigbee2mqtt_assistant: - container_name: zigbee2mqtt_assistant - image: carldebilly/zigbee2mqttassistant - restart: unless-stopped - ports: - - "8880:80" - environment: - - VIRTUAL_HOST=~^zigbee2mqtt_assistant\..*\.xip\.io - - Z2MA_SETTINGS__MQTTSERVER=mosquitto - - VIRTUAL_PORT=8880 - networks: - - iotstack_nw - diff --git a/.templates/zigbee2mqtt_assistant/zigbee2mqtt_assistant.env b/.templates/zigbee2mqtt_assistant/zigbee2mqtt_assistant.env deleted file mode 100755 index e7145a1e0..000000000 --- a/.templates/zigbee2mqtt_assistant/zigbee2mqtt_assistant.env +++ /dev/null @@ -1,4 +0,0 @@ -#TZ=Etc/UTC -Z2MA_SETTINGS__MQTTSERVER=mosquitto -#Z2MA_SETTINGS__MQTTUSERNAME=MQTTUSER -#Z2MA_SETTINGS__MQTTPASSWORD=MQTTPASS diff --git a/README.md b/README.md index 059fc7c99..9cad026ea 100644 --- a/README.md +++ b/README.md @@ -22,3 +22,8 @@ Please use the [issues](https://github.com/SensorsIot/IOTstack/issues) tab to re We have a Discord server setup for discussions: [IOTstack Discord channel](https://discord.gg/ZpKHnks) if you want to comment on features, suggest new container types, or ask the IOTstack community for help. If you use some of the tools in the project please consider donating or contributing on their projects. It doesn't have to be monetary. Reporting bugs and [creating Pull Requests](https://gist.github.com/Paraphraser/818bf54faf5d3b3ed08d16281f32297d) helps improve the projects for everyone. + +### Quick Install on fresh system +``` +curl -fsSL https://raw.githubusercontent.com/SensorsIot/IOTstack/master/install.sh | bash +``` diff --git a/docs/Containers/Portainer-ce.md b/docs/Containers/Portainer-ce.md index 1867acbe1..3ee4484bd 100644 --- a/docs/Containers/Portainer-ce.md +++ b/docs/Containers/Portainer-ce.md @@ -1,11 +1,11 @@ # Portainer CE -## References +##
References - [Docker](https://hub.docker.com/r/portainer/portainer-ce/) - [Website](https://www.portainer.io/portainer-ce/) -## Definition +## Definition - "#yourip" means any of the following: @@ -13,7 +13,7 @@ - the multicast domain name of your Raspberry Pi (eg `iot-hub.local`) - the domain name of your Raspberry Pi (eg `iot-hub.mydomain.com`) -## About *Portainer CE* +## About *Portainer CE* *Portainer CE* (Community Edition) is an application for managing Docker. It is a successor to *Portainer*. According to [the *Portainer CE* documentation](https://www.portainer.io/2020/08/portainer-ce-2-0-what-to-expect/) @@ -21,13 +21,7 @@ From that it should be clear that *Portainer* is deprecated and that *Portainer CE* is the way forward. -## *Portainer CE* coexistence with *Portainer* - -IOTstack has been set up so that *Portainer CE* and *Portainer* can coexist. This is intended as a short-term migration aid rather than a long-term proposition. - -If you are a first-time user of IOTstack, you should choose *Portainer CE* and forget about *Portainer*. - -## Installing *Portainer CE* +## Installing *Portainer CE* Run the menu: @@ -46,102 +40,18 @@ Ignore any message like this: > WARNING: Found orphan containers (portainer) for this project … -### Migration note - -*Portainer CE* and *Portainer* use different locations for their persistent data: - -Edition | Persistent Data Directory | Reference --------------|---------------------------------|:---------: -Portainer | ~/IOTstack/volumes/portainer | [A] -Portainer CE | ~/IOTstack/volumes/portainer-ce | [B] - -If you have been running *Portainer* but have **never** run *Portainer CE* then: - -* [A] will exist, but -* [B] will not exist. - -Whenever "Portainer-ce" is enabled in `menu.sh`, a check is made for the **presence** of [A] combined with the **absence** [B]. If and only if that situation exists, [B] is initialised as a copy of [A]. - -This one-time copy is intended to preserve your *Portainer* settings and admin user password for use in *Portainer CE*. Thereafter, any settings you change in *Portainer CE* will not be reflected in *Portainer*, nor vice versa. +## First run of *Portainer CE* -## Port Number = 9002 +In your web browser navigate to `#yourip:9000/`: -Both *Portainer CE* and *Portainer* are usually configured to listen to port 9000 but, in the IOTstack implementation: +- the first screen will suggest a username of "admin" and ask for a password. Supply those credentials and click "Create User". +- the second screen will ask you to select a connection method. For IOTstack, "Docker (Manage the local Docker environment)" is usually appropriate so click that and then click "Connect". -* *Portainer CE* uses port 9002; and -* *Portainer* uses port 9000. - -> You can always change the port numbers in your `docker-compose.yml`. - -## First run of *Portainer CE* - -In your web browser navigate to `#yourip:9002/`. - -* If you are migrating from *Portainer*: - - - review the [Migration note](#MigrationNote) which explains why your *Portainer* credentials will likely apply to *Portainer CE*, and then - - supply your *Portainer* credentials. - -* If you are **not** migrating from *Portainer*: - - - the first screen will suggest a username of "admin" and ask for a password. Supply those credentials and click "Create User". - - the second screen will ask you to select a connection method. For IOTstack, "Docker (Manage the local Docker environment)" is usually appropriate so click that and then click "Connect". - -From there, you can click on the "Local" group and take a look around. One of the things *Portainer CE* can help you do is find unused containers but beware of reading too much into this because, sometimes, an "unused" container is actually the base for another container (eg Node-Red). +From there, you can click on the "Local" group and take a look around. One of the things *Portainer CE* can help you do is find unused containers but beware of reading too much into this because, sometimes, an "unused" container is actually the base for another container (eg Node-RED). There are 'Quick actions' to view logs and other stats. This can all be done from terminal commands but *Portainer CE* makes it easier. -## Ceasing use of Portainer - -As soon as you are happy that *Portainer CE* meets your needs, you can dispense with *Portainer*. IOTstack only has limited support for getting rid of unwanted services so you should do the following. - -1. Stop Portainer from running and remove its image: - - ``` - $ cd ~/IOTstack - $ docker-compose stop portainer - $ docker-compose rm -f portainer - $ docker rmi portainer/portainer - ``` - -2. Either: - - - run `menu.sh` - - choose "Build Stack" - - de-select "portainer", and - - follow through to the end choosing "Do not overwrite" for existing services, - - or: - - - edit `docker-compose.yml` and remove these lines: - - ``` - portainer: - container_name: portainer - image: portainer/portainer - restart: unless-stopped - ports: - - "9000:9000" - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - ./volumes/portainer/data:/data - ``` - - - edit `services/selection.txt` and remove this line: - - ``` - portainer - ``` - -3. Tidy-up: - - ``` - $ cd ~/IOTstack - $ rm -rf ./services/portainer - $ sudo rm -rf ./volumes/portainer - ``` - -## Setting the Public IP address for your end-point +## Setting the Public IP address for your end-point If you click on a "Published Port" in the "Containers" list, your browser may return an error saying something like "can't connect to server" associated with an IP address of "0.0.0.0". @@ -169,7 +79,7 @@ Keep in mind that clicking on a "Published Port" does not guarantee that your br > All things considered, you will get more consistent behaviour if you simply bookmark the URLs you want to use for your IOTstack services. -## If you forget your password +## If you forget your password If you forget the password you created for *Portainer CE*, you can recover by doing the following: @@ -180,4 +90,7 @@ $ sudo rm -r ./volumes/portainer-ce $ docker-compose start portainer-ce ``` -Then use your browser to navigate to `#yourip:9002/` and follow the steps in [if you are **not** migrating from *Portainer*](#NewAccount). +Then, follow the steps in: + +1. [First run of *Portainer CE*](#firstRun); and +2. [Setting the Public IP address for your end-point](#setPublicIP). diff --git a/docs/Containers/Portainer.md b/docs/Containers/Portainer.md deleted file mode 100644 index 14afb0f19..000000000 --- a/docs/Containers/Portainer.md +++ /dev/null @@ -1,26 +0,0 @@ -# Portainer -## References -- [Docker](https://hub.docker.com/r/portainer/portainer/) -- [Website](https://www.portainer.io/) - -## Portainer restart by itself - -There is an issue with the armhf Portainer image where it randomly restarts. This does not affect its operation. The bug has been reported. - -## About - -Portainer is a great application for managing Docker. In your web browser navigate to `#yourip:9000`. You will be asked to choose a password. In the next window select 'Local' and connect, it shouldn't ask you this again. From here you can play around, click local, and take a look around. This can help you find unused images/containers. On the Containers section, there are 'Quick actions' to view logs and other stats. Note: This can all be done from the CLI but portainer just makes it much much easier. - -## Setup Public IP - -When you first run Portainer and navigate to the Containers list you will see that there is a clickable link to the ports however this will direct you to `0.0.0.0:port`. This is because Portainer doesn't know your IP address. This can be set in the endpoint - -![image](https://user-images.githubusercontent.com/46672225/69695462-26a31a80-10e5-11ea-991d-24b7282c8963.png) - -and set the public IP - -![image](https://user-images.githubusercontent.com/46672225/69695485-3c184480-10e5-11ea-85f7-8385ac339d76.png) - -## Forgotten password - -If you have forgotten the password you created for the container, stop the stack remove portainers volume with `sudo rm -r ./volumes/portainer` and start the stack. Your browser may get a little confused when it restarts. Just navigate to "yourip:9000" (may require more than one attempt) and create your new login details. If it doesn't ask you to connect to the 'Local' docker or shows an empty endpoint just logout and log back in and it will give you the option. From now on it should just work fine. \ No newline at end of file diff --git a/docs/Containers/openHAB.md b/docs/Containers/openHAB.md index b289e48bb..a6bef69cf 100644 --- a/docs/Containers/openHAB.md +++ b/docs/Containers/openHAB.md @@ -1,6 +1,43 @@ -# Openhab +# openHAB + ## References -- [Docker](https://hub.docker.com/r/openhab/openhab/) -- [website](https://www.openhab.org/) -openHAB has been added without Amazon Dashbutton binding. Port binding is `8080` for http and `8443` for https. +- [DockerHub](https://hub.docker.com/r/openhab/openhab/) +- [GitHub](https://github.com/openhab/openhab-docker) +- [openHAB website](https://www.openhab.org/) + +openHAB runs in "host mode" so there are no port mappings. The default port bindings on IOTstack are: + +* 4050 - the HTTP port of the web interface (instead of 8080) +* 4051 - the HTTPS port of the web interface (instead of 8443) +* 8101 - the SSH port of the Console (since openHAB 2.0.0) +* 5007 - the LSP port for validating rules (since openHAB 2.2.0) + +If you want to change either of the first two: + +1. Edit the `openhab` fragment in `docker-compose.yml`: + + ``` + - OPENHAB_HTTP_PORT=4050 + - OPENHAB_HTTPS_PORT=4051 + ``` + +2. Recreate the openHAB container: + + ``` + $ cd ~/IOTstack + $ docker-compose up -d openhab + ``` + +There do not appear to be any environment variables to control ports 8101 or 5007 so, if other containers you need to run also depend on those ports, you will have to figure out some way of resolving the conflict. + +Note: + +* The original IOTstack documentation included: + + > openHAB has been added without Amazon Dashbutton binding. + + but it is not clear if this is still the case. + +* [Amazon Dashbuttons have been discontinued](https://www.theverge.com/2019/2/28/18245315/amazon-dash-buttons-discontinued) so this may no longer be relevant. + diff --git a/duck/duck.sh b/duck/duck.sh deleted file mode 100755 index 2405c60d8..000000000 --- a/duck/duck.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash -# Your comma-separated domains list -DOMAINS="YOUR_DOMAINS" -# Your DuckDNS Token -DUCKDNS_TOKEN="YOUR_DUCKDNS_TOKEN" - -# A random delay to avoid every client contacting the duckdns server at the same moment -sleep $((RANDOM % 60)) -# Request duckdns to update your domain name with your public IP address -curl --silent --max-time 10 --output /dev/null "https://www.duckdns.org/update?domains=${DOMAINS}&token=${DUCKDNS_TOKEN}&ip=" diff --git a/install.sh b/install.sh index d61b8f150..3acb76bcd 100755 --- a/install.sh +++ b/install.sh @@ -2,307 +2,343 @@ # Minimum Software Versions REQ_DOCKER_VERSION=18.2.0 -REQ_PYTHON_VERSION=3.6.9 -REQ_PIP_VERSION=3.6.9 -REQ_PYAML_VERSION=0.16.12 -REQ_BLESSED_VERSION=1.17.5 -PYTHON_CMD=python3 +# Required to generate and install a ssh key so menu containers can securely execute commands on host +AUTH_KEYS_FILE=~/.ssh/authorized_keys +CONTAINER_KEYS_FILE=./.internal/.ssh/id_rsa +REBOOT_REQ="false" +HAS_ERROR="false" sys_arch=$(uname -m) while test $# -gt 0 do - case "$1" in - --no-ask) NOASKCONFIRM="true" - ;; - --*) echo "bad option $1" - ;; - esac - shift + case "$1" in + --no-ask) NOASKCONFIRM="true" + ;; + --*) echo "bad option $1" + ;; + esac + shift done echo "IOTstack Installation" +echo "Running as '$(whoami)' in '$(pwd)'" if [ "$EUID" -eq "0" ]; then echo "Please do not run as root" exit fi +if [ -f "./menu.sh" ]; then + echo "'./menu.sh' file detected, will not reclone. Is IOTstack already installed in this directory?" +fi + +echo "Please enter sudo password if prompted to do so." +echo "" + function command_exists() { - command -v "$@" > /dev/null 2>&1 + command -v "$@" > /dev/null 2>&1 } function minimum_version_check() { - # Usage: minimum_version_check required_version current_major current_minor current_build - # Example: minimum_version_check "1.2.3" 1 2 3 - REQ_MIN_VERSION_MAJOR=$(echo "$1"| cut -d' ' -f 2 | cut -d'.' -f 1) - REQ_MIN_VERSION_MINOR=$(echo "$1"| cut -d' ' -f 2 | cut -d'.' -f 2) - REQ_MIN_VERSION_BUILD=$(echo "$1"| cut -d' ' -f 2 | cut -d'.' -f 3) - - CURR_VERSION_MAJOR=$2 - CURR_VERSION_MINOR=$3 - CURR_VERSION_BUILD=$4 - - VERSION_GOOD="Unknown" - - if [ -z "$CURR_VERSION_MAJOR" ]; then - echo "$VERSION_GOOD" - return 1 - fi - - if [ -z "$CURR_VERSION_MINOR" ]; then - echo "$VERSION_GOOD" - return 1 - fi - - if [ -z "$CURR_VERSION_BUILD" ]; then - echo "$VERSION_GOOD" - return 1 - fi - - if [ "${CURR_VERSION_MAJOR}" -ge $REQ_MIN_VERSION_MAJOR ]; then - VERSION_GOOD="true" - echo "$VERSION_GOOD" - return 0 - else - VERSION_GOOD="false" - fi - - if [ "${CURR_VERSION_MAJOR}" -ge $REQ_MIN_VERSION_MAJOR ] && \ - [ "${CURR_VERSION_MINOR}" -ge $REQ_MIN_VERSION_MINOR ]; then - VERSION_GOOD="true" - echo "$VERSION_GOOD" - return 0 - else - VERSION_GOOD="false" - fi - - if [ "${CURR_VERSION_MAJOR}" -ge $REQ_MIN_VERSION_MAJOR ] && \ - [ "${CURR_VERSION_MINOR}" -ge $REQ_MIN_VERSION_MINOR ] && \ - [ "${CURR_VERSION_BUILD}" -ge $REQ_MIN_VERSION_BUILD ]; then - VERSION_GOOD="true" - echo "$VERSION_GOOD" - return 0 - else - VERSION_GOOD="false" - fi - - echo "$VERSION_GOOD" -} + # Usage: minimum_version_check required_version current_major current_minor current_build + # Example: minimum_version_check "1.2.3" 1 2 3 + REQ_MIN_VERSION_MAJOR=$(echo "$1"| cut -d' ' -f 2 | cut -d'.' -f 1) + REQ_MIN_VERSION_MINOR=$(echo "$1"| cut -d' ' -f 2 | cut -d'.' -f 2) + REQ_MIN_VERSION_BUILD=$(echo "$1"| cut -d' ' -f 2 | cut -d'.' -f 3) + + CURR_VERSION_MAJOR=$2 + CURR_VERSION_MINOR=$3 + CURR_VERSION_BUILD=$4 + + VERSION_GOOD="Unknown" + + if [ -z "$CURR_VERSION_MAJOR" ]; then + echo "$VERSION_GOOD" + return 1 + fi + + if [ -z "$CURR_VERSION_MINOR" ]; then + echo "$VERSION_GOOD" + return 1 + fi + + if [ -z "$CURR_VERSION_BUILD" ]; then + echo "$VERSION_GOOD" + return 1 + fi + + if [ "${CURR_VERSION_MAJOR}" -ge $REQ_MIN_VERSION_MAJOR ]; then + VERSION_GOOD="true" + echo "$VERSION_GOOD" + return 0 + else + VERSION_GOOD="false" + fi + + if [ "${CURR_VERSION_MAJOR}" -ge $REQ_MIN_VERSION_MAJOR ] && \ + [ "${CURR_VERSION_MINOR}" -ge $REQ_MIN_VERSION_MINOR ]; then + VERSION_GOOD="true" + echo "$VERSION_GOOD" + return 0 + else + VERSION_GOOD="false" + fi + + if [ "${CURR_VERSION_MAJOR}" -ge $REQ_MIN_VERSION_MAJOR ] && \ + [ "${CURR_VERSION_MINOR}" -ge $REQ_MIN_VERSION_MINOR ] && \ + [ "${CURR_VERSION_BUILD}" -ge $REQ_MIN_VERSION_BUILD ]; then + VERSION_GOOD="true" + echo "$VERSION_GOOD" + return 0 + else + VERSION_GOOD="false" + fi -function user_in_group() -{ - if grep -q $1 /etc/group ; then - if id -nGz "$USER" | grep -qzxF "$1"; then - echo "true" - else - echo "false" - fi - else - echo "notgroup" - fi + echo "$VERSION_GOOD" } -function install_python3_and_deps() { - CURR_PYTHON_VER="${1:-Unknown}" - if [ "$NOASKCONFIRM" == "true" ]; then - echo "Installing Python3" - sudo apt install -y python3-pip python3-dev - if [ $? -eq 0 ]; then - PYTHON_VERSION_GOOD="true" +function user_in_group() { + if grep -q $1 /etc/group ; then + if id -nGz "$USER" | grep -qzxF "$1"; then + echo "true" else - echo "Failed to install Python" >&2 - exit 1 - fi - echo "Installing ruamel.yaml and blessed" - pip3 install -U ruamel.yaml==0.16.12 blessed - if [ $? -eq 0 ]; then - PYAML_VERSION_GOOD="true" - BLESSED_GOOD="true" - else - echo "Failed to install ruamel.yaml and Blessed" >&2 - exit 1 + echo "false" fi else - if (whiptail --title "Python 3 and Dependencies" --yesno "Python 3.6.9 or later (Current = $CURR_PYTHON_VER), ruamel.yaml 0.16.12 or later, blessed and pip3 are required for the main menu and compose-overrides.yml file to merge into the docker-compose.yml file. Install these now?" 20 78); then - sudo apt install -y python3-pip python3-dev - if [ $? -eq 0 ]; then - PYTHON_VERSION_GOOD="true" - else - echo "Failed to install Python" >&2 - exit 1 - fi - pip3 install -U ruamel.yaml==0.16.12 blessed - if [ $? -eq 0 ]; then - PYAML_VERSION_GOOD="true" - BLESSED_GOOD="true" - else - echo "Failed to install ruamel.yaml and Blessed" >&2 - exit 1 - fi - fi + echo "notgroup" fi } function install_docker() { + DOCKERREBOOT="false" if command_exists docker; then - echo "Docker already installed" >&2 + echo "Docker already installed" >&1 else - echo "Install Docker" >&2 + echo "Install Docker:" >&1 + echo "curl -fsSL https://get.docker.com | sh" >&1 curl -fsSL https://get.docker.com | sh - sudo usermod -aG docker $USER + sudo -E usermod -aG docker $USER + DOCKERREBOOT="true" fi if command_exists docker-compose; then - echo "docker-compose already installed" >&2 + echo "docker-compose already installed" >&1 else - echo "Install docker-compose" >&2 - sudo apt install -y docker-compose + echo "Install docker-compose" >&1 + sudo -E apt install -y docker-compose + DOCKERREBOOT="true" fi - echo "" >&2 - echo "You should now restart your system" >&2 + if [[ "$DOCKERREBOOT" == "true" ]]; then + REBOOT_REQ="true" + echo "" >&1 + echo "You should restart your system after IOTstack is installed" >&1 + fi } -function update_docker() { - sudo apt upgrade docker docker-compose -} +function check_container_ssh() { + KEYS_EXIST="false" + if [[ -f "$CONTAINER_KEYS_FILE" && -f "$CONTAINER_KEYS_FILE.pub" ]]; then + KEYS_EXIST="true" + fi -function do_python3_checks() { - PYTHON_VERSION_GOOD="false" - PYAML_VERSION_GOOD="false" - BLESSED_GOOD="false" - - if command_exists $PYTHON_CMD && command_exists pip3; then - PYTHON_VERSION=$($PYTHON_CMD --version) - PYTHON_VERSION_MAJOR=$(echo "$PYTHON_VERSION"| cut -d' ' -f 2 | cut -d'.' -f 1) - PYTHON_VERSION_MINOR=$(echo "$PYTHON_VERSION"| cut -d' ' -f 2 | cut -d'.' -f 2) - PYTHON_VERSION_BUILD=$(echo "$PYTHON_VERSION"| cut -d' ' -f 2 | cut -d'.' -f 3) - - printf "Python Version: '${PYTHON_VERSION:-Unknown}'. " - if [ "$(minimum_version_check $REQ_PYTHON_VERSION $PYTHON_VERSION_MAJOR $PYTHON_VERSION_MINOR $PYTHON_VERSION_BUILD)" == "true" ]; then - PYTHON_VERSION_GOOD="true" - echo "Python is up to date." >&2 - else - echo "Python is outdated." >&2 - install_python3_and_deps "$PYTHON_VERSION_MAJOR.$PYTHON_VERSION_MINOR.$PYTHON_VERSION_BUILD" "$PYAML_VERSION_MAJOR.$PYAML_VERSION_MINOR.$PYAML_VERSION_BUILD" - return 1 - fi - else - install_python3_and_deps - return 1 - fi + echo $KEYS_EXIST } -function do_env_setup() { - echo "Setting up environment:" - if [[ ! "$(user_in_group bluetooth)" == "notgroup" ]] && [[ ! "$(user_in_group bluetooth)" == "true" ]]; then - echo "User is NOT in 'bluetooth' group. Adding:" >&2 - echo "sudo usermod -G bluetooth -a $USER" >&2 - sudo usermod -G "bluetooth" -a $USER - fi - - if [ ! "$(user_in_group docker)" == "true" ]; then - echo "User is NOT in 'docker' group. Adding:" >&2 - echo "sudo usermod -G docker -a $USER" >&2 - sudo usermod -G "docker" -a $USER - fi +function check_host_ssh_keys() { + KEY_EXISTS="false" + grep -f "$CONTAINER_KEYS_FILE.pub" $AUTH_KEYS_FILE + GRES=$? + if [[ $GRES -eq 0 ]]; then + KEY_EXISTS="true" + fi + + echo $KEY_EXISTS } -function do_docker_checks() { - if command_exists docker; then - DOCKER_VERSION_GOOD="false" - DOCKER_VERSION=$(docker version -f "{{.Server.Version}}") - if [ ! -z "$DOCKER_VERSION" ]; then - echo "Error getting docker version. Error when running docker command. Check that docker is installed correctly." - fi - DOCKER_VERSION_MAJOR=$(echo "$DOCKER_VERSION"| cut -d'.' -f 1) - DOCKER_VERSION_MINOR=$(echo "$DOCKER_VERSION"| cut -d'.' -f 2) - DOCKER_VERSION_BUILD=$(echo "$DOCKER_VERSION"| cut -d'.' -f 3) - - if [ "$(minimum_version_check $REQ_DOCKER_VERSION $DOCKER_VERSION_MAJOR $DOCKER_VERSION_MINOR $DOCKER_VERSION_BUILD )" == "true" ]; then - [ -f .docker_outofdate ] && rm .docker_outofdate - DOCKER_VERSION_GOOD="true" - echo "Docker version $DOCKER_VERSION >= $REQ_DOCKER_VERSION. Docker is good to go." >&2 - else - if [ "$NOASKCONFIRM" == "true" ]; then - update_docker +function check_ssh_state() { + echo "" >&1 + echo "Check SSH state" >&1 + printf "Checking Container keys... " >&1 + if [[ "$(check_container_ssh)" == "false" ]]; then + HAS_ERROR="true" + echo " --- Something went wrong with SSH key installation --- " >&1 + echo "SSH keys for containers do not exist. the menu containers will not be able to execute commands on your host." >&1 + echo "To regenerate these keys, run:" >&1 + echo " bash ./menu.sh --run-env-setup" >&1 + else + echo "Keys file found." >&1 + printf "Checking Host Authorised keys... " >&1 + if [[ "$(check_host_ssh_keys)" == "false" ]]; then + HAS_ERROR="true" + echo " --- Something went wrong with SSH key installation --- " >&1 + echo "SSH key for menu containers not found in authorized_keys file" >&1 + echo "To regenerate and install keys, run:" >&1 + echo " bash ./menu.sh --run-env-setup" >&1 else - if [ ! -f .docker_outofdate ]; then - if (whiptail --title "Docker and Docker-Compose Version Issue" --yesno "Docker version is currently $DOCKER_VERSION which is less than $REQ_DOCKER_VERSION consider upgrading or you may experience issues. You will not be prompted again. You can manually upgrade by typing:\n sudo apt upgrade docker docker-compose\n\nAttempt to upgrade now?" 20 78); then - update_docker - else - touch .docker_outofdate - fi - fi + echo "Key found in authorized_keys file." >&1 fi - fi - else - [ -f .docker_outofdate ] && rm .docker_outofdate - echo "Docker not installed" >&2 - if [ "$NOASKCONFIRM" == "true" ]; then - do_env_setup - install_docker - else - if [ ! -f .docker_notinstalled ]; then - if (whiptail --title "Docker and Docker-Compose" --yesno "Docker is not currently installed, and is required to run IOTstack. Would you like to install docker and docker-compose now?\nYou will not be prompted again." 20 78); then - [ -f .docker_notinstalled ] && rm .docker_notinstalled - do_env_setup - install_docker - else - touch .docker_notinstalled - fi - fi - fi - fi + fi } -function do_env_checks() { - GROUPSGOOD=0 +function do_group_setup() { + echo "" >&1 + echo "User group setup" >&1 + GROUPCHANGE="false" + if [[ ! "$(user_in_group bluetooth)" == "notgroup" ]] && [[ ! "$(user_in_group bluetooth)" == "true" ]]; then + echo "User is NOT in 'bluetooth' group. Adding:" >&1 + echo "sudo usermod -G bluetooth -a $USER" >&1 + sudo -E usermod -G "bluetooth" -a $USER + GROUPCHANGE="true" + else + echo "User already in bluetooth group" >&1 + fi - if [[ ! "$(user_in_group bluetooth)" == "notgroup" ]] && [[ ! "$(user_in_group bluetooth)" == "true" ]]; then - GROUPSGOOD=1 - echo "User is NOT in 'bluetooth' group" >&2 - fi + if [ ! "$(user_in_group docker)" == "true" ]; then + echo "User is NOT in 'docker' group. Adding:" >&1 + echo "sudo usermod -G docker -a $USER" >&1 + sudo -E usermod -G "docker" -a $USER + GROUPCHANGE="true" + else + echo "User already in docker group" >&1 + fi - if [[ ! "$(user_in_group docker)" == "true" ]]; then - GROUPSGOOD=1 - echo "User is NOT in 'docker' group" >&2 - fi + if [[ "$GROUPCHANGE" == "true" ]]; then + REBOOT_REQ="true" + echo "" >&1 + echo "Rebooting or logging off is advised." >&1 + fi +} - if [ "$GROUPSGOOD" == 1 ]; then - echo "!! You might experience issues with docker or bluetooth. To fix run: ./menu.sh --run-env-setup" - fi +function do_env_setup() { + echo "" >&1 + echo "Host environment and dependency installation" >&1 + sudo -E apt update + echo "Installing dependencies: git, wget, unzip, jq, netcat, screen" >&1 + sudo -E apt install git wget unzip jq netcat screen -y + if [ ! $? -eq 0 ]; then + HAS_ERROR="true" + echo "" >&1 + echo "Dependency install failed. Aborting installation" >&1 + exit 1 + fi } -touch .new_install -echo "Enter in the sudo password when prompted, to install dependencies" +function do_iotstack_setup() { + echo "" >&1 + echo "IOTstack setup" >&1 + if [ -f "./menu.sh" ]; then + echo "'./menu.sh' file detected, will not reclone." >&1 + else + echo "IOTstack will be cloned into $(pwd)/IOTstack" >&1 + git clone https://github.com/SensorsIot/IOTstack.git + + if [[ $? -eq 0 ]]; then + echo "IOTstack cloned" >&1 + else + echo "Error cloning IOTstack" >&1 + fi -sudo apt-get install git -y -git clone https://github.com/SensorsIot/IOTstack.git -cd IOTstack + cd IOTstack + IOTCDRS=$? + echo "Current Dir: $(pwd)" >&1 + if [[ $IOTCDRS -eq 0 ]]; then + echo "IOTstack directory found" >&1 + else + HAS_ERROR="true" + echo "Could not find IOTstack directory" >&1 + exit 5 + fi + + if [[ -n "$IOTSTACK_INSTALL_BRANCH" ]]; then + echo "Attempting to switch to install branch: '$IOTSTACK_INSTALL_BRANCH'" >&1 + git checkout $IOTSTACK_INSTALL_BRANCH + fi + fi +} -if [ $? -eq 0 ]; then - echo "IOTstack cloned" +function generate_container_ssh() { + cat /dev/null | ssh-keygen -q -N "" -f $CONTAINER_KEYS_FILE +} + +function install_ssh_keys() { + echo "" >&1 + echo "Install SSH Keys" >&1 + touch $AUTH_KEYS_FILE + if [ -f "$CONTAINER_KEYS_FILE" ]; then + NEW_KEY="$(cat $CONTAINER_KEYS_FILE.pub)" + if grep -Fxq "$NEW_KEY" $AUTH_KEYS_FILE ; then + echo "Key already exists in '$AUTH_KEYS_FILE' Skipping..." >&1 + else + echo "$NEW_KEY" >> $AUTH_KEYS_FILE + echo "cat $CONTAINER_KEYS_FILE.pub >> $AUTH_KEYS_FILE" >&1 + echo "Key added." >&1 + fi + fi +} + +function ssh_management() { + if [[ "$SSH_KEY_INSTALL" == "true" ]]; then + generate_container_ssh + install_ssh_keys + check_ssh_state + elif [[ "$SSH_KEY_INSTALL" == "false" ]]; then + echo "Skipping container SSH key install" >&1 + else + echo "" >&1 + echo "IOTstack runs its menu and API inside docker containers. In order for these containers to be able to execute commands on your host, SSH keys are required to be generated and installed." >&1 + echo "These keys never leave your host and are only consumed by the menu containers. You can set these up yourself later, either manually or by running ./menu.sh --run-env-setup" >&1 + echo "See the documentation in the github for more information." >&1 + echo "In the future, setting the environment variable 'SSH_KEY_INSTALL' to 'true' or 'false' will skip this prompt" >&1 + echo " " >&1 + echo " " + read -p "Generate and Install the SSH keys? [y/n] " -n 1 -r < /dev/tty + if [[ $REPLY =~ ^[Yy]$ ]]; then + generate_container_ssh + install_ssh_keys + check_ssh_state + else + echo "Skipping container SSH key install" >&1 + fi + fi +} + +# Entry point +do_env_setup +do_iotstack_setup +ssh_management +install_docker +do_group_setup + +touch .installed + +if [[ "$HAS_ERROR" == "true" ]]; then + echo "" + echo "--------" + echo "" + echo "An error occured installing IOTstack. Please review the output above." + echo "If you have just installed the OS, try giving the your system 30 minutes to complete setup and try installing IOTstack again." + read -n 1 -s -r -p "Press any key to continue" else - echo "Could not find IOTstack directory" - exit 5 + echo "IOTstack setup completed" fi -do_python3_checks -do_docker_checks -echo "Setting up environment:" -if [[ ! "$(user_in_group bluetooth)" == "notgroup" ]] && [[ ! "$(user_in_group bluetooth)" == "true" ]]; then - echo "User is NOT in 'bluetooth' group. Adding:" >&2 - echo "sudo usermod -G bluetooth -a $USER" >&2 - echo "You will need to restart your system before the changes take effect." - sudo usermod -G "bluetooth" -a $USER +if [[ "$REBOOT_REQ" == "true" ]]; then + if [[ "$NOASKCONFIRM" == "true" ]]; then + echo "Rebooting..." + sudo reboot + else + echo "" + echo "You need to reboot your system to ensure IOTstack runs correctly." + if (whiptail --title "Reboot Required" --yesno "A restart is required to ensure IOTstack runs correctly.\n\nAfter reboot start IOTstack by running:\n ./menu.sh\n\nFrom the IOTstack directory:\n $(pwd)\n\nReboot now?" 20 78); then + echo "Rebooting..." + sleep 1 + sudo reboot + fi + fi fi -if [ ! "$(user_in_group docker)" == "true" ]; then - echo "User is NOT in 'docker' group. Adding:" >&2 - echo "sudo usermod -G docker -a $USER" >&2 - echo "You will need to restart your system before the changes take effect." - sudo usermod -G "docker" -a $USER -fi -do_env_checks +echo "" +echo "Start IOTstack by running in IOTstack's directory:" +echo " ./menu.sh" diff --git a/menu.sh b/menu.sh index f70fc883c..bea15901f 100755 --- a/menu.sh +++ b/menu.sh @@ -4,433 +4,308 @@ CURRENT_BRANCH=$(git name-rev --name-only HEAD) # Minimum Software Versions REQ_DOCKER_VERSION=18.2.0 -REQ_PYTHON_VERSION=3.6.9 -REQ_PIP_VERSION=3.6.9 -REQ_PYAML_VERSION=0.16.12 -REQ_BLESSED_VERSION=1.17.5 - -PYTHON_CMD=python3 -VGET_CMD="$PYTHON_CMD ./scripts/python_deps_check.py" sys_arch=$(uname -m) # ---------------------------------------------- # Helper functions # ---------------------------------------------- -function command_exists() { - command -v "$@" > /dev/null 2>&1 -} - -function user_in_group() -{ - # see if the group exists - grep -q "^$1:" /etc/group; - - # sense that the group does not exist - if [ $? -ne 0 ]; then return 0; fi - - # group exists - now check that the user is a member - groups | grep -q "\b$1\b" -} - -function minimum_version_check() { - # Usage: minimum_version_check required_version current_major current_minor current_build - # Example: minimum_version_check "1.2.3" 1 2 3 - REQ_MIN_VERSION_MAJOR=$(echo "$1"| cut -d' ' -f 2 | cut -d'.' -f 1) - REQ_MIN_VERSION_MINOR=$(echo "$1"| cut -d' ' -f 2 | cut -d'.' -f 2) - REQ_MIN_VERSION_BUILD=$(echo "$1"| cut -d' ' -f 2 | cut -d'.' -f 3) - - CURR_VERSION_MAJOR=$2 - CURR_VERSION_MINOR=$3 - CURR_VERSION_BUILD=$4 - - VERSION_GOOD="Unknown" - - NUMB_REG='^[0-9]+$' - if ! [[ $CURR_VERSION_MAJOR =~ $NUMB_REG ]] ; then - echo "$VERSION_GOOD" - return 1 - fi - if ! [[ $CURR_VERSION_MINOR =~ $NUMB_REG ]] ; then - echo "$VERSION_GOOD" - return 1 - fi - if ! [[ $CURR_VERSION_BUILD =~ $NUMB_REG ]] ; then - echo "$VERSION_GOOD" - return 1 - fi - - if [ -z "$CURR_VERSION_MAJOR" ]; then - echo "$VERSION_GOOD" - return 1 - fi - - if [ -z "$CURR_VERSION_MINOR" ]; then - echo "$VERSION_GOOD" - return 1 - fi - - if [ -z "$CURR_VERSION_BUILD" ]; then - echo "$VERSION_GOOD" - return 1 - fi - - if [ "${CURR_VERSION_MAJOR}" -ge $REQ_MIN_VERSION_MAJOR ]; then - VERSION_GOOD="true" - echo "$VERSION_GOOD" - return 0 - else - VERSION_GOOD="false" - fi - - if [ "${CURR_VERSION_MAJOR}" -ge $REQ_MIN_VERSION_MAJOR ] && \ - [ "${CURR_VERSION_MINOR}" -ge $REQ_MIN_VERSION_MINOR ]; then - VERSION_GOOD="true" - echo "$VERSION_GOOD" - return 0 - else - VERSION_GOOD="false" - fi - - if [ "${CURR_VERSION_MAJOR}" -ge $REQ_MIN_VERSION_MAJOR ] && \ - [ "${CURR_VERSION_MINOR}" -ge $REQ_MIN_VERSION_MINOR ] && \ - [ "${CURR_VERSION_BUILD}" -ge $REQ_MIN_VERSION_BUILD ]; then - VERSION_GOOD="true" - echo "$VERSION_GOOD" - return 0 - else - VERSION_GOOD="false" - fi - - echo "$VERSION_GOOD" -} - -function user_in_group() -{ - if grep -q $1 /etc/group ; then - if id -nGz "$USER" | grep -qzxF "$1"; then - echo "true" - else - echo "false" - fi - else - echo "notgroup" - fi -} - -function check_git_updates() -{ - UPSTREAM=${1:-'@{u}'} - LOCAL=$(git rev-parse @) - REMOTE=$(git rev-parse "$UPSTREAM") - BASE=$(git merge-base @ "$UPSTREAM") - - if [ $LOCAL = $REMOTE ]; then - echo "Up-to-date" - elif [ $LOCAL = $BASE ]; then - echo "Need to pull" - elif [ $REMOTE = $BASE ]; then - echo "Need to push" - else - echo "Diverged" - fi -} -function install_python3_and_deps() { - CURR_PYTHON_VER="${1:-Unknown}" - CURR_PYAML_VER="${2:-Unknown}" - if (whiptail --title "Python 3 and Dependencies" --yesno "Python 3.6.9 or later (Current = $CURR_PYTHON_VER), ruamel.yaml 0.16.12 or later (Current = $CURR_PYAML_VER), blessed and pip3 are required for IOTstack to function correctly. Install these now?" 20 78); then - sudo apt update - sudo apt install -y python3-pip python3-dev - if [ $? -eq 0 ]; then - PYTHON_VERSION_GOOD="true" - else - echo "Failed to install Python" >&2 - exit 1 - fi - pip3 install -U ruamel.yaml==0.16.12 blessed - if [ $? -eq 0 ]; then - PYAML_VERSION_GOOD="true" - BLESSED_GOOD="true" - else - echo "Failed to install ruamel.yaml and Blessed" >&2 - exit 1 - fi - fi -} - -function install_docker() { - sudo bash ./scripts/install_docker.sh install -} - -function update_docker() { - sudo bash ./scripts/install_docker.sh upgrade +source ./scripts/setup_iotstack.sh +source ./.internal/meta.sh + +SKIPCHECKS="false" +FORCE_REBUILD="false" + +function check_git_updates() { + UPSTREAM=${1:-'@{u}'} + LOCAL=$(git rev-parse @) + REMOTE=$(git rev-parse "$UPSTREAM") + BASE=$(git merge-base @ "$UPSTREAM") + + if [ $LOCAL = $REMOTE ]; then + echo "Up-to-date" + elif [ $LOCAL = $BASE ]; then + echo "Need to pull" + elif [ $REMOTE = $BASE ]; then + echo "Need to push" + else + echo "Diverged" + fi } function update_project() { - git pull origin $CURRENT_BRANCH - git status -} - -function do_python3_checks() { - PYTHON_VERSION_GOOD="false" - PYAML_VERSION_GOOD="false" - BLESSED_GOOD="false" - - if command_exists $PYTHON_CMD && command_exists pip3; then - PYTHON_VERSION=$($PYTHON_CMD --version 2>/dev/null) - PYTHON_VERSION_MAJOR=$(echo "$PYTHON_VERSION"| cut -d' ' -f 2 | cut -d' ' -f 2 | cut -d'.' -f 1) - PYTHON_VERSION_MINOR=$(echo "$PYTHON_VERSION"| cut -d' ' -f 2 | cut -d'.' -f 2) - PYTHON_VERSION_BUILD=$(echo "$PYTHON_VERSION"| cut -d' ' -f 2 | cut -d'.' -f 3) - - PYAML_VERSION=$($VGET_CMD --pyaml-version 2>/dev/null) - PYAML_VERSION="${PYAML_VERSION:-Unknown}" - PYAML_VERSION_MAJOR=$(echo "$PYAML_VERSION"| cut -d' ' -f 2 | cut -d'.' -f 1) - PYAML_VERSION_MINOR=$(echo "$PYAML_VERSION"| cut -d' ' -f 2 | cut -d'.' -f 2) - PYAML_VERSION_BUILD=$(echo "$PYAML_VERSION"| cut -d' ' -f 2 |cut -d'.' -f 3) - - BLESSED_VERSION=$($VGET_CMD --blessed-version 2>/dev/null) - BLESSED_VERSION="${BLESSED_VERSION:-Unknown}" - BLESSED_VERSION_MAJOR=$(echo "$BLESSED_VERSION"| cut -d' ' -f 2 | cut -d'.' -f 1) - BLESSED_VERSION_MINOR=$(echo "$BLESSED_VERSION"| cut -d' ' -f 2 | cut -d'.' -f 2) - BLESSED_VERSION_BUILD=$(echo "$BLESSED_VERSION"| cut -d' ' -f 2 | cut -d'.' -f 3) - - printf "Python Version: '${PYTHON_VERSION:-Unknown}'. " - if [ "$(minimum_version_check $REQ_PYTHON_VERSION $PYTHON_VERSION_MAJOR $PYTHON_VERSION_MINOR $PYTHON_VERSION_BUILD)" == "true" ]; then - PYTHON_VERSION_GOOD="true" - echo "Python is up to date." >&2 - else - echo "Python is outdated." >&2 - install_python3_and_deps "$PYTHON_VERSION_MAJOR.$PYTHON_VERSION_MINOR.$PYTHON_VERSION_BUILD" "$PYAML_VERSION_MAJOR.$PYAML_VERSION_MINOR.$PYAML_VERSION_BUILD" - return 1 - fi - printf "ruamel.yaml Version: '$PYAML_VERSION'. " - if [ "$(minimum_version_check $REQ_PYAML_VERSION $PYAML_VERSION_MAJOR $PYAML_VERSION_MINOR $PYAML_VERSION_BUILD)" == "true" ]; then - PYAML_VERSION_GOOD="true" - echo "ruamel.yaml is up to date." >&2 - else - echo "ruamel.yaml is outdated." >&2 - if [ "$PYAML_VERSION" != "Unknown" ]; then - install_python3_and_deps "$PYTHON_VERSION_MAJOR.$PYTHON_VERSION_MINOR.$PYTHON_VERSION_BUILD" "$PYAML_VERSION_MAJOR.$PYAML_VERSION_MINOR.$PYAML_VERSION_BUILD" - else - install_python3_and_deps "$PYTHON_VERSION_MAJOR.$PYTHON_VERSION_MINOR.$PYTHON_VERSION_BUILD" - fi - return 1 - fi - printf "Blessed Version: '$BLESSED_VERSION'. " - if [ "$(minimum_version_check $REQ_BLESSED_VERSION $BLESSED_VERSION_MAJOR $BLESSED_VERSION_MINOR $BLESSED_VERSION_BUILD)" == "true" ]; then - BLESSED_GOOD="true" - echo "Blessed is up to date." >&2 - else - echo "Blessed is outdated." >&2 - if [ "$BLESSED_VERSION" != "Unknown" ]; then - install_python3_and_deps "$PYTHON_VERSION_MAJOR.$PYTHON_VERSION_MINOR.$PYTHON_VERSION_BUILD" "$PYAML_VERSION_MAJOR.$PYAML_VERSION_MINOR.$PYAML_VERSION_BUILD" - else - install_python3_and_deps "$PYTHON_VERSION_MAJOR.$PYTHON_VERSION_MINOR.$PYTHON_VERSION_BUILD" - fi - return 1 - fi - else - install_python3_and_deps - return 1 - fi -} - -function do_env_setup() { - echo "Setting up environment:" - if [[ ! "$(user_in_group bluetooth)" == "notgroup" ]] && [[ ! "$(user_in_group bluetooth)" == "true" ]]; then - echo "User is NOT in 'bluetooth' group. Adding:" >&2 - echo "sudo usermod -G bluetooth -a $USER" >&2 - echo "You will need to restart your system before the changes take effect." - sudo usermod -G "bluetooth" -a $USER - fi - - if [ ! "$(user_in_group docker)" == "true" ]; then - echo "User is NOT in 'docker' group. Adding:" >&2 - echo "sudo usermod -G docker -a $USER" >&2 - echo "You will need to restart your system before the changes take effect." - sudo usermod -G "docker" -a $USER - fi + git pull origin $CURRENT_BRANCH + git status } -function do_docker_checks() { - if command_exists docker; then - DOCKER_VERSION_GOOD="false" - DOCKER_VERSION=$(docker version -f "{{.Server.Version}}" 2>&1) - echo "Command: docker version -f \"{{.Server.Version}}\"" - if [[ "$DOCKER_VERSION" == *"Cannot connect to the Docker daemon"* ]]; then - echo "Error getting docker version. Error when connecting to docker daemon. Check that docker is running." - if (whiptail --title "Docker and Docker-Compose" --yesno "Error getting docker version. Error when connecting to docker daemon. Check that docker is running.\n\nCommand: docker version -f \"{{.Server.Version}}\"\n\nExit?" 20 78); then - exit 1 - fi - elif [[ "$DOCKER_VERSION" == *" permission denied"* ]]; then - echo "Error getting docker version. Received permission denied error. Try running with: ./menu.sh --run-env-setup" - if (whiptail --title "Docker and Docker-Compose" --yesno "Error getting docker version. Received permission denied error.\n\nTry rerunning the menu with: ./menu.sh --run-env-setup\n\nExit?" 20 78); then - exit 1 - fi - return 0 - fi - - if [[ -z "$DOCKER_VERSION" ]]; then - echo "Error getting docker version. Error when running docker command. Check that docker is installed correctly." - fi - - DOCKER_VERSION_MAJOR=$(echo "$DOCKER_VERSION"| cut -d'.' -f 1) - DOCKER_VERSION_MINOR=$(echo "$DOCKER_VERSION"| cut -d'.' -f 2) - - DOCKER_VERSION_BUILD=$(echo "$DOCKER_VERSION"| cut -d'.' -f 3) - DOCKER_VERSION_BUILD=$(echo "$DOCKER_VERSION_BUILD"| cut -f1 -d"-") - - if [ "$(minimum_version_check $REQ_DOCKER_VERSION $DOCKER_VERSION_MAJOR $DOCKER_VERSION_MINOR $DOCKER_VERSION_BUILD )" == "true" ]; then - [ -f .docker_outofdate ] && rm .docker_outofdate - DOCKER_VERSION_GOOD="true" - echo "Docker version $DOCKER_VERSION >= $REQ_DOCKER_VERSION. Docker is good to go." >&2 - else - if [ ! -f .docker_outofdate ]; then - if (whiptail --title "Docker and Docker-Compose Version Issue" --yesno "Docker version is currently $DOCKER_VERSION which is less than $REQ_DOCKER_VERSION consider upgrading or you may experience issues. You will not be prompted again. You can manually upgrade by typing:\n sudo apt upgrade docker docker-compose\n\nAttempt to upgrade now?" 20 78); then - update_docker - else - touch .docker_outofdate - fi - fi - fi - else - [ -f .docker_outofdate ] && rm .docker_outofdate - echo "Docker not installed" >&2 - if [ ! -f .docker_notinstalled ]; then - if (whiptail --title "Docker and Docker-Compose" --yesno "Docker is not currently installed, and is required to run IOTstack. Would you like to install docker and docker-compose now?\nYou will not be prompted again." 20 78); then - [ -f .docker_notinstalled ] && rm .docker_notinstalled - echo "Setting up environment:" - if [[ ! "$(user_in_group bluetooth)" == "notgroup" ]] && [[ ! "$(user_in_group bluetooth)" == "true" ]]; then - echo "User is NOT in 'bluetooth' group. Adding:" >&2 - echo "sudo usermod -G bluetooth -a $USER" >&2 - echo "You will need to restart your system before the changes take effect." - sudo usermod -G "bluetooth" -a $USER - fi - - if [ ! "$(user_in_group docker)" == "true" ]; then - echo "User is NOT in 'docker' group. Adding:" >&2 - echo "sudo usermod -G docker -a $USER" >&2 - echo "You will need to restart your system before the changes take effect." - sudo usermod -G "docker" -a $USER - fi - install_docker - else - touch .docker_notinstalled - fi - fi - fi +function project_checks() { + echo "Checking for project update" >&1 + git fetch origin $CURRENT_BRANCH + + if [[ "$(check_git_updates)" == "Need to pull" ]]; then + echo "An update is available for IOTstack" >&1 + if [ ! -f .ignore_project_outofdate ]; then + if (whiptail --title "Project update" --yesno "An update is available for IOTstack\nYou will not be reminded again until after you update.\nYou can upgrade manually by typing:\n git pull origin $CURRENT_BRANCH \n\n\nWould you like to update now?" 14 78); then + update_project + else + touch .ignore_project_outofdate + fi + fi + else + [ -f .ignore_project_outofdate ] && rm .ignore_project_outofdate + echo "Project is up to date" >&1 + fi } -function do_project_checks() { - echo "Checking for project update" >&2 - git fetch origin $CURRENT_BRANCH - - if [[ "$(check_git_updates)" == "Need to pull" ]]; then - echo "An update is available for IOTstack" >&2 - if [ ! -f .project_outofdate ]; then - if (whiptail --title "Project update" --yesno "An update is available for IOTstack\nYou will not be reminded again until after you update.\nYou can upgrade manually by typing:\n git pull origin $CURRENT_BRANCH \n\n\nWould you like to update now?" 14 78); then - update_project - else - touch .project_outofdate - fi - fi - else - [ -f .project_outofdate ] && rm .project_outofdate - echo "Project is up to date" >&2 - fi -} - -function do_env_checks() { - GROUPSGOOD=0 - - if [[ ! "$(user_in_group bluetooth)" == "notgroup" ]] && [[ ! "$(user_in_group bluetooth)" == "true" ]]; then - GROUPSGOOD=1 - echo "User is NOT in 'bluetooth' group" >&2 - fi - - if [[ ! "$(user_in_group docker)" == "true" ]]; then - GROUPSGOOD=1 - echo "User is NOT in 'docker' group" >&2 - fi - - if [ "$GROUPSGOOD" == 1 ]; then - echo "!! You might experience issues with docker or bluetooth. To fix run: ./menu.sh --run-env-setup" - fi -} +while test $# -gt 0 +do + case "$1" in + --branch) CURRENT_BRANCH=${2:-$(git name-rev --name-only HEAD)} + ;; + --no-check) echo "" && SKIPCHECKS="true" + ;; + --stop) echo "Stopping all menu containers" && bash ./.internal/docker_menu.sh stop + ;; + --rebuild) echo "Force rebuild all menu containers" && FORCE_REBUILD="true" + ;; + --remerge-yaml-override) echo "Remerging 'compose-override.yml' and 'docker-compose-base.yml'. Menu will exit after merge" && REMERGE_COMPOSE_OVERRIDE="true" + ;; + --run-env-setup) + echo "Setting up environment:" + generate_container_ssh + install_ssh_keys + if [[ ! "$(user_in_group bluetooth)" == "notgroup" ]] && [[ ! "$(user_in_group bluetooth)" == "true" ]]; then + echo "User is NOT in 'bluetooth' group. Adding:" >&1 + echo "sudo -E usermod -G bluetooth -a $USER" >&1 + echo "You will need to restart your system before the changes take effect." + sudo -E usermod -G "bluetooth" -a $USER + fi + + if [ ! "$(user_in_group docker)" == "true" ]; then + echo "User is NOT in 'docker' group. Adding:" >&1 + echo "sudo -E usermod -G docker -a $USER" >&1 + echo "You will need to restart your system before the changes take effect." + sudo -E usermod -G "docker" -a $USER + fi + + echo "Setup completed" + ;; + --encoding) ENCODING_TYPE=$2 + ;; + --*) echo "bad option $1" + ;; + esac + shift +done # ---------------------------------------------- # Menu bootstrap entry point # ---------------------------------------------- - -if [[ "$*" == *"--no-check"* ]]; then - echo "Skipping preflight checks." +if [[ "$SKIPCHECKS" == "true" ]]; then + echo "Skipping preflight checks." else - do_project_checks - do_env_checks - do_python3_checks - echo "Please enter sudo pasword if prompted" - do_docker_checks - - if [[ "$DOCKER_VERSION_GOOD" == "true" ]] && \ - [[ "$PYTHON_VERSION_GOOD" == "true" ]] && \ - [[ "$PYAML_VERSION_GOOD" == "true" ]] && \ - [[ "$BLESSED_GOOD" == "true" ]]; then - echo "Project dependencies up to date" - echo "" - else - echo "Project dependencies not up to date. Menu may crash." - echo "To be prompted to update again, run command:" - echo " rm .docker_notinstalled || rm .docker_outofdate || rm .project_outofdate" - echo "" - fi + echo "Please enter sudo pasword if prompted" + + if [[ ! -f .installed ]]; then + echo "IOTstack has not yet been installed. Please reboot your system after installation is completed. " + echo " ./install.sh" + bash ./install.sh + exit 0 + fi + + project_checks + + echo "" + printf "Checking Container keys... " + if [[ "$(check_container_ssh)" == "false" ]]; then + echo "SSH keys for containers do not exist. the menu containers will not be able to execute commands on your host." + echo "To regenerate these keys, run:" + echo " bash ./menu.sh --run-env-setup" + else + echo "Keys file found." + printf "Checking Host Authorised keys... " + if [[ "$(check_host_ssh_keys)" == "false" ]]; then + echo "SSH key for menu containers not found in authorized_keys file" + echo "To regenerate and install keys, run:" + echo " bash ./menu.sh --run-env-setup" + else + echo "Key found in authorized_keys file." + fi + fi + + echo "" + printf "Checking Docker state... " + DOCKER_CHECK_RESULT="$(docker_check)" + if [[ "$DOCKER_CHECK_RESULT" == "fail" ]]; then + echo "Docker is not setup. Cannot continue" + exit 2 + fi + + if [[ "$DOCKER_CHECK_RESULT" == "outdated" ]]; then + echo "" + echo "Docker is outdated. You should consider updating. To be reprompted, type:" + echo " rm .ignore_docker_outofdate" + echo "" + fi + + echo "" + printf "Checking User groups ['bluetooth', 'docker']: " + if [[ "$(group_check)" == "fail" ]]; then + echo "User not in correct groups. Run:" + echo " bash ./menu.sh --run-env-setup" + else + echo "User in required groups." + fi + + # ---------------------------------------------- + # Check state of running menu instances + # ---------------------------------------------- + echo "" + printf "Checking menu container state... " + + PREBUILT_IMAGES="true" + if [[ "$(docker images -q iostack_api:$VERSION 2> /dev/null)" == "" ]]; then + PREBUILT_IMAGES="false" + fi + + if [[ "$(docker images -q iostack_pycli:$VERSION 2> /dev/null)" == "" ]]; then + PREBUILT_IMAGES="false" + fi + + if [[ "$(docker images -q iostack_wui:$VERSION 2> /dev/null)" == "" ]]; then + PREBUILT_IMAGES="false" + fi + + if [[ "$PREBUILT_IMAGES" == "false" || "$FORCE_REBUILD" == "true" ]]; then + echo " Rebuild required. All running menu containers will be restarted." + echo "You either recently installed or upgraded IOTstack. The menu docker images need to be rebuilt in order for the menu to run correctly. This will take about 10 minutes and is completely automatic." + echo "" + echo "Spinning down container instances" + # Change directory to .internal for docker build + CPWD=$(pwd) + + if [[ ! "$(basename $CPWD)" == ".internal" ]]; then + cd .internal/ + fi + + bash ./docker_menu.sh stop + + sleep 1 + + docker rmi $(docker images -q --format "{{.Repository}}:{{.Tag}}" | grep 'iostack_wui') --force 2> /dev/null + docker rmi $(docker images -q --format "{{.Repository}}:{{.Tag}}" | grep 'iostack_api') --force 2> /dev/null + docker rmi $(docker images -q --format "{{.Repository}}:{{.Tag}}" | grep 'iostack_pycli') --force 2> /dev/null + + echo "" + echo "Beginning menu build process now." + sleep 1 + echo "" + + # Build all asynchronously, so it's faster. Give PyCLI a slight headstart to keep the user waiting the shortest time. + docker build --quiet -t iostack_pycli:$VERSION -f ./pycli.Dockerfile . > /dev/null & + sleep 1 + docker build --quiet -t iostack_api:$VERSION -f ./api.Dockerfile . > /dev/null & + docker build --quiet -t iostack_wui:$VERSION -f ./wui.Dockerfile . > /dev/null & + + cd $CPWD # Change back to previous directory. + + SLEEP_COUNTER=0 + API_REBUILD_DONE="not completed" + PYCLI_REBUILD_DONE="not completed" + WUI_REBUILD_DONE="not completed" + + until [[ $SLEEP_COUNTER -gt 721 || ("$API_REBUILD_DONE" == "completed" && "$PYCLI_REBUILD_DONE" == "completed" && "$WUI_REBUILD_DONE" == "completed") ]]; do + if [[ ! "$(docker images -q iostack_api:$VERSION)" == "" && ! $API_REBUILD_DONE == "completed" ]]; then + API_REBUILD_DONE="completed" + echo "" + echo "iostack_api:$VERSION build complete" + fi + + if [[ ! "$(docker images -q iostack_pycli:$VERSION)" == "" && ! $PYCLI_REBUILD_DONE == "completed" ]]; then + PYCLI_REBUILD_DONE="completed" + echo "" + echo "iostack_pycli:$VERSION build complete" + fi + + if [[ ! "$(docker images -q iostack_wui:$VERSION)" == "" && ! $WUI_REBUILD_DONE == "completed" ]]; then + WUI_REBUILD_DONE="completed" + echo "" + echo "iostack_wui:$VERSION build complete" + fi + + if [ "$(( $SLEEP_COUNTER % 60 ))" -eq 0 ]; then + echo "" + if [[ $SLEEP_COUNTER -gt 1 ]]; then + echo "$SLEEP_COUNTER seconds passed. Still building..." + fi + else + printf . + fi + sleep 1 + + ((SLEEP_COUNTER++)) + done + + echo "" + fi + + if [[ $SLEEP_COUNTER -gt 720 ]]; then + echo "" + echo "Build timeout occured" + echo "It's possible the container(s) just need a little more time to finish building." + echo "This error can occur if your system is busy running other processes while building the containers." + echo "Only the API and PyCLI containers need to build to run the CLI menu." + echo "Only the API and WUI containers need to build to view the web UI." + echo "You can also try rerunning the menu after waiting a short time." + echo "" + echo "API Build: $API_REBUILD_DONE" + echo "PyCLI Build: $PYCLI_REBUILD_DONE" + echo "WUI Build: $WUI_REBUILD_DONE" + echo "" + if [[ "$SKIPCHECKS" == "false" ]]; then + read -n 1 -s -r -p "Press any key to continue" + fi + fi fi +echo " Menu check completed." +echo "" -while test $# -gt 0 -do - case "$1" in - --branch) CURRENT_BRANCH=${2:-$(git name-rev --name-only HEAD)} - ;; - --no-check) echo "" - ;; - --run-env-setup) # Sudo cannot be run from inside functions. - echo "Setting up environment:" - if [[ ! "$(user_in_group bluetooth)" == "notgroup" ]] && [[ ! "$(user_in_group bluetooth)" == "true" ]]; then - echo "User is NOT in 'bluetooth' group. Adding:" >&2 - echo "sudo usermod -G bluetooth -a $USER" >&2 - echo "You will need to restart your system before the changes take effect." - sudo usermod -G "bluetooth" -a $USER - fi - - if [ ! "$(user_in_group docker)" == "true" ]; then - echo "User is NOT in 'docker' group. Adding:" >&2 - echo "sudo usermod -G docker -a $USER" >&2 - echo "You will need to restart your system before the changes take effect." - sudo usermod -G "docker" -a $USER - fi - ;; - --encoding) ENCODING_TYPE=$2 - ;; - --*) echo "bad option $1" - ;; - esac - shift -done +if [[ "$REMERGE_COMPOSE_OVERRIDE" == "true" ]]; then + echo "Merging 'compose-override.yml' with 'docker-compose-base.yml':" + echo "docker container run -it -v $(pwd)/compose-override.yml:/usr/iotstack_pycli/compose-override.yml:ro -v $(pwd)/docker-compose-base.yml:/usr/iotstack_pycli/docker-compose-base.yml:ro -v $(pwd)/docker-compose.yml:/usr/iotstack_pycli/docker-compose.yml -e \"PYCLI_OVERRIDE_YML=compose-override.yml\" -e \"PYCLI_BASE_YML=docker-compose-base.yml\" -e \"PYCLI_OUTPUT_YML=docker-compose.yml\" iostack_pycli:$VERSION /usr/local/bin/python3 /usr/iotstack_pycli/compose_override_entry.py" -# This section is temporary, it's just for notifying people of potential breaking changes. -if [[ -f .new_install ]]; then - echo "Existing installation detected." -else - if [[ -f docker-compose.yml ]]; then - echo "Warning: Please ensure to read the following prompt" - sleep 1 - if (whiptail --title "Project update" --yesno "There has been a large update to IOTstack, and there may be breaking changes to your current setup. Would you like to switch to the older branch by having the command:\ngit checkout old-menu\n\nrun for you?\n\nIt's suggested that you backup your existing IOTstack instance if you select No\n\nIf you run into problems, please open an issue: https://github.com/SensorsIot/IOTstack/issues\n\nOr Discord: https://discord.gg/ZpKHnks\n\nRelease Notes: https://github.com/SensorsIot/IOTstack/blob/master/docs/New-Menu-Release-Notes.md" 24 95); then - echo "Running command: git checkout old-menu" - git checkout old-menu - sleep 2 - fi - fi - touch .new_install + docker container run -it -v $(pwd)/compose-override.yml:/usr/iotstack_pycli/compose-override.yml:ro -v $(pwd)/docker-compose-base.yml:/usr/iotstack_pycli/docker-compose-base.yml:ro -v $(pwd)/docker-compose.yml:/usr/iotstack_pycli/docker-compose.yml -e "PYCLI_OVERRIDE_YML=compose-override.yml" -e "PYCLI_BASE_YML=docker-compose-base.yml" -e "PYCLI_OUTPUT_YML=docker-compose.yml" iostack_pycli:$VERSION /usr/local/bin/python3 /usr/iotstack_pycli/compose_override_entry.py + exit +fi + +echo "Spinning up menu containers... " + +if [[ "$SKIPCHECKS" == "false" ]]; then + if nc -w 1 $HOST_CON_IP $API_PORT ; then + echo "WUI detected on $HOST_CON_IP:$API_PORT" + fi + + if nc -w 1 $HOST_CON_IP $WUI_PORT ; then + echo "API detected on $HOST_CON_IP:$WUI_PORT" + fi fi -# Hand control to new menu -$PYTHON_CMD ./scripts/menu_main.py $ENCODING_TYPE +# If PyCLI is already running then reattach +PYCLI_ID="$(docker ps --format '{{.ID}} {{.Image}}' | grep -w iostack_pycli:$VERSION | cut -d ' ' -f1 | head -n 1)" +if [[ "$PYCLI_ID" == "" ]]; then + CPWD=$(pwd) + if [[ ! "$(basename $CPWD)" == ".internal" ]]; then + cd .internal/ + fi + bash ./docker_menu.sh + cd $CPWD +else + CPWD=$(pwd) + if [[ ! "$(basename $CPWD)" == ".internal" ]]; then + cd .internal/ + fi + bash ./ctrl_api.sh > /dev/null + cd $CPWD + echo "PyCLI menu is already running. Reattaching..." + docker attach --sig-proxy=false $PYCLI_ID +fi diff --git a/scripts/2022-10-01-wireguard-restructure.sh b/scripts/2022-10-01-wireguard-restructure.sh new file mode 100755 index 000000000..22f2d35db --- /dev/null +++ b/scripts/2022-10-01-wireguard-restructure.sh @@ -0,0 +1,155 @@ +#!/usr/bin/env bash + +# support user renaming of script +SCRIPT=$(basename "$0") + +# dependency check +if [ -z "$(which rsync)" -o -z "$(which jq)" ] ; then + echo "This script depends on jq and rsync. Please run" + echo " sudo apt update && sudo apt install jq rsync" + exit -1 +fi + +# useful function +isContainerRunning() { + if STATUS=$(curl -s --unix-socket /var/run/docker.sock http://localhost/containers/$1/json | jq .State.Status) ; then + if [ "$STATUS" = "\"running\"" ] ; then + return 0 + fi + fi + return 1 +} + + +# should not run as root +[ "$EUID" -eq 0 ] && echo "$SCRIPT should NOT be run using sudo" && exit -1 + +# dependency check +if [ -z "$(which rsync)" -o -z "$(which jq)" ] ; then + echo "This script depends on jq and rsync. Please run" + echo " sudo apt update && sudo apt install jq rsync" + exit -1 +fi + +read -r -d '' RUNNINGNOTES <<-EOM +\n +=============================================================================== + +Error: The WireGuard container can't be running during the migration. + Please stop the container like this: + + $ cd ~/IOTstack + $ docker-compose rm --force --stop -v wireguard + + Do not start the container again until the migration is complete and + you have followed the instructions for modifying WireGuard's service + definition in your docker-compose.yml + +=============================================================================== +\n +EOM + +# wireguard can't be running +isContainerRunning "wireguard" && echo -e "$RUNNINGNOTES" && exit -1 + +# source directory is +WIREGUARD="$HOME/IOTstack/volumes/wireguard" + +# source directory must exist +[ ! -d "$WIREGUARD" ] && echo "Error: $WIREGUARD does not exist" && exit -1 + +# the backup directory is +BACKUP="$WIREGUARD.bak" + +read -r -d '' REPEATNOTES <<-EOM +\n +=============================================================================== + +Error: It looks like you might be trying to migrate twice! You can't do that. + + If you need to start over, you can try resetting like this: + + $ cd ~/IOTstack/volumes + $ sudo rm -rf wireguard + $ sudo mv wireguard.bak wireguard + + Alternatively, restore ~/IOTstack/volumes/wireguard from a backup. + +=============================================================================== +\n +EOM + +# required sub-directories are +CONFIGD="config" +INITD="custom-cont-init.d" +SERVICESD="custom-services.d" + +# backup directory must not exist +[ -d "$BACKUP" ] && echo -e "$REPEATNOTES" && exit -1 + +# required sub-directories must not exist +[ -d "$WIREGUARD/$CONFIGD" ] && echo -e "$REPEATNOTES" && exit -1 +[ -d "$WIREGUARD/$INITD" ] && echo -e "$REPEATNOTES" && exit -1 +[ -d "$WIREGUARD/$SERVICESD" ] && echo -e "$REPEATNOTES" && exit -1 + +# rename source to backup +echo "Renaming $WIREGUARD to $BACKUP" +sudo mv "$WIREGUARD" "$BACKUP" + +# create the required directories +echo "creating required sub-folders" +sudo mkdir -p "$WIREGUARD/$CONFIGD" "$WIREGUARD/$INITD" "$WIREGUARD/$SERVICESD" + +# for now, set ownership to the current user +echo "setting ownership on $WIREGUARD to $USER" +sudo chown -R "$USER":"$USER" "$WIREGUARD" + +# migrate config directory components +echo "migrating user-configuration components" +rsync -r --ignore-existing --exclude="${INITD}*" --exclude="${SERVICESD}*" "$BACKUP"/ "$WIREGUARD/$CONFIGD" + +# migrate special cases and change ownership to root +echo "migrating custom configuration options" +for C in "$INITD" "$SERVICESD" ; do + for D in "$BACKUP/$C"* ; do + echo " merging $D into $WIREGUARD/$C" + rsync -r --ignore-existing --exclude="README.txt" "$D"/ "$WIREGUARD/$C" + echo " changing ownership to root" + sudo chown -R root:root "$WIREGUARD/$C" + done +done + +# force correct mode for wg0.conf +echo "Setting mode 600 on $WIREGUARD/$CONFIGD/wg0.conf" +chmod 600 "$WIREGUARD/$CONFIGD/wg0.conf" + +read -r -d '' COMPOSENOTES <<-EOM +\n +=============================================================================== + +Migration seems to have been successful. Do NOT start the WireGuard container +until you have updated WireGuard's service definition: + +Old: + + volumes: + - ./volumes/wireguard:/config + - /lib/modules:/lib/modules:ro + +New: + + volumes: + - ./volumes/wireguard/config:/config + - ./volumes/wireguard/custom-cont-init.d:/custom-cont-init.d + - ./volumes/wireguard/custom-services.d:/custom-services.d + - /lib/modules:/lib/modules:ro + +Pay careful attention to the lines starting with "- ./volumes". Do NOT +just copy and paste the middle two lines. The first line has changed too. + +=============================================================================== +\n +EOM + +# all done - display the happy news +echo -e "$COMPOSENOTES" diff --git a/scripts/backup.sh b/scripts/backup.sh index 6856c1d78..c50895e18 100755 --- a/scripts/backup.sh +++ b/scripts/backup.sh @@ -26,14 +26,14 @@ # This will only produce a backup in the rollowing folder and change all the permissions to the 'pi' user. if [ -d "./menu.sh" ]; then - echo "./menu.sh file was not found. Ensure that you are running this from IOTstack's directory." + echo "./menu.sh file was not found. Ensure that you are running this from IOTstack's directory." exit 1 fi BACKUPTYPE=${1:-"3"} if [[ "$BACKUPTYPE" -ne "1" && "$BACKUPTYPE" -ne "2" && "$BACKUPTYPE" -ne "3" ]]; then - echo "Unknown backup type '$BACKUPTYPE', can only be 1, 2 or 3" + echo "Unknown backup type '$BACKUPTYPE', can only be 1, 2 or 3" exit 1 fi diff --git a/scripts/buildstack_menu.py b/scripts/buildstack_menu.py deleted file mode 100755 index cee54dba1..000000000 --- a/scripts/buildstack_menu.py +++ /dev/null @@ -1,636 +0,0 @@ -#!/usr/bin/env python3 -import signal - -checkedMenuItems = [] -results = {} - -def main(): - import os - import time - import ruamel.yaml - import math - import sys - import subprocess - from deps.chars import specialChars, commonTopBorder, commonBottomBorder, commonEmptyLine, padText - from deps.consts import servicesDirectory, templatesDirectory, volumesDirectory, buildCache, envFile, dockerPathOutput, servicesFileName, composeOverrideFile - from deps.yaml_merge import mergeYaml - from blessed import Terminal - global signal - global renderMode - global term - global paginationSize - global paginationStartIndex - global hideHelpText - global activeMenuLocation - global lastSelection - - yaml = ruamel.yaml.YAML() - yaml.preserve_quotes = True - - # Constants - buildScriptFile = 'build.py' - dockerSavePathOutput = buildCache - - # Runtime vars - menu = [] - dockerComposeServicesYaml = {} - templatesDirectoryFolders = next(os.walk(templatesDirectory))[1] - term = Terminal() - hotzoneLocation = [7, 0] # Top text - paginationToggle = [10, term.height - 22] # Top text + controls text - paginationStartIndex = 0 - paginationSize = paginationToggle[0] - activeMenuLocation = 0 - lastSelection = 0 - - try: # If not already set, then set it. - hideHelpText = hideHelpText - except: - hideHelpText = False - - def buildServices(): # TODO: Move this into a dependency so that it can be executed with just a list of services. - global dockerComposeServicesYaml - try: - runPrebuildHook() - dockerFileYaml = {} - menuStateFileYaml = {} - dockerFileYaml["version"] = "3.6" - dockerFileYaml["services"] = {} - menuStateFileYaml["services"] = {} - dockerFileYaml["services"] = dockerComposeServicesYaml - menuStateFileYaml["services"] = dockerComposeServicesYaml - - if os.path.exists(envFile): - with open(r'%s' % envFile) as fileEnv: - envSettings = yaml.load(fileEnv) - mergedYaml = mergeYaml(envSettings, dockerFileYaml) - dockerFileYaml = mergedYaml - - if os.path.exists(composeOverrideFile): - with open(r'%s' % composeOverrideFile) as fileOverride: - yamlOverride = yaml.load(fileOverride) - - mergedYaml = mergeYaml(yamlOverride, dockerFileYaml) - dockerFileYaml = mergedYaml - - with open(r'%s' % dockerPathOutput, 'w') as outputFile: - yaml.dump(dockerFileYaml, outputFile) - - if not os.path.exists(servicesDirectory): - os.makedirs(servicesDirectory, exist_ok=True) - - with open(r'%s' % dockerSavePathOutput, 'w') as outputFile: - yaml.dump(menuStateFileYaml, outputFile) - runPostBuildHook() - - if os.path.exists('./postbuild.sh'): - servicesList = "" - for (index, serviceName) in enumerate(dockerComposeServicesYaml): - servicesList += " " + serviceName - subprocess.call("./postbuild.sh" + servicesList, shell=True) - - return True - except Exception as err: - print("Issue running build:") - print(err) - input("Press Enter to continue...") - return False - - def generateTemplateList(templatesDirectoryFolders): - templatesDirectoryFolders.sort() - templateListDirectories = [] - for directory in templatesDirectoryFolders: - serviceFilePath = templatesDirectory + '/' + directory + '/' + servicesFileName - if os.path.exists(serviceFilePath): - templateListDirectories.append(directory) - - return templateListDirectories - - def generateLineText(text, textLength=None, paddingBefore=0, lineLength=26): - result = "" - for i in range(paddingBefore): - result += " " - - textPrintableCharactersLength = textLength - - if (textPrintableCharactersLength) == None: - textPrintableCharactersLength = len(text) - - result += text - remainingSpace = lineLength - textPrintableCharactersLength - - for i in range(remainingSpace): - result += " " - - return result - - def renderHotZone(term, renderType, menu, selection, paddingBefore, allIssues): - global paginationSize - optionsLength = len(" >> Options ") - optionsIssuesSpace = len(" ") - selectedTextLength = len("-> ") - spaceAfterissues = len(" ") - issuesLength = len(" !! Issue ") - - print(term.move(hotzoneLocation[0], hotzoneLocation[1])) - - if paginationStartIndex >= 1: - print(term.center("{b} {uaf} {uaf}{uaf}{uaf} {ual} {b}".format( - b=specialChars[renderMode]["borderVertical"], - uaf=specialChars[renderMode]["upArrowFull"], - ual=specialChars[renderMode]["upArrowLine"] - ))) - else: - print(term.center(commonEmptyLine(renderMode))) - - menuItemsActiveRow = term.get_location()[0] - if renderType == 2 or renderType == 1: # Rerender entire hotzone - for (index, menuItem) in enumerate(menu): # Menu loop - if "issues" in menuItem[1] and menuItem[1]["issues"]: - allIssues.append({ "serviceName": menuItem[0], "issues": menuItem[1]["issues"] }) - - if index >= paginationStartIndex and index < paginationStartIndex + paginationSize: - lineText = generateLineText(menuItem[0], paddingBefore=paddingBefore) - - # Menu highlight logic - if index == selection: - activeMenuLocation = term.get_location()[0] - formattedLineText = '-> {t.blue_on_green}{title}{t.normal} <-'.format(t=term, title=menuItem[0]) - paddedLineText = generateLineText(formattedLineText, textLength=len(menuItem[0]) + selectedTextLength, paddingBefore=paddingBefore - selectedTextLength) - toPrint = paddedLineText - else: - toPrint = '{title}{t.normal}'.format(t=term, title=lineText) - # ##### - - # Options and issues - if "buildHooks" in menuItem[1] and "options" in menuItem[1]["buildHooks"] and menuItem[1]["buildHooks"]["options"]: - toPrint = toPrint + '{t.blue_on_black} {raf}{raf} {t.normal}'.format(t=term, raf=specialChars[renderMode]["rightArrowFull"]) - toPrint = toPrint + ' {t.white_on_black} Options {t.normal}'.format(t=term) - else: - for i in range(optionsLength): - toPrint += " " - - for i in range(optionsIssuesSpace): - toPrint += " " - - if "issues" in menuItem[1] and menuItem[1]["issues"]: - toPrint = toPrint + '{t.red_on_orange} !! {t.normal}'.format(t=term) - toPrint = toPrint + ' {t.orange_on_black} Issue {t.normal}'.format(t=term) - else: - if menuItem[1]["checked"]: - if not menuItem[1]["issues"] == None and len(menuItem[1]["issues"]) == 0: - toPrint = toPrint + ' {t.green_on_blue} Pass {t.normal} '.format(t=term) - else: - for i in range(issuesLength): - toPrint += " " - else: - for i in range(issuesLength): - toPrint += " " - - for i in range(spaceAfterissues): - toPrint += " " - # ##### - - # Menu check render logic - if menuItem[1]["checked"]: - toPrint = " (X) " + toPrint - else: - toPrint = " ( ) " + toPrint - - toPrint = "{bv} {toPrint} {bv}".format(bv=specialChars[renderMode]["borderVertical"], toPrint=toPrint) # Generate border - toPrint = term.center(toPrint) # Center Text (All lines should have the same amount of printable characters) - # ##### - print(toPrint) - - - if renderType == 3: # Only partial rerender of hotzone (the unselected menu item, and the newly selected menu item rows) - global lastSelection - global renderOffsetLastSelection - global renderOffsetCurrentSelection - # TODO: Finish this, currently disabled. To enable, update the actions for UP and DOWN array keys below to assigned 3 to needsRender - renderOffsetLastSelection = lastSelection - paginationStartIndex - renderOffsetCurrentSelection = selection - paginationStartIndex - lineText = generateLineText(menu[lastSelection][0], paddingBefore=paddingBefore) - toPrint = '{title}{t.normal}'.format(t=term, title=lineText) - print('{t.move_y(lastSelection)}{title}'.format(t=term, title=toPrint)) - # print(toPrint) - print(renderOffsetCurrentSelection, lastSelection, renderOffsetLastSelection) - lastSelection = selection - - # menuItemsActiveRow - # activeMenuLocation - - - if paginationStartIndex + paginationSize < len(menu): - print(term.center("{b} {daf} {daf}{daf}{daf} {dal} {b}".format( - b=specialChars[renderMode]["borderVertical"], - daf=specialChars[renderMode]["downArrowFull"], - dal=specialChars[renderMode]["downArrowLine"] - ))) - else: - print(term.center(commonEmptyLine(renderMode))) - - def mainRender(menu, selection, renderType = 1): - global paginationStartIndex - global paginationSize - paddingBefore = 4 - - allIssues = [] - - if selection >= paginationStartIndex + paginationSize: - paginationStartIndex = selection - (paginationSize - 1) + 1 - renderType = 1 - - if selection <= paginationStartIndex - 1: - paginationStartIndex = selection - renderType = 1 - - try: - if (renderType == 1): - checkForOptions() - print(term.clear()) - print(term.move_y(7 - hotzoneLocation[0])) - print(term.black_on_cornsilk4(term.center('IOTstack Build Menu'))) - print("") - print(term.center(commonTopBorder(renderMode))) - - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Select containers to build {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - - renderHotZone(term, renderType, menu, selection, paddingBefore, allIssues) - - if (renderType == 1): - print(term.center(commonEmptyLine(renderMode))) - if not hideHelpText: - room = term.height - (28 + len(allIssues) + paginationSize) - if room < 0: - allIssues.append({ "serviceName": "BuildStack Menu", "issues": { "screenSize": 'Not enough scren height to render correctly (t-height = ' + str(term.height) + ' v-lines = ' + str(room) + ')' } }) - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Not enough vertical room to render controls help text ({th}, {rm}) {bv}".format(bv=specialChars[renderMode]["borderVertical"], th=padText(str(term.height), 3), rm=padText(str(room), 3)))) - print(term.center(commonEmptyLine(renderMode))) - else: - print(term.center(commonEmptyLine(renderMode))) - print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Space] to select or deselect image {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Right] for options for containers that support them {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Tab] Expand or collapse build menu size {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - # print(term.center("{bv} [F] Filter options {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Enter] to begin build {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center("{bv} [Escape] to cancel build {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonEmptyLine(renderMode))) - print(term.center(commonBottomBorder(renderMode))) - - if len(allIssues) > 0: - print(term.center("")) - print(term.center("")) - print(term.center("")) - print(term.center(("{btl}{bh}{bh}{bh}{bh}{bh}{bh} Build Issues " - "{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}" - "{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}" - "{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}" - "{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}" - "{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}" - "{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}" - "{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}{bh}" - "{bh}{bh}{bh}{bh}{bh}{bh}{bh}{btr}").format( - btl=specialChars[renderMode]["borderTopLeft"], - btr=specialChars[renderMode]["borderTopRight"], - bh=specialChars[renderMode]["borderHorizontal"] - ))) - print(term.center(commonEmptyLine(renderMode, size = 139))) - for serviceIssues in allIssues: - for index, issue in enumerate(serviceIssues["issues"]): - spacesAndBracketsLen = 5 - issueAndTypeLen = len(issue) + len(serviceIssues["serviceName"]) + spacesAndBracketsLen - serviceNameAndConflictType = '{t.red_on_black}{issueService}{t.normal} ({t.yellow_on_black}{issueType}{t.normal}) '.format(t=term, issueService=serviceIssues["serviceName"], issueType=issue) - formattedServiceNameAndConflictType = generateLineText(str(serviceNameAndConflictType), textLength=issueAndTypeLen, paddingBefore=0, lineLength=32) - issueDescription = generateLineText(str(serviceIssues["issues"][issue]), textLength=len(str(serviceIssues["issues"][issue])), paddingBefore=0, lineLength=103) - print(term.center("{bv} {nm} - {desc} {bv}".format(nm=formattedServiceNameAndConflictType, desc=issueDescription, bv=specialChars[renderMode]["borderVertical"]) )) - print(term.center(commonEmptyLine(renderMode, size = 139))) - print(term.center(commonBottomBorder(renderMode, size = 139))) - - except Exception as err: - print("There was an error rendering the menu:") - print(err) - print("Press [Esc] to go back") - return - - return - - def setCheckedMenuItems(): - global checkedMenuItems - checkedMenuItems.clear() - for (index, menuItem) in enumerate(menu): - if menuItem[1]["checked"]: - checkedMenuItems.append(menuItem[0]) - - def loadAllServices(reload = False): - global dockerComposeServicesYaml - dockerComposeServicesYaml.clear() - for (index, checkedMenuItem) in enumerate(checkedMenuItems): - if reload == False: - if not checkedMenuItem in dockerComposeServicesYaml: - serviceFilePath = templatesDirectory + '/' + checkedMenuItem + '/' + servicesFileName - with open(r'%s' % serviceFilePath) as yamlServiceFile: - dockerComposeServicesYaml[checkedMenuItem] = yaml.load(yamlServiceFile)[checkedMenuItem] - else: - print("reload!") - time.sleep(1) - serviceFilePath = templatesDirectory + '/' + checkedMenuItem + '/' + servicesFileName - with open(r'%s' % serviceFilePath) as yamlServiceFile: - dockerComposeServicesYaml[checkedMenuItem] = yaml.load(yamlServiceFile)[checkedMenuItem] - - return True - - def loadService(serviceName, reload = False): - try: - global dockerComposeServicesYaml - if reload == False: - if not serviceName in dockerComposeServicesYaml: - serviceFilePath = templatesDirectory + '/' + serviceName + '/' + servicesFileName - with open(r'%s' % serviceFilePath) as yamlServiceFile: - dockerComposeServicesYaml[serviceName] = yaml.load(yamlServiceFile)[serviceName] - else: - print("reload!") - time.sleep(1) - servicesFileNamePath = templatesDirectory + '/' + serviceName + '/' + servicesFileName - with open(r'%s' % serviceFilePath) as yamlServiceFile: - dockerComposeServicesYaml[serviceName] = yaml.load(yamlServiceFile)[serviceName] - except Exception as err: - print("Error running build menu:", err) - print("Check the following:") - print("* YAML service name matches the folder name") - print("* Error in YAML file") - print("* YAML file is unreadable") - print("* Buildstack script was modified") - input("Press Enter to exit...") - sys.exit(1) - - return True - - def checkForIssues(): - global dockerComposeServicesYaml - for (index, checkedMenuItem) in enumerate(checkedMenuItems): - buildScriptPath = templatesDirectory + '/' + checkedMenuItem + '/' + buildScriptFile - if os.path.exists(buildScriptPath): - try: - with open(buildScriptPath, "rb") as pythonDynamicImportFile: - code = compile(pythonDynamicImportFile.read(), buildScriptPath, "exec") - execGlobals = { - "dockerComposeServicesYaml": dockerComposeServicesYaml, - "toRun": "checkForRunChecksHook", - "currentServiceName": checkedMenuItem - } - execLocals = locals() - exec(code, execGlobals, execLocals) - if "buildHooks" in execGlobals and "runChecksHook" in execGlobals["buildHooks"] and execGlobals["buildHooks"]["runChecksHook"]: - execGlobals = { - "dockerComposeServicesYaml": dockerComposeServicesYaml, - "toRun": "runChecks", - "currentServiceName": checkedMenuItem - } - execLocals = locals() - try: - exec(code, execGlobals, execLocals) - if "issues" in execGlobals and len(execGlobals["issues"]) > 0: - menu[getMenuItemIndexByService(checkedMenuItem)][1]["issues"] = execGlobals["issues"] - else: - menu[getMenuItemIndexByService(checkedMenuItem)][1]["issues"] = [] - except Exception as err: - print("Error running checkForIssues on '%s'" % checkedMenuItem) - print(err) - input("Press Enter to continue...") - else: - menu[getMenuItemIndexByService(checkedMenuItem)][1]["issues"] = [] - except Exception as err: - print("Error running checkForIssues on '%s'" % checkedMenuItem) - print(err) - input("Press any key to exit...") - sys.exit(1) - - def checkForOptions(): - global dockerComposeServicesYaml - for (index, menuItem) in enumerate(menu): - buildScriptPath = templatesDirectory + '/' + menuItem[0] + '/' + buildScriptFile - if os.path.exists(buildScriptPath): - try: - with open(buildScriptPath, "rb") as pythonDynamicImportFile: - code = compile(pythonDynamicImportFile.read(), buildScriptPath, "exec") - execGlobals = { - "dockerComposeServicesYaml": dockerComposeServicesYaml, - "toRun": "checkForOptionsHook", - "currentServiceName": menuItem[0], - "renderMode": renderMode - } - execLocals = {} - exec(code, execGlobals, execLocals) - if not "buildHooks" in menu[getMenuItemIndexByService(menuItem[0])][1]: - menu[getMenuItemIndexByService(menuItem[0])][1]["buildHooks"] = {} - if "options" in execGlobals["buildHooks"] and execGlobals["buildHooks"]["options"]: - menu[getMenuItemIndexByService(menuItem[0])][1]["buildHooks"]["options"] = True - except Exception as err: - print("Error running checkForOptions on '%s'" % menuItem[0]) - print(err) - input("Press any key to exit...") - sys.exit(1) - - def runPrebuildHook(): - global dockerComposeServicesYaml - for (index, checkedMenuItem) in enumerate(checkedMenuItems): - buildScriptPath = templatesDirectory + '/' + checkedMenuItem + '/' + buildScriptFile - if os.path.exists(buildScriptPath): - with open(buildScriptPath, "rb") as pythonDynamicImportFile: - code = compile(pythonDynamicImportFile.read(), buildScriptPath, "exec") - execGlobals = { - "dockerComposeServicesYaml": dockerComposeServicesYaml, - "toRun": "checkForPreBuildHook", - "currentServiceName": checkedMenuItem - } - execLocals = locals() - try: - exec(code, execGlobals, execLocals) - if "preBuildHook" in execGlobals["buildHooks"] and execGlobals["buildHooks"]["preBuildHook"]: - execGlobals = { - "dockerComposeServicesYaml": dockerComposeServicesYaml, - "toRun": "preBuild", - "currentServiceName": checkedMenuItem - } - execLocals = locals() - exec(code, execGlobals, execLocals) - except Exception as err: - print("Error running PreBuildHook on '%s'" % checkedMenuItem) - print(err) - input("Press Enter to continue...") - try: # If the prebuild hook modified the docker-compose object, pull it from the script back to here. - dockerComposeServicesYaml = execGlobals["dockerComposeServicesYaml"] - except: - pass - - def runPostBuildHook(): - for (index, checkedMenuItem) in enumerate(checkedMenuItems): - buildScriptPath = templatesDirectory + '/' + checkedMenuItem + '/' + buildScriptFile - if os.path.exists(buildScriptPath): - with open(buildScriptPath, "rb") as pythonDynamicImportFile: - code = compile(pythonDynamicImportFile.read(), buildScriptPath, "exec") - execGlobals = { - "dockerComposeServicesYaml": dockerComposeServicesYaml, - "toRun": "checkForPostBuildHook", - "currentServiceName": checkedMenuItem - } - execLocals = locals() - try: - exec(code, execGlobals, execLocals) - if "postBuildHook" in execGlobals["buildHooks"] and execGlobals["buildHooks"]["postBuildHook"]: - execGlobals = { - "dockerComposeServicesYaml": dockerComposeServicesYaml, - "toRun": "postBuild", - "currentServiceName": checkedMenuItem - } - execLocals = locals() - exec(code, execGlobals, execLocals) - except Exception as err: - print("Error running PostBuildHook on '%s'" % checkedMenuItem) - print(err) - input("Press Enter to continue...") - - def executeServiceOptions(): - global dockerComposeServicesYaml - menuItem = menu[selection] - if menu[selection][1]["checked"] and "buildHooks" in menuItem[1] and "options" in menuItem[1]["buildHooks"] and menuItem[1]["buildHooks"]["options"]: - buildScriptPath = templatesDirectory + '/' + menuItem[0] + '/' + buildScriptFile - if os.path.exists(buildScriptPath): - with open(buildScriptPath, "rb") as pythonDynamicImportFile: - code = compile(pythonDynamicImportFile.read(), buildScriptPath, "exec") - - execGlobals = { - "dockerComposeServicesYaml": dockerComposeServicesYaml, - "toRun": "runOptionsMenu", - "currentServiceName": menuItem[0], - "renderMode": renderMode - } - execLocals = locals() - exec(code, execGlobals, execLocals) - dockerComposeServicesYaml = execGlobals["dockerComposeServicesYaml"] - checkForIssues() - mainRender(menu, selection, 1) - - def getMenuItemIndexByService(serviceName): - for (index, menuItem) in enumerate(menu): - if (menuItem[0] == serviceName): - return index - - def checkMenuItem(selection): - global dockerComposeServicesYaml - if menu[selection][1]["checked"] == True: - menu[selection][1]["checked"] = False - menu[selection][1]["issues"] = None - del dockerComposeServicesYaml[menu[selection][0]] - else: - menu[selection][1]["checked"] = True - print(menu[selection][0]) - loadService(menu[selection][0]) - - def prepareMenuState(): - global dockerComposeServicesYaml - for (index, serviceName) in enumerate(dockerComposeServicesYaml): - checkMenuItem(getMenuItemIndexByService(serviceName)) - setCheckedMenuItems() - checkForIssues() - - return True - - def loadCurrentConfigs(templatesList): - global dockerComposeServicesYaml - if os.path.exists(dockerSavePathOutput): - print("Loading config fom: '%s'" % dockerSavePathOutput) - with open(r'%s' % dockerSavePathOutput) as fileSavedConfigs: - previousConfigs = yaml.load(fileSavedConfigs) - if not previousConfigs == None: - if "services" in previousConfigs: - dockerComposeServicesYaml = {} - for (index, serviceName) in enumerate(previousConfigs["services"]): - if serviceName in templatesList: # This ensures every service loaded has a template directory - dockerComposeServicesYaml[serviceName] = previousConfigs["services"][serviceName] - return True - dockerComposeServicesYaml = {} - return False - - def onResize(sig, action): - global paginationToggle - paginationToggle = [10, term.height - 25] - mainRender(menu, selection, 1) - - templatesList = generateTemplateList(templatesDirectoryFolders) - for directory in templatesList: - menu.append([directory, { "checked": False, "issues": None }]) - - if __name__ == 'builtins': - global results - global signal - needsRender = 1 - signal.signal(signal.SIGWINCH, onResize) - with term.fullscreen(): - print('Loading...') - selection = 0 - if loadCurrentConfigs(templatesList): - prepareMenuState() - mainRender(menu, selection, 1) - selectionInProgress = True - with term.cbreak(): - while selectionInProgress: - key = term.inkey(esc_delay=0.05) - if key.is_sequence: - if key.name == 'KEY_TAB': - needsRender = 1 - if paginationSize == paginationToggle[0]: - paginationSize = paginationToggle[1] - paginationStartIndex = 0 - else: - paginationSize = paginationToggle[0] - if key.name == 'KEY_DOWN': - selection += 1 - needsRender = 2 - if key.name == 'KEY_UP': - selection -= 1 - needsRender = 2 - if key.name == 'KEY_RIGHT': - executeServiceOptions() - if key.name == 'KEY_ENTER': - setCheckedMenuItems() - checkForIssues() - selectionInProgress = False - results["buildState"] = buildServices() - return results["buildState"] - if key.name == 'KEY_ESCAPE': - results["buildState"] = False - return results["buildState"] - elif key: - if key == ' ': # Space pressed - checkMenuItem(selection) # Update checked list - setCheckedMenuItems() # Update UI memory - checkForIssues() - needsRender = 1 - elif key == 'h': # H pressed - if hideHelpText: - hideHelpText = False - else: - hideHelpText = True - needsRender = 1 - else: - print(key) - time.sleep(0.5) - - selection = selection % len(menu) - - mainRender(menu, selection, needsRender) - -originalSignalHandler = signal.getsignal(signal.SIGINT) -main() -signal.signal(signal.SIGWINCH, originalSignalHandler) diff --git a/scripts/deps/__pycache__/__init__.cpython-36.pyc b/scripts/deps/__pycache__/__init__.cpython-36.pyc deleted file mode 100644 index e6d825d1d..000000000 Binary files a/scripts/deps/__pycache__/__init__.cpython-36.pyc and /dev/null differ diff --git a/scripts/deps/__pycache__/chars.cpython-36.pyc b/scripts/deps/__pycache__/chars.cpython-36.pyc deleted file mode 100644 index 768c81657..000000000 Binary files a/scripts/deps/__pycache__/chars.cpython-36.pyc and /dev/null differ diff --git a/scripts/deps/__pycache__/common_functions.cpython-36.pyc b/scripts/deps/__pycache__/common_functions.cpython-36.pyc deleted file mode 100644 index 0256a061e..000000000 Binary files a/scripts/deps/__pycache__/common_functions.cpython-36.pyc and /dev/null differ diff --git a/scripts/deps/__pycache__/consts.cpython-36.pyc b/scripts/deps/__pycache__/consts.cpython-36.pyc deleted file mode 100644 index 2d73746ef..000000000 Binary files a/scripts/deps/__pycache__/consts.cpython-36.pyc and /dev/null differ diff --git a/scripts/deps/__pycache__/version_check.cpython-36.pyc b/scripts/deps/__pycache__/version_check.cpython-36.pyc deleted file mode 100644 index 9ad47fa25..000000000 Binary files a/scripts/deps/__pycache__/version_check.cpython-36.pyc and /dev/null differ diff --git a/scripts/deps/buildstack.py b/scripts/deps/buildstack.py deleted file mode 100755 index 5cb2777f1..000000000 --- a/scripts/deps/buildstack.py +++ /dev/null @@ -1,106 +0,0 @@ -import os -import ruamel.yaml -import math -import sys -from deps.yaml_merge import mergeYaml -from deps.consts import servicesDirectory, templatesDirectory, volumesDirectory, buildCache, envFile, dockerPathOutput, servicesFileName, composeOverrideFile - -yaml = ruamel.yaml.YAML() -yaml.preserve_quotes = True - -buildScriptFile = 'build.py' - -def buildServices(dockerComposeServicesYaml): - try: - runPrebuildHook() - dockerFileYaml = {} - menuStateFileYaml = {} - dockerFileYaml["version"] = "3.6" - dockerFileYaml["services"] = {} - menuStateFileYaml["services"] = {} - dockerFileYaml["services"] = dockerComposeServicesYaml - menuStateFileYaml["services"] = dockerComposeServicesYaml - - if os.path.exists(envFile): - with open(r'%s' % envFile) as fileEnv: - envSettings = yaml.load(fileEnv) - mergedYaml = mergeYaml(envSettings, dockerFileYaml) - dockerFileYaml = mergedYaml - - if os.path.exists(composeOverrideFile): - with open(r'%s' % composeOverrideFile) as fileOverride: - yamlOverride = yaml.load(fileOverride) - - mergedYaml = mergeYaml(yamlOverride, dockerFileYaml) - dockerFileYaml = mergedYaml - - with open(r'%s' % dockerPathOutput, 'w') as outputFile: - yaml.dump(dockerFileYaml, outputFile, explicit_start=True, default_style='"') - - with open(r'%s' % buildCache, 'w') as outputFile: - yaml.dump(menuStateFileYaml, outputFile, explicit_start=True, default_style='"') - runPostBuildHook() - return True - except Exception as err: - print("Issue running build:") - print(err) - input("Press Enter to continue...") - return False - -def runPrebuildHook(dockerComposeServicesYaml): - for (index, checkedMenuItem) in enumerate(checkedMenuItems): - buildScriptPath = templatesDirectory + '/' + checkedMenuItem + '/' + buildScriptFile - if os.path.exists(buildScriptPath): - with open(buildScriptPath, "rb") as pythonDynamicImportFile: - code = compile(pythonDynamicImportFile.read(), buildScriptPath, "exec") - execGlobals = { - "dockerComposeServicesYaml": dockerComposeServicesYaml, - "toRun": "checkForPreBuildHook", - "currentServiceName": checkedMenuItem - } - execLocals = locals() - try: - exec(code, execGlobals, execLocals) - if "preBuildHook" in execGlobals["buildHooks"] and execGlobals["buildHooks"]["preBuildHook"]: - execGlobals = { - "dockerComposeServicesYaml": dockerComposeServicesYaml, - "toRun": "preBuild", - "currentServiceName": checkedMenuItem - } - execLocals = locals() - exec(code, execGlobals, execLocals) - except Exception as err: - print("Error running PreBuildHook on '%s'" % checkedMenuItem) - print(err) - input("Press Enter to continue...") - try: # If the prebuild hook modified the docker-compose object, pull it from the script back to here. - dockerComposeServicesYaml = execGlobals["dockerComposeServicesYaml"] - except: - pass - -def runPostBuildHook(): - for (index, checkedMenuItem) in enumerate(checkedMenuItems): - buildScriptPath = templatesDirectory + '/' + checkedMenuItem + '/' + buildScriptFile - if os.path.exists(buildScriptPath): - with open(buildScriptPath, "rb") as pythonDynamicImportFile: - code = compile(pythonDynamicImportFile.read(), buildScriptPath, "exec") - execGlobals = { - "dockerComposeServicesYaml": dockerComposeServicesYaml, - "toRun": "checkForPostBuildHook", - "currentServiceName": checkedMenuItem - } - execLocals = locals() - try: - exec(code, execGlobals, execLocals) - if "postBuildHook" in execGlobals["buildHooks"] and execGlobals["buildHooks"]["postBuildHook"]: - execGlobals = { - "dockerComposeServicesYaml": dockerComposeServicesYaml, - "toRun": "postBuild", - "currentServiceName": checkedMenuItem - } - execLocals = locals() - exec(code, execGlobals, execLocals) - except Exception as err: - print("Error running PostBuildHook on '%s'" % checkedMenuItem) - print(err) - input("Press Enter to continue...") diff --git a/scripts/deps/common_functions.py b/scripts/deps/common_functions.py deleted file mode 100755 index b4309df48..000000000 --- a/scripts/deps/common_functions.py +++ /dev/null @@ -1,190 +0,0 @@ -import time -import string -import random -import sys -import os -import subprocess -from deps.consts import ifCheckList - -def generateRandomString(size = 0, chars = string.ascii_uppercase + string.ascii_lowercase + string.digits): - if size == 0: - size = random.randint(16, 24) - return ''.join(random.choice(chars) for _ in range(size)) - -def getNetworkDetails(inputList = None): - ifList = inputList - if (inputList == None): - ifList = ifCheckList - - results = { - "name": "", - "mac": "", - "ip": "" - } - - for (index, ifName) in enumerate(ifList): - try: - ip = getIpAddress(ifName) - mac = getMacAddress(ifName) - results["name"] = ifName - results["ip"] = ip - results["mac"] = mac - if (results["ip"] == "" or results["mac"] == ""): - continue - break - except: - continue - # pass - - return results - -def getMacAddress(ifName = None): - if (ifName == None): - print("getMacAddress: Need interface name") - return "" - - mac = "" - - if sys.platform == 'win32': - print("getMacAddress: Linux support only") - else: - FNULL = open(os.devnull, 'w') - ipRes = subprocess.Popen("/sbin/ifconfig %s" % ifName, shell=True, stdout=subprocess.PIPE, stderr=FNULL).communicate() - for line in ipRes[0].decode('utf-8').splitlines(): - if line.find('Ethernet') > -1: - mac = line.split()[1] - break - return mac - -def getIpAddress(ifName = None): - if (ifName == None): - print("getIpAddress: Need interface name") - return "" - - ip = "" - - if sys.platform == 'win32': - print("getIpAddress: Linux support only") - else: - FNULL = open(os.devnull, 'w') - ipRes = subprocess.Popen("/sbin/ifconfig %s" % ifName, shell=True, stdout=subprocess.PIPE, stderr=FNULL).communicate() - for line in ipRes[0].decode('utf-8').splitlines(): - if line.find('inet') > -1: - ip = line.split()[1] - break - return ip - -def getExternalPorts(serviceName, dockerComposeServicesYaml): - externalPorts = [] - try: - yamlService = dockerComposeServicesYaml[serviceName] - if "ports" in yamlService: - for (index, port) in enumerate(yamlService["ports"]): - try: - externalAndInternal = port.split(":") - externalPorts.append(externalAndInternal[0]) - except: - pass - except: - pass - return externalPorts - -def getInternalPorts(serviceName, dockerComposeServicesYaml): - externalPorts = [] - try: - yamlService = dockerComposeServicesYaml[serviceName] - if "ports" in yamlService: - for (index, port) in enumerate(yamlService["ports"]): - try: - externalAndInternal = port.split(":") - externalPorts.append(externalAndInternal[1]) - except: - pass - except: - pass - return externalPorts - -def checkPortConflicts(serviceName, currentPorts, dockerComposeServicesYaml): - portConflicts = [] - yamlService = dockerComposeServicesYaml[serviceName] - servicePorts = getExternalPorts(serviceName, dockerComposeServicesYaml) - for (index, servicePort) in enumerate(servicePorts): - for (index, currentPort) in enumerate(currentPorts): - if (servicePort == currentPort): - portConflicts.append([servicePort, serviceName]) - return portConflicts - -def checkDependsOn(serviceName, dockerComposeServicesYaml): - missingServices = [] - yamlService = dockerComposeServicesYaml[serviceName] - if "depends_on" in yamlService: - for (index, dependsOnName) in enumerate(yamlService["depends_on"]): - if not dependsOnName in dockerComposeServicesYaml: - missingServices.append([dependsOnName, serviceName]) - return missingServices - -def enterPortNumber(term, dockerComposeServicesYaml, currentServiceName, hotzoneLocation, createMenuFn): - newPortNumber = "" - try: - print(term.move_y(hotzoneLocation[0])) - print(term.center(" ")) - print(term.center(" ")) - print(term.center(" ")) - print(term.move_y(hotzoneLocation[0] + 1)) - time.sleep(0.1) # Prevent loop - newPortNumber = input(term.center("Enter new port number: ")) - # newPortNumber = sys.stdin.readline() - time.sleep(0.1) # Prevent loop - newPortNumber = int(str(newPortNumber)) - if 1 <= newPortNumber <= 65535: - time.sleep(0.2) # Prevent loop - internalPort = getInternalPorts(currentServiceName, dockerComposeServicesYaml)[0] - dockerComposeServicesYaml[currentServiceName]["ports"][0] = "{newExtPort}:{oldIntPort}".format( - newExtPort = newPortNumber, - oldIntPort = internalPort - ) - createMenuFn() - return True - else: - print(term.center(' {t.white_on_red} "{port}" {message} {t.normal} <-'.format(t=term, port=newPortNumber, message="is not a valid port"))) - time.sleep(2) # Give time to read error - return False - except Exception as err: - print(term.center(' {t.white_on_red} "{port}" {message} {t.normal} <-'.format(t=term, port=newPortNumber, message="is not a valid port"))) - print(term.center(' {t.white_on_red} Error: {errorMsg} {t.normal} <-'.format(t=term, errorMsg=err))) - time.sleep(2.5) # Give time to read error - return False - -def enterPortNumberWithWhiptail(term, dockerComposeServicesYaml, currentServiceName, hotzoneLocation, defaultPort): - newPortNumber = "" - try: - portProcess = subprocess.Popen(['./scripts/deps/portWhiptail.sh', defaultPort, currentServiceName], stdout=subprocess.PIPE) - portResult = portProcess.communicate()[0] - portResult = portResult.decode("utf-8").split(",") - newPortNumber = portResult[0] - returnCode = portResult[1] - time.sleep(0.1) # Prevent loop - - if not returnCode == "0": - return -1 - - newPortNumber = int(str(newPortNumber)) - if 1 <= newPortNumber <= 65535: - time.sleep(0.2) # Prevent loop - return newPortNumber - else: - print(term.center(' {t.white_on_red} "{port}" {message} {t.normal} <-'.format(t=term, port=newPortNumber, message="is not a valid port"))) - time.sleep(2) # Give time to read error - return -1 - except Exception as err: - print(term.center(' {t.white_on_red} "{port}" {message} {t.normal} <-'.format(t=term, port=newPortNumber, message="is not a valid port"))) - print(term.center(' {t.white_on_red} Error: {errorMsg} {t.normal} <-'.format(t=term, errorMsg=err))) - time.sleep(2.5) # Give time to read error - return -1 - -def literalPresenter(dumper, data): - if isinstance(data, str) and "\n" in data: - return dumper.represent_scalar('tag:yaml.org,2002:str', data, style='|') - # if isinstance(data, None): - # return self.represent_scalar('tag:yaml.org,2002:null', u'') - return dumper.represent_scalar('tag:yaml.org,2002:str', data, style='"') \ No newline at end of file diff --git a/scripts/deps/consts.py b/scripts/deps/consts.py deleted file mode 100755 index 9e055cab8..000000000 --- a/scripts/deps/consts.py +++ /dev/null @@ -1,12 +0,0 @@ -servicesDirectory = './services/' -templatesDirectory = './.templates/' -volumesDirectory = './volumes/' -tempDirectory = './.tmp/' -scriptsDirectory = './scripts/' -buildSettingsFileName = '/build_settings.yml' -buildCache = servicesDirectory + 'docker-compose.save.yml' -composeOverrideFile = './compose-override.yml' -envFile = templatesDirectory + 'env.yml' -dockerPathOutput = './docker-compose.yml' -servicesFileName = 'service.yml' -ifCheckList = ['eth0', 'wlan0'] diff --git a/scripts/deps/version_check.py b/scripts/deps/version_check.py deleted file mode 100755 index aa3a272e1..000000000 --- a/scripts/deps/version_check.py +++ /dev/null @@ -1,36 +0,0 @@ -def checkVersion(requiredVersion, currentVersion): - requiredSplit = requiredVersion.split('.') - - if len(requiredSplit) < 2: - return False, 'Invalid Required Version', requiredVersion - - try: - requiredMajor = int(requiredSplit[0]) - requiredMinor = int(requiredSplit[1]) - requiredBuild = int(requiredSplit[2]) - except: - return False, 'Invalid Required Version', requiredVersion - - currentSplit = currentVersion.split('.') - - if len(currentSplit) < 2: - return False, 'Invalid Current Version', currentVersion - - try: - currentMajor = int(currentSplit[0]) - currentMinor = int(currentSplit[1]) - currentBuild = currentSplit[2].split("-")[0] - currentBuild = int(currentBuild) - except: - return False, 'Invalid Current Version', currentVersion - - if currentMajor > requiredMajor: - return True, '', [] - - if currentMajor == requiredMajor and currentMajor > requiredMinor: - return True, '', [] - - if currentMajor == requiredMajor and currentMinor == requiredMinor and currentBuild >= requiredBuild: - return True, '', [] - - return False, 'Version Check Fail', [currentMajor == requiredMajor, currentMinor == requiredMinor, currentBuild >= requiredBuild] \ No newline at end of file diff --git a/scripts/deps/yaml_merge.py b/scripts/deps/yaml_merge.py deleted file mode 100755 index fa953a337..000000000 --- a/scripts/deps/yaml_merge.py +++ /dev/null @@ -1,17 +0,0 @@ - -def mergeYaml(priorityYaml, defaultYaml): - finalYaml = {} - if isinstance(defaultYaml, dict): - for dk, dv in defaultYaml.items(): - if dk in priorityYaml: - finalYaml[dk] = mergeYaml(priorityYaml[dk], dv) - else: - finalYaml[dk] = dv - for pk, pv in priorityYaml.items(): - if pk in finalYaml: - finalYaml[pk] = mergeYaml(finalYaml[pk], pv) - else: - finalYaml[pk] = pv - else: - finalYaml = defaultYaml - return finalYaml diff --git a/scripts/host_installers/duck.sh b/scripts/host_installers/duck.sh new file mode 100644 index 000000000..37852a956 --- /dev/null +++ b/scripts/host_installers/duck.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +# Your DuckDNS domain (or comma-separated list of DuckDNS domains if you +# have multiple domains associated with the same IP address). +DOMAINS="YOURS.duckdns.org" + +# Your DuckDNS Token +DUCKDNS_TOKEN="YOUR_DUCKDNS_TOKEN" + +# is this script running in the foreground or background? +if [ "$(tty)" = "not a tty" ] ; then + + # background! Assume launched by cron. Add a random delay to avoid + # every client contacting DuckDNS at exactly the same moment. + sleep $((RANDOM % 60)) + +fi + +# mark the event in case this is being logged. +echo "$(date "+%a, %d %b %Y %H:%M:%S %z") - updating DuckDNS" + +# Request duckdns to update your domain name with your public IP address +curl --max-time 10 \ + "https://www.duckdns.org/update?domains=${DOMAINS}&token=${DUCKDNS_TOKEN}&ip=" + +# curl does not append newline so fix that +echo "" diff --git a/scripts/host_installers/hassio_supervisor.sh b/scripts/host_installers/hassio_supervisor.sh new file mode 100644 index 000000000..deac68497 --- /dev/null +++ b/scripts/host_installers/hassio_supervisor.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +echo " " +echo "Ensure that you have read the documentation on installing Hass.io before continuing." +echo "Not following the installation instructions may render you system to be unable to connect to the internet." +echo "Hass.io Documentation: " +echo " https://sensorsiot.github.io/IOTstack/Containers/Home-Assistant/" + +echo " " +sleep 1 + +read -r -n 1 -p "Press Y to continue, any other key to cancel " response; + +if [[ $response == "y" || $response == "Y" ]]; then + echo "Install requirements for Hass.io" + sudo apt install -y bash jq curl avahi-daemon dbus + hassio_machine=$(whiptail --title "Machine type" --menu \ + "Please select you device type" 20 78 12 -- \ + "raspberrypi4-64" " " \ + "raspberrypi4" " " \ + "raspberrypi3-64" " " \ + "raspberrypi3" " " \ + "raspberrypi2" " " \ + "qemux86" " " \ + "qemux86-64" " " \ + "qemuarm" " " \ + "qemuarm-64" " " \ + "orangepi-prime" " " \ + "odroid-xu" " " \ + "odroid-c2" " " \ + "intel-nuc" " " \ + "tinker" " " \ + 3>&1 1>&2 2>&3) + + if [ -n "$hassio_machine" ]; then + sudo systemctl disable ModemManager + sudo systemctl stop ModemManager + curl -sL "https://raw.githubusercontent.com/Kanga-Who/home-assistant/master/supervised-installer.sh" | sudo bash -s -- -m $hassio_machine + clear + exit 0 + else + clear + echo "No selection" + exit 4 + fi + clear + exit 3 +else + clear + exit 5 +fi \ No newline at end of file diff --git a/scripts/install_docker.sh b/scripts/host_installers/install_docker.sh old mode 100755 new mode 100644 similarity index 69% rename from scripts/install_docker.sh rename to scripts/host_installers/install_docker.sh index 046c2a048..4e9033e69 --- a/scripts/install_docker.sh +++ b/scripts/host_installers/install_docker.sh @@ -10,25 +10,25 @@ if [ "$EUID" -ne 0 ] exit fi function command_exists() { - command -v "$@" > /dev/null 2>&1 + command -v "$@" > /dev/null 2>&1 } if [ "$1" == "install" ]; then RESTART_REQUIRED="false" if command_exists docker; then - echo "Docker already installed" >&2 + echo "Docker already installed" >&1 else - echo "Install Docker" >&2 + echo "Install Docker" >&1 curl -fsSL https://get.docker.com | sh RESTART_REQUIRED="true" sudo usermod -aG docker $USER fi if command_exists docker-compose; then - echo "docker-compose already installed" >&2 + echo "docker-compose already installed" >&1 else RESTART_REQUIRED="true" - echo "Install docker-compose" >&2 + echo "Install docker-compose" >&1 sudo apt install -y docker-compose sudo usermod -aG docker $USER fi @@ -43,9 +43,9 @@ fi if [ "$1" == "upgrade" ]; then sudo apt upgrade docker docker-compose - if [ $? -eq 0 ]; then - if (whiptail --title "Restart Required" --yesno "It is recommended that you restart your device now. Select yes to do so now" 20 78); then - reboot - fi - fi + if [ $? -eq 0 ]; then + if (whiptail --title "Restart Required" --yesno "It is recommended that you restart your device now. Select yes to do so now" 20 78); then + reboot + fi + fi fi diff --git a/scripts/install_log2ram.sh b/scripts/host_installers/install_log2ram.sh old mode 100755 new mode 100644 similarity index 100% rename from scripts/install_log2ram.sh rename to scripts/host_installers/install_log2ram.sh diff --git a/scripts/install_ssh_keys.sh b/scripts/host_installers/install_ssh_keys.sh old mode 100755 new mode 100644 similarity index 93% rename from scripts/install_ssh_keys.sh rename to scripts/host_installers/install_ssh_keys.sh index 49c6c9fdd..9eda38ece --- a/scripts/install_ssh_keys.sh +++ b/scripts/host_installers/install_ssh_keys.sh @@ -115,16 +115,16 @@ KEYS_ADDED=0 KEYS_SKIPPED=0 if [[ "$SSH_KEYS" == "Not Found" ]]; then - >&2 echo "Username '$GH_USERNAME' not found" - >&2 echo "URL: 'https://github.com/$GH_USERNAME.keys'" + >&1 echo "Username '$GH_USERNAME' not found" + >&1 echo "URL: 'https://github.com/$GH_USERNAME.keys'" exit 1 fi if [[ ${#SSH_KEYS} -le 16 ]]; then - >&2 echo "Something went wrong retrieving SSH keys for '$GH_USERNAME'" - >&2 echo "URL: 'https://github.com/$GH_USERNAME.keys'" - >&2 echo "Result: " - >&2 echo "$SSH_KEYS" + >&1 echo "Something went wrong retrieving SSH keys for '$GH_USERNAME'" + >&1 echo "URL: 'https://github.com/$GH_USERNAME.keys'" + >&1 echo "Result: " + >&1 echo "$SSH_KEYS" exit 2 fi diff --git a/.native/rpieasy.sh b/scripts/host_installers/rpieasy.sh similarity index 100% rename from .native/rpieasy.sh rename to scripts/host_installers/rpieasy.sh diff --git a/.native/rtl_433.sh b/scripts/host_installers/rtl_433.sh old mode 100755 new mode 100644 similarity index 88% rename from .native/rtl_433.sh rename to scripts/host_installers/rtl_433.sh index 5e42a6d78..a5dc67bd5 --- a/.native/rtl_433.sh +++ b/scripts/host_installers/rtl_433.sh @@ -7,12 +7,12 @@ sudo touch /etc/modprobe.d/blacklist-rtl8xxxu.conf sudo apt-get update sudo apt-get install -y libtool \ - libusb-1.0.0-dev \ - librtlsdr-dev \ - rtl-sdr \ - doxygen \ - cmake \ - automake + libusb-1.0.0-dev \ + librtlsdr-dev \ + rtl-sdr \ + doxygen \ + cmake \ + automake git clone https://github.com/merbanan/rtl_433.git ~/rtl_433 cd ~/rtl_433/ diff --git a/scripts/nodered_version_check.sh b/scripts/nodered_version_check.sh new file mode 100755 index 000000000..761c76668 --- /dev/null +++ b/scripts/nodered_version_check.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash + +# the name of this script is +SCRIPT=$(basename "$0") + +# default image is +DEFAULTIMAGE="iotstack-nodered:latest" + +# zero or one arguments supported +if [ "$#" -gt 1 ]; then + echo "Usage: $SCRIPT {image:tag}" + echo " eg: $SCRIPT $DEFAULTIMAGE" + exit -1 +fi + +# image can be passed as first argument, else default +IMAGE=${1:-"$DEFAULTIMAGE"} + +# fetch latest version details from GitHub +LATEST=$(wget -O - -q https://raw.githubusercontent.com/node-red/node-red-docker/master/package.json | jq -r .version) + +# figure out the version in the local image +INSTALLED=$(docker image inspect "$IMAGE" | jq -r .[0].Config.Labels[\"org.label-schema.version\"]) + +# compare versions and report result +if [ "$INSTALLED" = "$LATEST" ] ; then + + echo "Node-Red is up-to-date (version $INSTALLED)" + +else + +/bin/cat <<-COLLECT_TEXT + + ==================================================================== + Node-Red version number has changed on GitHub: + + Local Version: $INSTALLED + GitHub Version: $LATEST + + This means a new version MIGHT be available on Dockerhub. Check here: + + https://hub.docker.com/r/nodered/node-red/tags?page=1&ordering=last_updated + + When an updated version is actually avaliable, proceed like this: + + $ REBUILD nodered + $ UP nodered + $ docker system prune + ==================================================================== + +COLLECT_TEXT + +fi + diff --git a/scripts/restore.sh b/scripts/restore.sh index 02c9e8a9c..3d656215d 100755 --- a/scripts/restore.sh +++ b/scripts/restore.sh @@ -73,7 +73,7 @@ sudo rm -rf ./post_restore.sh >> $LOGFILE 2>&1 sudo rm -rf ./post_restore.sh >> $LOGFILE 2>&1 sudo tar -zxvf \ - $RESTOREFILE >> $LOGFILE 2>&1 + $RESTOREFILE >> $LOGFILE 2>&1 echo "" >> $LOGFILE diff --git a/scripts/setup_iotstack.sh b/scripts/setup_iotstack.sh new file mode 100644 index 000000000..aa0ea7a78 --- /dev/null +++ b/scripts/setup_iotstack.sh @@ -0,0 +1,246 @@ +#!/bin/bash + +# Minimum Software Versions +REQ_DOCKER_VERSION=18.2.0 + +# Required to generate and install a ssh key so menu containers can securely execute commands on host +AUTH_KEYS_FILE=~/.ssh/authorized_keys +CONTAINER_KEYS_FILE=./.internal/.ssh/id_rsa + +function command_exists() { + command -v "$@" > /dev/null 2>&1 +} + +function minimum_version_check() { + # Usage: minimum_version_check required_version current_major current_minor current_build + # Example: minimum_version_check "1.2.3" 1 2 3 + REQ_MIN_VERSION_MAJOR=$(echo "$1"| cut -d' ' -f 2 | cut -d'.' -f 1) + REQ_MIN_VERSION_MINOR=$(echo "$1"| cut -d' ' -f 2 | cut -d'.' -f 2) + REQ_MIN_VERSION_BUILD=$(echo "$1"| cut -d' ' -f 2 | cut -d'.' -f 3) + + CURR_VERSION_MAJOR=$2 + CURR_VERSION_MINOR=$3 + CURR_VERSION_BUILD=$4 + + VERSION_GOOD="Unknown" + + if [ -z "$CURR_VERSION_MAJOR" ]; then + echo "$VERSION_GOOD" + return 1 + fi + + if [ -z "$CURR_VERSION_MINOR" ]; then + echo "$VERSION_GOOD" + return 1 + fi + + if [ -z "$CURR_VERSION_BUILD" ]; then + echo "$VERSION_GOOD" + return 1 + fi + + if [ "${CURR_VERSION_MAJOR}" -ge $REQ_MIN_VERSION_MAJOR ]; then + VERSION_GOOD="true" + echo "$VERSION_GOOD" + return 0 + else + VERSION_GOOD="false" + fi + + if [ "${CURR_VERSION_MAJOR}" -ge $REQ_MIN_VERSION_MAJOR ] && \ + [ "${CURR_VERSION_MINOR}" -ge $REQ_MIN_VERSION_MINOR ]; then + VERSION_GOOD="true" + echo "$VERSION_GOOD" + return 0 + else + VERSION_GOOD="false" + fi + + if [ "${CURR_VERSION_MAJOR}" -ge $REQ_MIN_VERSION_MAJOR ] && \ + [ "${CURR_VERSION_MINOR}" -ge $REQ_MIN_VERSION_MINOR ] && \ + [ "${CURR_VERSION_BUILD}" -ge $REQ_MIN_VERSION_BUILD ]; then + VERSION_GOOD="true" + echo "$VERSION_GOOD" + return 0 + else + VERSION_GOOD="false" + fi + + echo "$VERSION_GOOD" +} + +function user_in_group() +{ + if grep -q $1 /etc/group ; then + if id -nGz "$USER" | grep -qzxF "$1"; then + echo "true" + else + echo "false" + fi + else + echo "notgroup" + fi +} + +function install_docker() { + if command_exists docker; then + echo "Docker already installed" >&1 + else + echo "Install Docker" >&1 + curl -fsSL https://get.docker.com | sh + sudo -E usermod -aG docker $USER + fi + + if command_exists docker-compose; then + echo "docker-compose already installed" >&1 + else + echo "Install docker-compose" >&1 + sudo -E apt install -y docker-compose + fi + + echo "" >&1 + echo "You should now restart your system" >&1 +} + +function docker_check() { + DOCKER_GOOD="fail" + + if command_exists docker; then + echo "Docker is installed" >&1 + DOCKER_VERSION=$(docker version -f "{{.Server.Version}}" 2>&1) + + if [[ "$DOCKER_VERSION" == *"Cannot connect to the Docker daemon"* ]]; then + echo "Error getting docker version. Error when connecting to docker daemon. Check that docker is running." >&1 + if (whiptail --title "Docker and Docker-Compose" --yesno "Error getting docker version. Error when connecting to docker daemon. Check that docker is running.\n\nCommand: docker version -f \"{{.Server.Version}}\"\n\nExit?" 20 78 >&1); then + exit 1 + fi + elif [[ "$DOCKER_VERSION" == *" permission denied"* ]]; then + echo "Error getting docker version. Received permission denied error. Try running with: ./menu.sh --run-env-setup" >&1 + if (whiptail --title "Docker and Docker-Compose" --yesno "Error getting docker version. Received permission denied error.\n\nTry rerunning the menu with: ./menu.sh --run-env-setup\n\nExit?" 20 78 >&1); then + exit 1 + fi + fi + + if [[ -z "$DOCKER_VERSION" ]]; then + echo "Error getting docker version. Error when running docker command. Check that docker is installed correctly." >&1 + fi + + DOCKER_VERSION_MAJOR=$(echo "$DOCKER_VERSION"| cut -d'.' -f 1) + DOCKER_VERSION_MINOR=$(echo "$DOCKER_VERSION"| cut -d'.' -f 2) + + DOCKER_VERSION_BUILD=$(echo "$DOCKER_VERSION"| cut -d'.' -f 3) + DOCKER_VERSION_BUILD=$(echo "$DOCKER_VERSION_BUILD"| cut -f1 -d"-") + + if [[ "$(minimum_version_check $REQ_DOCKER_VERSION $DOCKER_VERSION_MAJOR $DOCKER_VERSION_MINOR $DOCKER_VERSION_BUILD )" == "true" ]]; then + [ -f .ignore_docker_outofdate ] && rm .ignore_docker_outofdate + DOCKER_GOOD="true" + echo "Docker version $DOCKER_VERSION >= $REQ_DOCKER_VERSION. Docker is good to go." >&1 + else + DOCKER_GOOD="outdated" + if [ ! -f .ignore_docker_outofdate ]; then + if (whiptail --title "Docker and Docker-Compose Version Issue" --yesno "Docker version is currently $DOCKER_VERSION which is less than $REQ_DOCKER_VERSION consider upgrading or you may experience issues. You will not be prompted again. You can manually upgrade by typing:\n sudo apt upgrade docker docker-compose\n\nAttempt to upgrade now?" 20 78 >&1); then + update_docker + else + touch .ignore_docker_outofdate + fi + fi + fi + fi + + if command_exists docker-compose; then + COMPOSE_GOOD="pass" + echo "docker-compose is installed" >&1 + fi + + echo $DOCKER_GOOD +} + +function group_setup() { + echo "Setting up groups:" >&1 + if [[ ! "$(user_in_group bluetooth)" == "notgroup" ]] && [[ ! "$(user_in_group bluetooth)" == "true" ]]; then + echo "User is NOT in 'bluetooth' group. Adding:" >&1 + echo "sudo usermod -G bluetooth -a $USER" >&1 + sudo -E usermod -G "bluetooth" -a $USER + fi + + if [ ! "$(user_in_group docker)" == "true" ]; then + echo "User is NOT in 'docker' group. Adding:" >&1 + echo "sudo usermod -G docker -a $USER" >&1 + sudo -E usermod -G "docker" -a $USER + fi + + echo "" >&1 + echo "Rebooting or logging off is advised." >&1 +} + +function group_check() { + echo "Setting up groups:" >&1 + NEED_GROUP_SETUP="false" + if [[ ! "$(user_in_group bluetooth)" == "notgroup" ]] && [[ ! "$(user_in_group bluetooth)" == "true" ]]; then + NEED_GROUP_SETUP="fail" + echo "User is NOT in 'bluetooth' group." >&1 + fi + + if [ ! "$(user_in_group docker)" == "true" ]; then + NEED_GROUP_SETUP="fail" + echo "User is NOT in 'docker' group." >&1 + fi + + echo $NEED_GROUP_SETUP +} + +function do_env_setup() { + sudo -E apt-get install git wget unzip -y +} + +function generate_container_ssh() { + cat /dev/null | ssh-keygen -q -N "" -f $CONTAINER_KEYS_FILE > /dev/null +} + +function install_ssh_keys() { + touch $AUTH_KEYS_FILE + if [ -f "$CONTAINER_KEYS_FILE" ]; then + NEW_KEY="$(cat $CONTAINER_KEYS_FILE.pub)" + if grep -Fxq "$NEW_KEY" $AUTH_KEYS_FILE ; then + echo "Key already exists in '$AUTH_KEYS_FILE' Skipping..." >&1 + else + echo "$NEW_KEY" >> $AUTH_KEYS_FILE + echo "'$NEW_KEY' >> $AUTH_KEYS_FILE" >&1 + echo "Key added." >&1 + fi + else + echo "Something went wrong. Couldn't access container keys file '$CONTAINER_KEYS_FILE'" >&1 + fi +} + +function check_container_ssh() { + KEYS_EXIST="false" + if [[ -f "$CONTAINER_KEYS_FILE" && -f "$CONTAINER_KEYS_FILE.pub" ]]; then + KEYS_EXIST="true" + fi + + echo $KEYS_EXIST +} + +function check_host_ssh_keys() { + KEY_EXISTS="false" + grep -f "$CONTAINER_KEYS_FILE.pub" $AUTH_KEYS_FILE + GRES=$? + if [[ $GRES -eq 0 ]]; then + KEY_EXISTS="true" + fi + + echo $KEY_EXISTS +} + +function do_iotstack_setup() { + git clone https://github.com/SensorsIot/IOTstack.git + cd IOTstack + + if [ $? -eq 0 ]; then + echo "IOTstack cloned" >&1 + else + echo "Could not find IOTstack directory" >&1 + exit 5 + fi +} diff --git a/scripts/yaml_merge.py b/scripts/yaml_merge.py deleted file mode 100755 index ad7477ac0..000000000 --- a/scripts/yaml_merge.py +++ /dev/null @@ -1,71 +0,0 @@ -import sys -import traceback -import ruamel.yaml - -yaml = ruamel.yaml.YAML() -yaml.preserve_quotes = True - -if sys.argv[1] == "--pyyaml-version": - try: - print("pyyaml", yaml.__version__) - sys.exit(0) - except SystemExit: - sys.exit(0) - except: - print("could not get pyyaml version") - sys.exit(3) - -if len(sys.argv) < 4: - print("Error: Not enough args") - print("Usage:") - print(" yaml_merge.py [inputFile] [mergeFile] [outputFile]") - print("") - print("Example:") - print(" yaml_merge.py ./.tmp/docker-compose.tmp.yml ./compose-override.yml ./docker-compose.yml") - sys.exit(4) - -try: - pathTempDockerCompose = sys.argv[1] - pathOverride = sys.argv[2] - pathOutput = sys.argv[3] - - def mergeYaml(priorityYaml, defaultYaml): - finalYaml = {} - if isinstance(defaultYaml, dict): - for dk, dv in defaultYaml.items(): - if dk in priorityYaml: - finalYaml[dk] = mergeYaml(priorityYaml[dk], dv) - else: - finalYaml[dk] = dv - for pk, pv in priorityYaml.items(): - if pk in finalYaml: - finalYaml[pk] = mergeYaml(finalYaml[pk], pv) - else: - finalYaml[pk] = pv - else: - finalYaml = defaultYaml - return finalYaml - - with open(r'%s' % pathTempDockerCompose) as fileTempDockerCompose: - yamlTempDockerCompose = yaml.load(fileTempDockerCompose) - - with open(r'%s' % pathOverride) as fileOverride: - yamlOverride = yaml.load(fileOverride) - - mergedYaml = mergeYaml(yamlOverride, yamlTempDockerCompose) - - with open(r'%s' % pathOutput, 'w') as outputFile: - yaml.dump(mergedYaml, outputFile, explicit_start=True, default_style='"') - - sys.exit(0) -except SystemExit: - sys.exit(0) -except: - print("Something went wrong: ") - print(sys.exc_info()) - print(traceback.print_exc()) - print("") - print("") - print("PyYaml Version: ", yaml.__version__) - print("") - sys.exit(2)