diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000..a6454a51 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,4 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +ARG VARIANT="16-bullseye" +FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:0-${VARIANT} diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..326cac82 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +{ + "name": "Dev Containers CLI", + "build": { + "dockerfile": "Dockerfile", + "args": { + "VARIANT": "16-bullseye" + } + }, + + "extensions": [ + "dbaeumer.vscode-eslint" + ], + + "postCreateCommand": "yarn install", + + "remoteUser": "node", + + "features": { + "docker-in-docker": "latest" + } +} diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..4197b94e --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +**/node_modules/** \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..678ded87 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,63 @@ +module.exports = { + 'env': { + 'browser': true, + 'node': true + }, + 'parser': '@typescript-eslint/parser', + 'parserOptions': { + 'sourceType': 'module' + }, + 'plugins': [ + '@typescript-eslint' + ], + 'rules': { + // '@typescript-eslint/class-name-casing': 'warn', https://github.com/typescript-eslint/typescript-eslint/issues/2077 + '@typescript-eslint/member-delimiter-style': [ + 'warn', + { + 'multiline': { + 'delimiter': 'semi', + 'requireLast': true + }, + 'singleline': { + 'delimiter': 'semi', + 'requireLast': false + } + } + ], + '@typescript-eslint/semi': [ + 'warn', + 'always' + ], + 'constructor-super': 'warn', + 'curly': 'warn', + 'eqeqeq': [ + 'warn', + 'always' + ], + 'no-async-promise-executor': 'warn', + 'no-buffer-constructor': 'warn', + 'no-caller': 'warn', + 'no-debugger': 'warn', + 'no-duplicate-case': 'warn', + 'no-duplicate-imports': 'warn', + 'no-eval': 'warn', + 'no-extra-semi': 'warn', + 'no-new-wrappers': 'warn', + 'no-redeclare': 'off', + 'no-sparse-arrays': 'warn', + 'no-throw-literal': 'warn', + 'no-unsafe-finally': 'warn', + 'no-unused-labels': 'warn', + '@typescript-eslint/no-redeclare': 'warn', + 'code-no-unexternalized-strings': 'warn', + 'no-throw-literal': 'warn', + 'no-var': 'warn', + 'code-no-unused-expressions': [ + 'warn', + { + 'allowTernary': true + } + ], + } +}; diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..45413f2a --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +* text=auto +*.bat eol=crlf +*.cmd eol=crlf +*.ps1 eol=lf +*.sh eol=lf \ No newline at end of file diff --git a/.github/workflows/build-chat.yml b/.github/workflows/build-chat.yml new file mode 100644 index 00000000..8d7f95ca --- /dev/null +++ b/.github/workflows/build-chat.yml @@ -0,0 +1,34 @@ +name: Build Chat + +on: + workflow_run: + workflows: + - '**' + types: + - completed + branches: + - '**' + +jobs: + main: + runs-on: ubuntu-latest + steps: + - name: Checkout Actions + uses: actions/checkout@v2 + with: + repository: "microsoft/vscode-github-triage-actions" + path: ./actions + - name: Install Actions + run: npm install --production --prefix ./actions + - name: Install Additional Dependencies + # Pulls in a bunch of other packages that arent needed for the rest of the actions + run: npm install @azure/storage-blob@12.1.1 + - name: Build Chat + uses: ./actions/build-chat + with: + token: ${{ secrets.GITHUB_TOKEN }} + slack_token: ${{ secrets.SLACK_TOKEN }} + storage_connection_string: ${{ secrets.BUILD_CHAT_STORAGE_CONNECTION_STRING }} + workflow_run_url: ${{ github.event.workflow_run.url }} + notify_authors: true + log_channel: bot-log diff --git a/.github/workflows/dev-containers.yml b/.github/workflows/dev-containers.yml new file mode 100644 index 00000000..adf4ebc0 --- /dev/null +++ b/.github/workflows/dev-containers.yml @@ -0,0 +1,52 @@ +name: Dev Containers CI + +on: + push: + branches: + - '**' + pull_request: + branches: + - '**' + +jobs: + cli: + name: CLI + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: '16.x' + registry-url: 'https://npm.pkg.github.com' + scope: '@microsoft' + - run: yarn + - run: yarn package + - run: yarn test --forbid-only + - name: TGZ name + run: | + VERSION=$(jq -r '.version' < package.json) + echo "TGZ=dev-containers-cli-${VERSION}.tgz" | tee -a $GITHUB_ENV + echo "TGZ_UPLOAD=dev-containers-cli-${VERSION}-${GITHUB_SHA:0:8}.tgz" | tee -a $GITHUB_ENV + - name: Store TGZ + uses: actions/upload-artifact@v2 + with: + name: ${{ env.TGZ_UPLOAD }} + path: ${{ env.TGZ }} + container-features: + name: Test container-features + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Setup Node.js + uses: actions/setup-node@v1 + with: + node-version: '16.x' + registry-url: 'https://npm.pkg.github.com' + scope: '@microsoft' + - name: Install Dependencies + run: yarn + - name: Compile + run: yarn compile + - name: Run Offline Tests + run: yarn test-container-features-offline --forbid-only \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..523c0843 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +dist +node_modules +logs +*.log +*.tgz +tmp +.DS_Store \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 00000000..f351069e --- /dev/null +++ b/.npmignore @@ -0,0 +1,16 @@ +*.tgz +*.log +test +tsconfig*.json +src +tsfmt.json +scripts/gitAskPass.sh +*.js.map +build +azure-pipelines.yml +.vscode +.github +.gitattributes +.eslintrc.js +.eslintignore +.devcontainer \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..8b4c1e5e --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "dbaeumer.vscode-eslint", + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..7726b7f4 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,21 @@ +{ + "git.ignoreLimitWarning": true, + "search.exclude": { + "dist": true + }, + "typescript.tsc.autoDetect": "off", + "eslint.options": { + "rulePaths": [ + "./build/eslint" + ] + }, + "mochaExplorer.files": "test/**/*.test.ts", + "mochaExplorer.require": "ts-node/register", + "mochaExplorer.env": { + "TS_NODE_PROJECT": "src/test/tsconfig.json" + }, + "files.associations": { + "devcontainer-features.json": "jsonc" + }, + "typescript.tsdk": "node_modules/typescript/lib" +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..054fc067 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,47 @@ +// See https://go.microsoft.com/fwlink/?LinkId=733558 +// for the documentation about the tasks.json format +{ + "version": "2.0.0", + "windows": { + "options": { + "shell": { + // Run Tests requires powershell on Windows. + "executable": "powershell.exe" + } + } + }, + "tasks": [ + { + "type": "npm", + "script": "watch", + "problemMatcher": "$tsc-watch", + "isBackground": true, + "label": "Build Dev Containers CLI", + "presentation": { + "reveal": "never" + }, + "group": { + "kind": "build", + "isDefault": true + }, + }, + { + "type": "npm", + "script": "lint", + "problemMatcher": [ + "$eslint-stylish" + ], + "label": "npm: lint", + }, + { + "type": "npm", + "script": "test", + "problemMatcher": [], + "label": "Run Tests", + "group": { + "kind": "test", + "isDefault": true + } + } + ] +} \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..5655731f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,34 @@ +# Contributing + +We're excited for your contributions to the development container CLI! This document outlines how you can get involved. + +## Contribution approaches + +- Propose the change via an issue in the [specification repository](https://github.com/microsoft/dev-container-spec/issues). Try to get early feedback before spending too much effort formalizing it. +- More formally document the proposed change in terms of properties and their semantics. Look to format your proposal like our [devcontainer.json reference](https://aka.ms/devcontainer.json), which is a JSON with Comments (jsonc) format. + +Here is a sample: + +| Property | Type | Description | +|----------|------|-------------| +| `image` | string | **Required** when [using an image](/docs/remote/create-dev-container.md#using-an-image-or-dockerfile). The name of an image in a container registry ([DockerHub](https://hub.docker.com), [GitHub Container Registry](https://docs.github.com/packages/guides/about-github-container-registry), [Azure Container Registry](https://azure.microsoft.com/services/container-registry/)) that VS Code and other `devcontainer.json` supporting services / tools should use to create the dev container. | + +- You may open a PR, i.e code or shell scripts demonstrating approaches for implementation. +- Once there is discussion on your proposal, please also open and link a PR to update the [devcontainer.json reference doc](https://aka.ms/devcontainer.json). When your proposal is merged, the docs will be kept up-to-date with the latest spec. + +## Review process + +The specification repo uses the following [labels](https://github.com/microsoft/dev-container-spec/labels): + +- `proposal`: Issues under discussion, still collecting feedback. +- `finalization`: Proposals we intend to make part of the spec. + +[Milestones](https://github.com/microsoft/dev-container-spec/milestones) use a "month year" pattern (i.e. January 2022). If a finalized proposal is added to a milestone, it is intended to be merged during that milestone. + +## Miscellaneous + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or +contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +This project is under an [MIT license](LICENSE.txt). diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..dfde11a3 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) Microsoft Corporation. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..b723c3e1 --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# Dev Container CLI + +This repository holds the dev container CLI, which can take a devcontainer.json and create and configure a dev container from it. + +## Context + +A development container allows you to use a container as a full-featured development environment. It can be used to run an application, to separate tools, libraries, or runtimes needed for working with a codebase, and to aid in continuous integration and testing. Dev containers can be run locally or remotely, in a private or public cloud. + +![Diagram of inner and outerloop development with dev containers](/images/dev-container-stages.png) + +This CLI is in active development. Current status: + +- [x] `dev-containers-cli build` - Enables building/pre-building images +- [x] `dev-containers-cli up` - Spins up containers with `devcontainer.json` settings applied +- [x] `dev-containers-cli run-user-commands` - Runs lifecycle commands like `postCreateCommand` +- [x] `dev-containers-cli read-configuration` - Outputs current configuration for workspace +- [x] `dev-containers-cli exec` - Executes a command in a container with `userEnvProbe`, `remoteUser`, `remoteEnv`, and other properties applied +- [ ] `dev-containers-cli stop` - Stops containers +- [ ] `dev-containers-cli down` - Stops and deletes containers + +## Specification + +The dev container CLI is part of the [Development Containers Specification](https://github.com/microsoft/dev-container-spec). This spec seeks to find ways to enrich existing formats with common development specific settings, tools, and configuration while still providing a simplified, un-orchestrated single container option – so that they can be used as coding environments or for continuous integration and testing. + +Learn more on the [dev container spec website](https://devcontainers.github.io/containers.dev/). + +## Additional resources + +You may review other resources part of the specification in the [`devcontainers` GitHub organization](https://github.com/devcontainers). + +## Contributing + +Check out how to contribute to the CLI in [CONTRIBUTING.md](contributing.md). + +## License + +This project is under an [MIT license](LICENSE.txt). diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt new file mode 100644 index 00000000..db9d3074 --- /dev/null +++ b/ThirdPartyNotices.txt @@ -0,0 +1,1319 @@ +NOTICES AND INFORMATION +Do Not Translate or Localize + +This software incorporates material from third parties. +Microsoft makes certain open source code available at https://3rdpartysource.microsoft.com, +or you may send a check or money order for US $5.00, including the product name, +the open source component name, platform, and version number, to: + +Source Code Compliance Team +Microsoft Corporation +One Microsoft Way +Redmond, WA 98052 +USA + +Notwithstanding any other terms, you may reverse engineer this software to the extent +required to debug changes to any libraries licensed under the GNU Lesser General Public License. + +--------------------------------------------------------- + +chownr 2.0.0 - ISC +https://github.com/isaacs/chownr#readme + +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +cliui 7.0.4 - ISC +https://github.com/yargs/cliui#readme + +Copyright (c) 2015 +Copyright (c) npm, Inc. and Contributors + +Copyright (c) 2015, Contributors + +Permission to use, copy, modify, and/or distribute this software +for any purpose with or without fee is hereby granted, provided +that the above copyright notice and this permission notice +appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE +LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +fs-minipass 2.1.0 - ISC +https://github.com/npm/fs-minipass#readme + +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +get-caller-file 2.0.5 - ISC +https://github.com/stefanpenner/get-caller-file#readme + +Copyright 2018 Stefan Penner + +ISC License (ISC) +Copyright 2018 Stefan Penner + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +lru-cache 6.0.0 - ISC +https://github.com/isaacs/node-lru-cache#readme + +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +minipass 3.1.3 - ISC +https://github.com/isaacs/minipass#readme + +Copyright (c) npm, Inc. and Contributors + +The ISC License + +Copyright (c) npm, Inc. and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +semver 7.3.5 - ISC +https://github.com/npm/node-semver#readme + +Copyright Isaac Z. Schlueter +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +tar 6.1.11 - ISC +https://github.com/npm/node-tar#readme + +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +y18n 5.0.8 - ISC +https://github.com/yargs/y18n + +Copyright (c) 2015 + +Copyright (c) 2015, Contributors + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +yallist 4.0.0 - ISC +https://github.com/isaacs/yallist#readme + +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +yargs-parser 20.2.7 - ISC +https://github.com/yargs/yargs-parser#readme + +Copyright (c) 2016 + +Copyright (c) 2016, Contributors + +Permission to use, copy, modify, and/or distribute this software +for any purpose with or without fee is hereby granted, provided +that the above copyright notice and this permission notice +appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE +LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +ansi-regex 5.0.1 - MIT +https://github.com/chalk/ansi-regex#readme + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +ansi-styles 4.3.0 - MIT +https://github.com/chalk/ansi-styles#readme + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +color-convert 2.0.1 - MIT +https://github.com/Qix-/color-convert#readme + +Copyright (c) 2011-2016, Heather Arthur and Josh Junon +Copyright (c) 2011-2016 Heather Arthur + +Copyright (c) 2011-2016 Heather Arthur + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + +--------------------------------------------------------- + +--------------------------------------------------------- + +color-name 1.1.4 - MIT +https://github.com/colorjs/color-name + +Copyright (c) 2015 Dmitry Ivanov + +The MIT License (MIT) +Copyright (c) 2015 Dmitry Ivanov + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +--------------------------------------------------------- + +--------------------------------------------------------- + +emoji-regex 8.0.0 - MIT +https://mths.be/emoji-regex + +Copyright Mathias Bynens + +Copyright Mathias Bynens + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +escalade 3.1.1 - MIT +https://github.com/lukeed/escalade#readme + +(c) Luke Edwards (https://lukeed.com) +Copyright (c) Luke Edwards (lukeed.com) + +MIT License + +Copyright (c) Luke Edwards (lukeed.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +follow-redirects 1.14.8 - MIT +https://github.com/follow-redirects/follow-redirects + +Copyright 2014-present Olivier Lalonde , James Talmage , Ruben Verborgh + +Copyright 2014–present Olivier Lalonde , James Talmage , Ruben Verborgh + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +is-fullwidth-code-point 3.0.0 - MIT +https://github.com/sindresorhus/is-fullwidth-code-point#readme + +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +jsonc-parser 3.0.0 - MIT +https://github.com/microsoft/node-jsonc-parser#readme + +Copyright (c) Microsoft +Copyright 2018, Microsoft +Copyright (c) Microsoft Corporation. + +The MIT License (MIT) + +Copyright (c) Microsoft + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +js-yaml 4.1.0 - MIT +https://github.com/nodeca/js-yaml#readme + +Copyright (c) 2011-2015 by Vitaly Puzrin + +(The MIT License) + +Copyright (C) 2011-2015 by Vitaly Puzrin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +looper 3.0.0 - MIT +https://github.com/dominictarr/looper + +Copyright (c) 2013 Dominic Tarr + +Copyright (c) 2013 Dominic Tarr + +Permission is hereby granted, free of charge, +to any person obtaining a copy of this software and +associated documentation files (the "Software"), to +deal in the Software without restriction, including +without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom +the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +minizlib 2.1.2 - MIT +https://github.com/isaacs/minizlib#readme + +Copyright Isaac Z. Schlueter and Contributors +Copyright Joyent, Inc. and other Node contributors. + +Minizlib was created by Isaac Z. Schlueter. +It is a derivative work of the Node.js project. + +""" +Copyright Isaac Z. Schlueter and Contributors +Copyright Node.js contributors. All rights reserved. +Copyright Joyent, Inc. and other Node contributors. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +""" + + +--------------------------------------------------------- + +--------------------------------------------------------- + +mkdirp 1.0.4 - MIT +https://github.com/isaacs/node-mkdirp#readme + +Copyright James Halliday (mail@substack.net) and Isaac Z. Schlueter (i@izs.me) + +Copyright James Halliday (mail@substack.net) and Isaac Z. Schlueter (i@izs.me) + +This project is free software released under the MIT license: + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +nan 2.14.2 - MIT +https://github.com/nodejs/nan#readme + +Copyright (c) 2018 NAN WG Members +Copyright (c) 2018 NAN contributors +Copyright Joyent, Inc. and other Node contributors. +Copyright (c) 2018 NAN contributors - Rod Vagg + +The MIT License (MIT) +===================== + +Copyright (c) 2018 NAN contributors +----------------------------------- + +*NAN contributors listed at * + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +node-pty 0.10.1 - MIT +https://github.com/Tyriar/node-pty + +Copyright This project +Copyright (c) 2016, Daniel Imms +Copyright (c) 2017, Daniel Imms +Copyright (c) 2015 Ryan Prichard +Copyright (c) 2016 Ryan Prichard +Copyright (c) 2011-2012 Ryan Prichard +Copyright (c) 2011-2015 Ryan Prichard +Copyright (c) 2011-2016 Ryan Prichard +Copyright (c) 2009 Microsoft Corporation. +Copyright (c) 2018, Microsoft Corporation +Copyright (c) 2019, Microsoft Corporation +Copyright (c) 2012-2015, Christopher Jeffrey +Copyright (c) 2009 Todd Carson +Copyright (c) 2018 - present Microsoft Corporation +Copyright (c) 2009 Joshua Elsasser +Copyright (c) 2012-2015, Christopher Jeffrey, Peter Sunde +Copyright (c) 2013-2015, Christopher Jeffrey, Peter Sunde +Copyright (c) 2009 Nicholas Marriott +Copyright (c) 2016, Daniel Imms (http://www.growingwiththeweb.com) +Copyright (c) 2012-2015, Christopher Jeffrey (https://github.com/chjj/) + +Copyright (c) 2012-2015, Christopher Jeffrey (https://github.com/chjj/) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + + +The MIT License (MIT) + +Copyright (c) 2016, Daniel Imms (http://www.growingwiththeweb.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +MIT License + +Copyright (c) 2018 - present Microsoft Corporation + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +The MIT License (MIT) + +Copyright (c) 2011-2016 Ryan Prichard + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +pull-stream 3.6.14 - MIT +https://pull-stream.github.io/ + +Copyright (c) 2013 Dominic Tarr + +Copyright (c) 2013 Dominic Tarr + +Permission is hereby granted, free of charge, +to any person obtaining a copy of this software and +associated documentation files (the "Software"), to +deal in the Software without restriction, including +without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom +the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +require-directory 2.1.1 - MIT +https://github.com/troygoode/node-require-directory/ + +Copyright (c) 2011 Troy Goode + +The MIT License (MIT) + +Copyright (c) 2011 Troy Goode + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +shell-quote 1.7.3 - MIT +https://github.com/substack/node-shell-quote + +Copyright (c) 2013 James Halliday (mail@substack.net) + +The MIT License + +Copyright (c) 2013 James Halliday (mail@substack.net) + +Permission is hereby granted, free of charge, +to any person obtaining a copy of this software and +associated documentation files (the "Software"), to +deal in the Software without restriction, including +without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom +the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +--------------------------------------------------------- + +--------------------------------------------------------- + +stream-to-pull-stream 1.7.3 - MIT +https://github.com/dominictarr/stream-to-pull-stream + +Copyright (c) 2013 Dominic Tarr + +Copyright (c) 2013 Dominic Tarr + +Permission is hereby granted, free of charge, +to any person obtaining a copy of this software and +associated documentation files (the "Software"), to +deal in the Software without restriction, including +without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom +the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +string-width 4.2.2 - MIT +https://github.com/sindresorhus/string-width#readme + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +strip-ansi 6.0.0 - MIT +https://github.com/chalk/strip-ansi#readme + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +vscode-uri 3.0.3 - MIT +https://github.com/microsoft/vscode-uri#readme + +Copyright (c) Microsoft +Copyright (c) Microsoft Corporation. +Copyright Joyent, Inc. and other Node contributors. + +The MIT License (MIT) + +Copyright (c) Microsoft + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +--------------------------------------------------------- + +--------------------------------------------------------- + +wrap-ansi 7.0.0 - MIT +https://github.com/chalk/wrap-ansi#readme + +Copyright (c) Sindre Sorhus (https://sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (https://sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +yargs 17.0.1 - MIT +https://yargs.js.org/ + +Copyright 2014 +Copyright 2010 James Halliday (mail@substack.net) + +MIT License + +Copyright 2010 James Halliday (mail@substack.net); Modified work Copyright 2014 Contributors (ben@npmjs.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +argparse 2.0.1 - Python-2.0 +https://github.com/nodeca/argparse#readme + +Copyright (c) 1999-2001 Gregory P. Ward. +Copyright (c) 2002, 2003 Python Software Foundation. +Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam +Copyright (c) 1995-2001 Corporation for National Research Initiatives +Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Python Software Foundation + +A. HISTORY OF THE SOFTWARE +========================== + +Python was created in the early 1990s by Guido van Rossum at Stichting +Mathematisch Centrum (CWI, see http://www.cwi.nl) in the Netherlands +as a successor of a language called ABC. Guido remains Python's +principal author, although it includes many contributions from others. + +In 1995, Guido continued his work on Python at the Corporation for +National Research Initiatives (CNRI, see http://www.cnri.reston.va.us) +in Reston, Virginia where he released several versions of the +software. + +In May 2000, Guido and the Python core development team moved to +BeOpen.com to form the BeOpen PythonLabs team. In October of the same +year, the PythonLabs team moved to Digital Creations, which became +Zope Corporation. In 2001, the Python Software Foundation (PSF, see +https://www.python.org/psf/) was formed, a non-profit organization +created specifically to own Python-related Intellectual Property. +Zope Corporation was a sponsoring member of the PSF. + +All Python releases are Open Source (see http://www.opensource.org for +the Open Source Definition). Historically, most, but not all, Python +releases have also been GPL-compatible; the table below summarizes +the various releases. + + Release Derived Year Owner GPL- + from compatible? (1) + + 0.9.0 thru 1.2 1991-1995 CWI yes + 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes + 1.6 1.5.2 2000 CNRI no + 2.0 1.6 2000 BeOpen.com no + 1.6.1 1.6 2001 CNRI yes (2) + 2.1 2.0+1.6.1 2001 PSF no + 2.0.1 2.0+1.6.1 2001 PSF yes + 2.1.1 2.1+2.0.1 2001 PSF yes + 2.1.2 2.1.1 2002 PSF yes + 2.1.3 2.1.2 2002 PSF yes + 2.2 and above 2.1.1 2001-now PSF yes + +Footnotes: + +(1) GPL-compatible doesn't mean that we're distributing Python under + the GPL. All Python licenses, unlike the GPL, let you distribute + a modified version without making your changes open source. The + GPL-compatible licenses make it possible to combine Python with + other software that is released under the GPL; the others don't. + +(2) According to Richard Stallman, 1.6.1 is not GPL-compatible, + because its license has a choice of law clause. According to + CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1 + is "not incompatible" with the GPL. + +Thanks to the many outside volunteers who have worked under Guido's +direction to make these releases possible. + + +B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON +=============================================================== + +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Python Software Foundation; +All Rights Reserved" are retained in Python alone or in any derivative version +prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 +------------------------------------------- + +BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 + +1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an +office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the +Individual or Organization ("Licensee") accessing and otherwise using +this software in source or binary form and its associated +documentation ("the Software"). + +2. Subject to the terms and conditions of this BeOpen Python License +Agreement, BeOpen hereby grants Licensee a non-exclusive, +royalty-free, world-wide license to reproduce, analyze, test, perform +and/or display publicly, prepare derivative works, distribute, and +otherwise use the Software alone or in any derivative version, +provided, however, that the BeOpen Python License is retained in the +Software, alone or in any derivative version prepared by Licensee. + +3. BeOpen is making the Software available to Licensee on an "AS IS" +basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE +SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS +AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY +DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +5. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +6. This License Agreement shall be governed by and interpreted in all +respects by the law of the State of California, excluding conflict of +law provisions. Nothing in this License Agreement shall be deemed to +create any relationship of agency, partnership, or joint venture +between BeOpen and Licensee. This License Agreement does not grant +permission to use BeOpen trademarks or trade names in a trademark +sense to endorse or promote products or services of Licensee, or any +third party. As an exception, the "BeOpen Python" logos available at +http://www.pythonlabs.com/logos.html may be used according to the +permissions granted on that web page. + +7. By copying, installing or otherwise using the software, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 +--------------------------------------- + +1. This LICENSE AGREEMENT is between the Corporation for National +Research Initiatives, having an office at 1895 Preston White Drive, +Reston, VA 20191 ("CNRI"), and the Individual or Organization +("Licensee") accessing and otherwise using Python 1.6.1 software in +source or binary form and its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, CNRI +hereby grants Licensee a nonexclusive, royalty-free, world-wide +license to reproduce, analyze, test, perform and/or display publicly, +prepare derivative works, distribute, and otherwise use Python 1.6.1 +alone or in any derivative version, provided, however, that CNRI's +License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) +1995-2001 Corporation for National Research Initiatives; All Rights +Reserved" are retained in Python 1.6.1 alone or in any derivative +version prepared by Licensee. Alternately, in lieu of CNRI's License +Agreement, Licensee may substitute the following text (omitting the +quotes): "Python 1.6.1 is made available subject to the terms and +conditions in CNRI's License Agreement. This Agreement together with +Python 1.6.1 may be located on the Internet using the following +unique, persistent identifier (known as a handle): 1895.22/1013. This +Agreement may also be obtained from a proxy server on the Internet +using the following URL: http://hdl.handle.net/1895.22/1013". + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python 1.6.1 or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python 1.6.1. + +4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" +basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. This License Agreement shall be governed by the federal +intellectual property law of the United States, including without +limitation the federal copyright law, and, to the extent such +U.S. federal law does not apply, by the law of the Commonwealth of +Virginia, excluding Virginia's conflict of law provisions. +Notwithstanding the foregoing, with regard to derivative works based +on Python 1.6.1 that incorporate non-separable material that was +previously distributed under the GNU General Public License (GPL), the +law of the Commonwealth of Virginia shall govern this License +Agreement only as to issues arising under or with respect to +Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this +License Agreement shall be deemed to create any relationship of +agency, partnership, or joint venture between CNRI and Licensee. This +License Agreement does not grant permission to use CNRI trademarks or +trade name in a trademark sense to endorse or promote products or +services of Licensee, or any third party. + +8. By clicking on the "ACCEPT" button where indicated, or by copying, +installing or otherwise using Python 1.6.1, Licensee agrees to be +bound by the terms and conditions of this License Agreement. + + ACCEPT + + +CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 +-------------------------------------------------- + +Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, +The Netherlands. All rights reserved. + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name of Stichting Mathematisch +Centrum or CWI not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior +permission. + +STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO +THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE +FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +--------------------------------------------------------- + diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 00000000..81b99630 --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,41 @@ +pool: + vmImage: "ubuntu-latest" + +trigger: + branches: + include: + - 'main' + - 'release/*' + - 'spec-main' + - 'spec-release/*' +pr: none + +steps: +- task: ComponentGovernanceComponentDetection@0 +- task: notice@0 + displayName: 'NOTICE File Generator' + inputs: + outputformat: 'text' +- task: DownloadPipelineArtifact@2 +- script: | + PIPELINE_WORKSPACE="$(Pipeline.Workspace)" + if [ "$(sort "$PIPELINE_WORKSPACE/NOTICE.txt/NOTICE.txt" | tr -d '\015')" = "$(sort ThirdPartyNotices.txt | tr -d '\015')" ] + then + echo "3rd-party notices unchanged." + else + echo "3rd-party notices changed." + cp "$PIPELINE_WORKSPACE/NOTICE.txt/NOTICE.txt" ThirdPartyNotices.txt + git status + git add ThirdPartyNotices.txt + git config --global user.email "chrmarti@microsoft.com" + git config --global user.name "Christof Marti" + git commit -m "Auto-update ThirdPartyNotices.txt" + if [ "$(git log -1 --pretty=%B | head -n 1)" != "$(git log HEAD~2..HEAD~1 --pretty=%B | head -n 1)" ] + then + GIT_ASKPASS=scripts/gitAskPass.sh git push origin HEAD:$(Build.SourceBranch) + else + echo "Triggered by own commit, not pushing." + fi + fi + env: + GIT_TOKEN: $(GIT_TOKEN) diff --git a/build/eslint/code-no-unexternalized-strings.js b/build/eslint/code-no-unexternalized-strings.js new file mode 100644 index 00000000..28fce5b9 --- /dev/null +++ b/build/eslint/code-no-unexternalized-strings.js @@ -0,0 +1,111 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +var _a; +const experimental_utils_1 = require("@typescript-eslint/experimental-utils"); +function isStringLiteral(node) { + return !!node && node.type === experimental_utils_1.AST_NODE_TYPES.Literal && typeof node.value === 'string'; +} +function isDoubleQuoted(node) { + return node.raw[0] === '"' && node.raw[node.raw.length - 1] === '"'; +} +module.exports = new (_a = class NoUnexternalizedStrings { + constructor() { + this.meta = { + messages: { + doubleQuoted: 'Only use double-quoted strings for externalized strings.', + badKey: 'The key \'{{key}}\' doesn\'t conform to a valid localize identifier.', + duplicateKey: 'Duplicate key \'{{key}}\' with different message value.', + badMessage: 'Message argument to \'{{message}}\' must be a string literal.' + } + }; + } + create(context) { + const externalizedStringLiterals = new Map(); + const doubleQuotedStringLiterals = new Set(); + function collectDoubleQuotedStrings(node) { + if (isStringLiteral(node) && isDoubleQuoted(node)) { + doubleQuotedStringLiterals.add(node); + } + } + function visitLocalizeCall(node) { + // localize(key, message) + const [keyNode, messageNode] = node.arguments; + // (1) + // extract key so that it can be checked later + let key; + if (isStringLiteral(keyNode)) { + doubleQuotedStringLiterals.delete(keyNode); //todo@joh reconsider + key = keyNode.value; + } + else if (keyNode.type === experimental_utils_1.AST_NODE_TYPES.ObjectExpression) { + for (let property of keyNode.properties) { + if (property.type === experimental_utils_1.AST_NODE_TYPES.Property && !property.computed) { + if (property.key.type === experimental_utils_1.AST_NODE_TYPES.Identifier && property.key.name === 'key') { + if (isStringLiteral(property.value)) { + doubleQuotedStringLiterals.delete(property.value); //todo@joh reconsider + key = property.value.value; + break; + } + } + } + } + } + if (typeof key === 'string') { + let array = externalizedStringLiterals.get(key); + if (!array) { + array = []; + externalizedStringLiterals.set(key, array); + } + array.push({ call: node, message: messageNode }); + } + // (2) + // remove message-argument from doubleQuoted list and make + // sure it is a string-literal + doubleQuotedStringLiterals.delete(messageNode); + if (!isStringLiteral(messageNode)) { + context.report({ + loc: messageNode.loc, + messageId: 'badMessage', + data: { message: context.getSourceCode().getText(node) } + }); + } + } + function reportBadStringsAndBadKeys() { + // (1) + // report all strings that are in double quotes + for (const node of doubleQuotedStringLiterals) { + context.report({ loc: node.loc, messageId: 'doubleQuoted' }); + } + for (const [key, values] of externalizedStringLiterals) { + // (2) + // report all invalid NLS keys + if (!key.match(NoUnexternalizedStrings._rNlsKeys)) { + for (let value of values) { + context.report({ loc: value.call.loc, messageId: 'badKey', data: { key } }); + } + } + // (2) + // report all invalid duplicates (same key, different message) + if (values.length > 1) { + for (let i = 1; i < values.length; i++) { + if (context.getSourceCode().getText(values[i - 1].message) !== context.getSourceCode().getText(values[i].message)) { + context.report({ loc: values[i].call.loc, messageId: 'duplicateKey', data: { key } }); + } + } + } + } + } + return { + ['Literal']: (node) => collectDoubleQuotedStrings(node), + ['ExpressionStatement[directive] Literal:exit']: (node) => doubleQuotedStringLiterals.delete(node), + ['CallExpression[callee.type="MemberExpression"][callee.object.name="nls"][callee.property.name="localize"]:exit']: (node) => visitLocalizeCall(node), + ['CallExpression[callee.name="localize"][arguments.length>=2]:exit']: (node) => visitLocalizeCall(node), + ['Program:exit']: reportBadStringsAndBadKeys, + }; + } + }, + _a._rNlsKeys = /^[_a-zA-Z0-9][ .\-_a-zA-Z0-9]*$/, + _a); diff --git a/build/eslint/code-no-unexternalized-strings.ts b/build/eslint/code-no-unexternalized-strings.ts new file mode 100644 index 00000000..29db884c --- /dev/null +++ b/build/eslint/code-no-unexternalized-strings.ts @@ -0,0 +1,126 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as eslint from 'eslint'; +import { TSESTree, AST_NODE_TYPES } from '@typescript-eslint/experimental-utils'; + +function isStringLiteral(node: TSESTree.Node | null | undefined): node is TSESTree.StringLiteral { + return !!node && node.type === AST_NODE_TYPES.Literal && typeof node.value === 'string'; +} + +function isDoubleQuoted(node: TSESTree.StringLiteral): boolean { + return node.raw[0] === '"' && node.raw[node.raw.length - 1] === '"'; +} + +export = new class NoUnexternalizedStrings implements eslint.Rule.RuleModule { + + private static _rNlsKeys = /^[_a-zA-Z0-9][ .\-_a-zA-Z0-9]*$/; + + readonly meta: eslint.Rule.RuleMetaData = { + messages: { + doubleQuoted: 'Only use double-quoted strings for externalized strings.', + badKey: 'The key \'{{key}}\' doesn\'t conform to a valid localize identifier.', + duplicateKey: 'Duplicate key \'{{key}}\' with different message value.', + badMessage: 'Message argument to \'{{message}}\' must be a string literal.' + } + }; + + create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { + + const externalizedStringLiterals = new Map(); + const doubleQuotedStringLiterals = new Set(); + + function collectDoubleQuotedStrings(node: TSESTree.Literal) { + if (isStringLiteral(node) && isDoubleQuoted(node)) { + doubleQuotedStringLiterals.add(node); + } + } + + function visitLocalizeCall(node: TSESTree.CallExpression) { + + // localize(key, message) + const [keyNode, messageNode] = (node).arguments; + + // (1) + // extract key so that it can be checked later + let key: string | undefined; + if (isStringLiteral(keyNode)) { + doubleQuotedStringLiterals.delete(keyNode); //todo@joh reconsider + key = keyNode.value; + + } else if (keyNode.type === AST_NODE_TYPES.ObjectExpression) { + for (let property of keyNode.properties) { + if (property.type === AST_NODE_TYPES.Property && !property.computed) { + if (property.key.type === AST_NODE_TYPES.Identifier && property.key.name === 'key') { + if (isStringLiteral(property.value)) { + doubleQuotedStringLiterals.delete(property.value); //todo@joh reconsider + key = property.value.value; + break; + } + } + } + } + } + if (typeof key === 'string') { + let array = externalizedStringLiterals.get(key); + if (!array) { + array = []; + externalizedStringLiterals.set(key, array); + } + array.push({ call: node, message: messageNode }); + } + + // (2) + // remove message-argument from doubleQuoted list and make + // sure it is a string-literal + doubleQuotedStringLiterals.delete(messageNode); + if (!isStringLiteral(messageNode)) { + context.report({ + loc: messageNode.loc, + messageId: 'badMessage', + data: { message: context.getSourceCode().getText(node) } + }); + } + } + + function reportBadStringsAndBadKeys() { + // (1) + // report all strings that are in double quotes + for (const node of doubleQuotedStringLiterals) { + context.report({ loc: node.loc, messageId: 'doubleQuoted' }); + } + + for (const [key, values] of externalizedStringLiterals) { + + // (2) + // report all invalid NLS keys + if (!key.match(NoUnexternalizedStrings._rNlsKeys)) { + for (let value of values) { + context.report({ loc: value.call.loc, messageId: 'badKey', data: { key } }); + } + } + + // (2) + // report all invalid duplicates (same key, different message) + if (values.length > 1) { + for (let i = 1; i < values.length; i++) { + if (context.getSourceCode().getText(values[i - 1].message) !== context.getSourceCode().getText(values[i].message)) { + context.report({ loc: values[i].call.loc, messageId: 'duplicateKey', data: { key } }); + } + } + } + } + } + + return { + ['Literal']: (node: any) => collectDoubleQuotedStrings(node), + ['ExpressionStatement[directive] Literal:exit']: (node: any) => doubleQuotedStringLiterals.delete(node), + ['CallExpression[callee.type="MemberExpression"][callee.object.name="nls"][callee.property.name="localize"]:exit']: (node: any) => visitLocalizeCall(node), + ['CallExpression[callee.name="localize"][arguments.length>=2]:exit']: (node: any) => visitLocalizeCall(node), + ['Program:exit']: reportBadStringsAndBadKeys, + }; + } +}; + diff --git a/build/eslint/code-no-unused-expressions.js b/build/eslint/code-no-unused-expressions.js new file mode 100644 index 00000000..80ae9a75 --- /dev/null +++ b/build/eslint/code-no-unused-expressions.js @@ -0,0 +1,141 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// FORKED FROM https://github.com/eslint/eslint/blob/b23ad0d789a909baf8d7c41a35bc53df932eaf30/lib/rules/no-unused-expressions.js +// and added support for `OptionalCallExpression`, see https://github.com/facebook/create-react-app/issues/8107 and https://github.com/eslint/eslint/issues/12642 + +/** + * @fileoverview Flag expressions in statement position that do not side effect + * @author Michael Ficarra + */ + +'use strict'; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + type: 'suggestion', + + docs: { + description: 'disallow unused expressions', + category: 'Best Practices', + recommended: false, + url: 'https://eslint.org/docs/rules/no-unused-expressions' + }, + + schema: [ + { + type: 'object', + properties: { + allowShortCircuit: { + type: 'boolean', + default: false + }, + allowTernary: { + type: 'boolean', + default: false + }, + allowTaggedTemplates: { + type: 'boolean', + default: false + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + const config = context.options[0] || {}, + allowShortCircuit = config.allowShortCircuit || false, + allowTernary = config.allowTernary || false, + allowTaggedTemplates = config.allowTaggedTemplates || false; + + /** + * @param {ASTNode} node any node + * @returns {boolean} whether the given node structurally represents a directive + */ + function looksLikeDirective(node) { + return node.type === 'ExpressionStatement' && + node.expression.type === 'Literal' && typeof node.expression.value === 'string'; + } + + /** + * @param {Function} predicate ([a] -> Boolean) the function used to make the determination + * @param {a[]} list the input list + * @returns {a[]} the leading sequence of members in the given list that pass the given predicate + */ + function takeWhile(predicate, list) { + for (let i = 0; i < list.length; ++i) { + if (!predicate(list[i])) { + return list.slice(0, i); + } + } + return list.slice(); + } + + /** + * @param {ASTNode} node a Program or BlockStatement node + * @returns {ASTNode[]} the leading sequence of directive nodes in the given node's body + */ + function directives(node) { + return takeWhile(looksLikeDirective, node.body); + } + + /** + * @param {ASTNode} node any node + * @param {ASTNode[]} ancestors the given node's ancestors + * @returns {boolean} whether the given node is considered a directive in its current position + */ + function isDirective(node, ancestors) { + const parent = ancestors[ancestors.length - 1], + grandparent = ancestors[ancestors.length - 2]; + + return (parent.type === 'Program' || parent.type === 'BlockStatement' && + (/Function/u.test(grandparent.type))) && + directives(parent).indexOf(node) >= 0; + } + + /** + * Determines whether or not a given node is a valid expression. Recurses on short circuit eval and ternary nodes if enabled by flags. + * @param {ASTNode} node any node + * @returns {boolean} whether the given node is a valid expression + */ + function isValidExpression(node) { + if (allowTernary) { + + // Recursive check for ternary and logical expressions + if (node.type === 'ConditionalExpression') { + return isValidExpression(node.consequent) && isValidExpression(node.alternate); + } + } + + if (allowShortCircuit) { + if (node.type === 'LogicalExpression') { + return isValidExpression(node.right); + } + } + + if (allowTaggedTemplates && node.type === 'TaggedTemplateExpression') { + return true; + } + + return /^(?:Assignment|OptionalCall|Call|New|Update|Yield|Await)Expression$/u.test(node.type) || + (node.type === 'UnaryExpression' && ['delete', 'void'].indexOf(node.operator) >= 0); + } + + return { + ExpressionStatement(node) { + if (!isValidExpression(node.expression) && !isDirective(node, context.getAncestors())) { + context.report({ node, message: 'Expected an assignment or function call and instead saw an expression.' }); + } + } + }; + + } +}; diff --git a/build/eslint/utils.js b/build/eslint/utils.js new file mode 100644 index 00000000..c58e4e24 --- /dev/null +++ b/build/eslint/utils.js @@ -0,0 +1,37 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createImportRuleListener = void 0; +function createImportRuleListener(validateImport) { + function _checkImport(node) { + if (node && node.type === 'Literal' && typeof node.value === 'string') { + validateImport(node, node.value); + } + } + return { + // import ??? from 'module' + ImportDeclaration: (node) => { + _checkImport(node.source); + }, + // import('module').then(...) OR await import('module') + ['CallExpression[callee.type="Import"][arguments.length=1] > Literal']: (node) => { + _checkImport(node); + }, + // import foo = ... + ['TSImportEqualsDeclaration > TSExternalModuleReference > Literal']: (node) => { + _checkImport(node); + }, + // export ?? from 'module' + ExportAllDeclaration: (node) => { + _checkImport(node.source); + }, + // export {foo} from 'module' + ExportNamedDeclaration: (node) => { + _checkImport(node.source); + }, + }; +} +exports.createImportRuleListener = createImportRuleListener; diff --git a/build/eslint/utils.ts b/build/eslint/utils.ts new file mode 100644 index 00000000..428832e9 --- /dev/null +++ b/build/eslint/utils.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as eslint from 'eslint'; +import { TSESTree } from '@typescript-eslint/experimental-utils'; + +export function createImportRuleListener(validateImport: (node: TSESTree.Literal, value: string) => any): eslint.Rule.RuleListener { + + function _checkImport(node: TSESTree.Node | null) { + if (node && node.type === 'Literal' && typeof node.value === 'string') { + validateImport(node, node.value); + } + } + + return { + // import ??? from 'module' + ImportDeclaration: (node: any) => { + _checkImport((node).source); + }, + // import('module').then(...) OR await import('module') + ['CallExpression[callee.type="Import"][arguments.length=1] > Literal']: (node: any) => { + _checkImport(node); + }, + // import foo = ... + ['TSImportEqualsDeclaration > TSExternalModuleReference > Literal']: (node: any) => { + _checkImport(node); + }, + // export ?? from 'module' + ExportAllDeclaration: (node: any) => { + _checkImport((node).source); + }, + // export {foo} from 'module' + ExportNamedDeclaration: (node: any) => { + _checkImport((node).source); + }, + + }; +} diff --git a/build/eslint/vscode-dts-create-func.js b/build/eslint/vscode-dts-create-func.js new file mode 100644 index 00000000..5a27bf51 --- /dev/null +++ b/build/eslint/vscode-dts-create-func.js @@ -0,0 +1,35 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +const experimental_utils_1 = require("@typescript-eslint/experimental-utils"); +module.exports = new class ApiLiteralOrTypes { + constructor() { + this.meta = { + docs: { url: 'https://github.com/microsoft/vscode/wiki/Extension-API-guidelines#creating-objects' }, + messages: { sync: '`createXYZ`-functions are constructor-replacements and therefore must return sync', } + }; + } + create(context) { + return { + ['TSDeclareFunction Identifier[name=/create.*/]']: (node) => { + var _a; + const decl = node.parent; + if (((_a = decl.returnType) === null || _a === void 0 ? void 0 : _a.typeAnnotation.type) !== experimental_utils_1.AST_NODE_TYPES.TSTypeReference) { + return; + } + if (decl.returnType.typeAnnotation.typeName.type !== experimental_utils_1.AST_NODE_TYPES.Identifier) { + return; + } + const ident = decl.returnType.typeAnnotation.typeName.name; + if (ident === 'Promise' || ident === 'Thenable') { + context.report({ + node, + messageId: 'sync' + }); + } + } + }; + } +}; diff --git a/build/eslint/vscode-dts-create-func.ts b/build/eslint/vscode-dts-create-func.ts new file mode 100644 index 00000000..295d099d --- /dev/null +++ b/build/eslint/vscode-dts-create-func.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as eslint from 'eslint'; +import { TSESTree, AST_NODE_TYPES } from '@typescript-eslint/experimental-utils'; + +export = new class ApiLiteralOrTypes implements eslint.Rule.RuleModule { + + readonly meta: eslint.Rule.RuleMetaData = { + docs: { url: 'https://github.com/microsoft/vscode/wiki/Extension-API-guidelines#creating-objects' }, + messages: { sync: '`createXYZ`-functions are constructor-replacements and therefore must return sync', } + }; + + create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { + + return { + ['TSDeclareFunction Identifier[name=/create.*/]']: (node: any) => { + + const decl = (node).parent; + + if (decl.returnType?.typeAnnotation.type !== AST_NODE_TYPES.TSTypeReference) { + return; + } + if (decl.returnType.typeAnnotation.typeName.type !== AST_NODE_TYPES.Identifier) { + return; + } + + const ident = decl.returnType.typeAnnotation.typeName.name; + if (ident === 'Promise' || ident === 'Thenable') { + context.report({ + node, + messageId: 'sync' + }); + } + } + }; + } +}; diff --git a/build/eslint/vscode-dts-event-naming.js b/build/eslint/vscode-dts-event-naming.js new file mode 100644 index 00000000..c93c1818 --- /dev/null +++ b/build/eslint/vscode-dts-event-naming.js @@ -0,0 +1,81 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +var _a; +const experimental_utils_1 = require("@typescript-eslint/experimental-utils"); +module.exports = new (_a = class ApiEventNaming { + constructor() { + this.meta = { + docs: { + url: 'https://github.com/microsoft/vscode/wiki/Extension-API-guidelines#event-naming' + }, + messages: { + naming: 'Event names must follow this patten: `on[Did|Will]`', + verb: 'Unknown verb \'{{verb}}\' - is this really a verb? Iff so, then add this verb to the configuration', + subject: 'Unknown subject \'{{subject}}\' - This subject has not been used before but it should refer to something in the API', + unknown: 'UNKNOWN event declaration, lint-rule needs tweaking' + } + }; + } + create(context) { + const config = context.options[0]; + const allowed = new Set(config.allowed); + const verbs = new Set(config.verbs); + return { + ['TSTypeAnnotation TSTypeReference Identifier[name="Event"]']: (node) => { + var _a, _b; + const def = (_b = (_a = node.parent) === null || _a === void 0 ? void 0 : _a.parent) === null || _b === void 0 ? void 0 : _b.parent; + let ident; + if ((def === null || def === void 0 ? void 0 : def.type) === experimental_utils_1.AST_NODE_TYPES.Identifier) { + ident = def; + } + else if (((def === null || def === void 0 ? void 0 : def.type) === experimental_utils_1.AST_NODE_TYPES.TSPropertySignature || (def === null || def === void 0 ? void 0 : def.type) === experimental_utils_1.AST_NODE_TYPES.ClassProperty) && def.key.type === experimental_utils_1.AST_NODE_TYPES.Identifier) { + ident = def.key; + } + if (!ident) { + // event on unknown structure... + return context.report({ + node, + message: 'unknown' + }); + } + if (allowed.has(ident.name)) { + // configured exception + return; + } + const match = ApiEventNaming._nameRegExp.exec(ident.name); + if (!match) { + context.report({ + node: ident, + messageId: 'naming' + }); + return; + } + // check that is spelled out (configured) as verb + if (!verbs.has(match[2].toLowerCase())) { + context.report({ + node: ident, + messageId: 'verb', + data: { verb: match[2] } + }); + } + // check that a subject (if present) has occurred + if (match[3]) { + const regex = new RegExp(match[3], 'ig'); + const parts = context.getSourceCode().getText().split(regex); + if (parts.length < 3) { + context.report({ + node: ident, + messageId: 'subject', + data: { subject: match[3] } + }); + } + } + } + }; + } + }, + _a._nameRegExp = /on(Did|Will)([A-Z][a-z]+)([A-Z][a-z]+)?/, + _a); diff --git a/build/eslint/vscode-dts-event-naming.ts b/build/eslint/vscode-dts-event-naming.ts new file mode 100644 index 00000000..6543c458 --- /dev/null +++ b/build/eslint/vscode-dts-event-naming.ts @@ -0,0 +1,91 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as eslint from 'eslint'; +import { TSESTree, AST_NODE_TYPES } from '@typescript-eslint/experimental-utils'; + +export = new class ApiEventNaming implements eslint.Rule.RuleModule { + + private static _nameRegExp = /on(Did|Will)([A-Z][a-z]+)([A-Z][a-z]+)?/; + + readonly meta: eslint.Rule.RuleMetaData = { + docs: { + url: 'https://github.com/microsoft/vscode/wiki/Extension-API-guidelines#event-naming' + }, + messages: { + naming: 'Event names must follow this patten: `on[Did|Will]`', + verb: 'Unknown verb \'{{verb}}\' - is this really a verb? Iff so, then add this verb to the configuration', + subject: 'Unknown subject \'{{subject}}\' - This subject has not been used before but it should refer to something in the API', + unknown: 'UNKNOWN event declaration, lint-rule needs tweaking' + } + }; + + create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { + + const config = <{ allowed: string[], verbs: string[] }>context.options[0]; + const allowed = new Set(config.allowed); + const verbs = new Set(config.verbs); + + return { + ['TSTypeAnnotation TSTypeReference Identifier[name="Event"]']: (node: any) => { + + const def = (node).parent?.parent?.parent; + let ident: TSESTree.Identifier | undefined; + + if (def?.type === AST_NODE_TYPES.Identifier) { + ident = def; + + } else if ((def?.type === AST_NODE_TYPES.TSPropertySignature || def?.type === AST_NODE_TYPES.ClassProperty) && def.key.type === AST_NODE_TYPES.Identifier) { + ident = def.key; + } + + if (!ident) { + // event on unknown structure... + return context.report({ + node, + message: 'unknown' + }); + } + + if (allowed.has(ident.name)) { + // configured exception + return; + } + + const match = ApiEventNaming._nameRegExp.exec(ident.name); + if (!match) { + context.report({ + node: ident, + messageId: 'naming' + }); + return; + } + + // check that is spelled out (configured) as verb + if (!verbs.has(match[2].toLowerCase())) { + context.report({ + node: ident, + messageId: 'verb', + data: { verb: match[2] } + }); + } + + // check that a subject (if present) has occurred + if (match[3]) { + const regex = new RegExp(match[3], 'ig'); + const parts = context.getSourceCode().getText().split(regex); + if (parts.length < 3) { + context.report({ + node: ident, + messageId: 'subject', + data: { subject: match[3] } + }); + } + } + } + }; + } +}; + diff --git a/build/eslint/vscode-dts-interface-naming.js b/build/eslint/vscode-dts-interface-naming.js new file mode 100644 index 00000000..70ca8108 --- /dev/null +++ b/build/eslint/vscode-dts-interface-naming.js @@ -0,0 +1,30 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +var _a; +module.exports = new (_a = class ApiInterfaceNaming { + constructor() { + this.meta = { + messages: { + naming: 'Interfaces must not be prefixed with uppercase `I`', + } + }; + } + create(context) { + return { + ['TSInterfaceDeclaration Identifier']: (node) => { + const name = node.name; + if (ApiInterfaceNaming._nameRegExp.test(name)) { + context.report({ + node, + messageId: 'naming' + }); + } + } + }; + } + }, + _a._nameRegExp = /I[A-Z]/, + _a); diff --git a/build/eslint/vscode-dts-interface-naming.ts b/build/eslint/vscode-dts-interface-naming.ts new file mode 100644 index 00000000..d9ec4e8c --- /dev/null +++ b/build/eslint/vscode-dts-interface-naming.ts @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as eslint from 'eslint'; +import { TSESTree } from '@typescript-eslint/experimental-utils'; + +export = new class ApiInterfaceNaming implements eslint.Rule.RuleModule { + + private static _nameRegExp = /I[A-Z]/; + + readonly meta: eslint.Rule.RuleMetaData = { + messages: { + naming: 'Interfaces must not be prefixed with uppercase `I`', + } + }; + + create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { + + return { + ['TSInterfaceDeclaration Identifier']: (node: any) => { + + const name = (node).name; + if (ApiInterfaceNaming._nameRegExp.test(name)) { + context.report({ + node, + messageId: 'naming' + }); + } + } + }; + } +}; + diff --git a/build/eslint/vscode-dts-literal-or-types.js b/build/eslint/vscode-dts-literal-or-types.js new file mode 100644 index 00000000..02e6de87 --- /dev/null +++ b/build/eslint/vscode-dts-literal-or-types.js @@ -0,0 +1,23 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +module.exports = new class ApiLiteralOrTypes { + constructor() { + this.meta = { + docs: { url: 'https://github.com/microsoft/vscode/wiki/Extension-API-guidelines#enums' }, + messages: { useEnum: 'Use enums, not literal-or-types', } + }; + } + create(context) { + return { + ['TSTypeAnnotation TSUnionType TSLiteralType']: (node) => { + context.report({ + node: node, + messageId: 'useEnum' + }); + } + }; + } +}; diff --git a/build/eslint/vscode-dts-literal-or-types.ts b/build/eslint/vscode-dts-literal-or-types.ts new file mode 100644 index 00000000..01a3eb21 --- /dev/null +++ b/build/eslint/vscode-dts-literal-or-types.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as eslint from 'eslint'; + +export = new class ApiLiteralOrTypes implements eslint.Rule.RuleModule { + + readonly meta: eslint.Rule.RuleMetaData = { + docs: { url: 'https://github.com/microsoft/vscode/wiki/Extension-API-guidelines#enums' }, + messages: { useEnum: 'Use enums, not literal-or-types', } + }; + + create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { + return { + ['TSTypeAnnotation TSUnionType TSLiteralType']: (node: any) => { + context.report({ + node: node, + messageId: 'useEnum' + }); + } + }; + } +}; diff --git a/build/hygiene.js b/build/hygiene.js new file mode 100644 index 00000000..e3326f55 --- /dev/null +++ b/build/hygiene.js @@ -0,0 +1,289 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +const filter = require('gulp-filter'); +const es = require('event-stream'); +const tsfmt = require('typescript-formatter'); +const gulpeslint = require('gulp-eslint'); +const VinylFile = require('vinyl'); +const vfs = require('vinyl-fs'); +const path = require('path'); +const fs = require('fs'); +const pall = require('p-all'); + +/** + * Hygiene works by creating cascading subsets of all our files and + * passing them through a sequence of checks. Here are the current subsets, + * named according to the checks performed on them. Each subset contains + * the following one, as described in mathematical notation: + * + * all ⊃ eol ⊇ indentation ⊃ copyright ⊃ typescript + */ + +const all = [ + '*', + 'build/**/*', + 'scripts/**/*', + 'src/**/*', + '!**/node_modules/**' +]; + +const indentationFilter = [ + '**', + + // except specific files + '!ThirdPartyNotices.txt', + '!LICENSE.{txt,rtf}', + '!LICENSES.chromium.html', + '!**/LICENSE', + + // except multiple specific files + '!**/package.json', + '!**/yarn.lock', + '!**/yarn-error.log', + '!**/*.tgz', + + // except specific file types + '!src/vs/*/**/*.d.ts', + '!**/typings/**/*.d.ts', + '!extensions/**/*.d.ts', + '!**/*.{svg,exe,png,bmp,scpt,bat,cmd,cur,ttf,woff,eot,md,ps1,template,yaml,yml,d.ts.recipe,ico,icns}', + '!build/{lib,download}/**/*.js', + '!build/**/*.sh', + '!build/azure-pipelines/**/*.js', + '!build/azure-pipelines/**/*.config', + '!**/Dockerfile', + '!**/Dockerfile.*', + '!**/*.Dockerfile', + '!**/*.dockerfile', +]; + +const copyrightFilter = [ + '**', + '!**/*.desktop', + '!**/*.json', + '!**/*.html', + '!**/*.template', + '!**/*.md', + '!**/*.bat', + '!**/*.cmd', + '!**/*.ico', + '!**/*.icns', + '!**/*.xml', + '!**/*.sh', + '!**/*.tgz', + '!**/*.txt', + '!**/*.xpm', + '!**/*.opts', + '!**/*.disabled', + '!**/*.code-workspace', + '!build/**/*.init', + '!src/async.ts', + '!**/typings/**.*' +]; + +const tsHygieneFilter = [ + 'src/**/*.ts', + 'test/**/*.ts', + '!**/fixtures/**', + '!**/typings/**', + '!**/node_modules/**', +]; + +const copyrightHeaderLines = [ + '/*---------------------------------------------------------------------------------------------', + ' * Copyright (c) Microsoft Corporation. All rights reserved.', + ' *--------------------------------------------------------------------------------------------*/' +]; + +function hygiene(some) { + let errorCount = 0; + + const indentation = es.through(function (file) { + const lines = file.contents.toString('utf8').split(/\r\n|\r|\n/); + file.__lines = lines; + + lines + .forEach((line, i) => { + if (/^\s*$/.test(line)) { + // empty or whitespace lines are OK + } else if (/^[\t]*[^\s]/.test(line)) { + // good indent + } else if (/^[\t]* \*/.test(line)) { + // block comment using an extra space + } else { + console.error(file.relative + '(' + (i + 1) + ',1): Bad whitespace indentation'); + errorCount++; + } + }); + + this.emit('data', file); + }); + + const copyrights = es.through(function (file) { + const lines = file.__lines; + + for (let i = 0; i < copyrightHeaderLines.length; i++) { + if (lines[i] !== copyrightHeaderLines[i]) { + console.error(file.relative + ': Missing or bad copyright statement'); + errorCount++; + break; + } + } + + this.emit('data', file); + }); + + const formatting = es.map(function (file, cb) { + tsfmt.processString(file.path, file.contents.toString('utf8'), { + verify: false, + tsfmt: true, + // verbose: true, + // keep checkJS happy + editorconfig: undefined, + replace: undefined, + tsconfig: undefined, + tsconfigFile: undefined, + tsfmtFile: undefined, + vscode: undefined, + vscodeFile: undefined + }).then(result => { + let original = result.src.replace(/\r\n/gm, '\n'); + let formatted = result.dest.replace(/\r\n/gm, '\n'); + + if (original !== formatted) { + console.error(`File not formatted. Run the 'Format Document' command to fix it:`, file.relative); + errorCount++; + } + cb(null, file); + + }, err => { + cb(err); + }); + }); + + let input; + + if (Array.isArray(some) || typeof some === 'string' || !some) { + input = vfs.src(some || all, { base: '.', follow: true, allowEmpty: true }); + } else { + input = some; + } + + const result = input + .pipe(filter(f => !f.stat.isDirectory())) + .pipe(filter(indentationFilter)) + .pipe(indentation) + .pipe(filter(copyrightFilter)) + .pipe(copyrights) + .pipe(filter(tsHygieneFilter)) + .pipe(formatting) + .pipe(gulpeslint({ + configFile: '.eslintrc.js', + rulePaths: ['./build/eslint'] + })) + .pipe(gulpeslint.formatEach('compact')) + .pipe(gulpeslint.result(result => { + errorCount += result.warningCount; + errorCount += result.errorCount; + })); + + let count = 0; + return result + .pipe(es.through(function (data) { + count++; + if (process.env['TRAVIS'] && count % 10 === 0) { + process.stdout.write('.'); + } + this.emit('data', data); + }, function () { + process.stdout.write('\n'); + if (errorCount > 0) { + this.emit('error', 'Hygiene failed with ' + errorCount + ' errors. Check \'build/gulpfile.hygiene.js\'.'); + } else { + this.emit('end'); + } + })); +} + +function createGitIndexVinyls(paths) { + const cp = require('child_process'); + const repositoryPath = process.cwd(); + + const fns = paths.map(relativePath => () => new Promise((c, e) => { + const fullPath = path.join(repositoryPath, relativePath); + + fs.stat(fullPath, (err, stat) => { + if (err && err.code === 'ENOENT') { // ignore deletions + return c(null); + } else if (err) { + return e(err); + } + + cp.exec(`git show :${relativePath}`, { maxBuffer: 2000 * 1024, encoding: 'buffer' }, (err, out) => { + if (err) { + return e(err); + } + + c(new VinylFile({ + path: fullPath, + base: repositoryPath, + contents: out, + stat + })); + }); + }); + })); + + return pall(fns, { concurrency: 4 }) + .then(r => r.filter(p => !!p)); +} + +// this allows us to run hygiene as a git pre-commit hook +if (require.main === module) { + const cp = require('child_process'); + + process.on('unhandledRejection', (reason, p) => { + console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); + process.exit(1); + }); + + if (process.argv.length > 2) { + hygiene(process.argv.slice(2)).on('error', err => { + console.error(); + console.error(err); + process.exit(1); + }); + } else { + cp.exec('git diff --cached --name-only', { maxBuffer: 2000 * 1024 }, (err, out) => { + if (err) { + console.error(); + console.error(err); + process.exit(1); + return; + } + + const some = out + .split(/\r?\n/) + .filter(l => !!l); + + if (some.length > 0) { + console.log('Reading git index versions...'); + + createGitIndexVinyls(some) + .then(vinyls => new Promise((c, e) => hygiene(es.readArray(vinyls)) + .on('end', () => c()) + .on('error', e))) + .catch(err => { + console.error(); + console.error(err); + process.exit(1); + }); + } + }); + } +} diff --git a/cli.js b/cli.js new file mode 100755 index 00000000..0d06227a --- /dev/null +++ b/cli.js @@ -0,0 +1,7 @@ +#!/usr/bin/env node +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +require('./dist/spec-node/devContainersSpecCLI'); diff --git a/images/README.md b/images/README.md new file mode 100644 index 00000000..64914842 --- /dev/null +++ b/images/README.md @@ -0,0 +1,3 @@ +## Images + +Images part of the dev container CLI repository. diff --git a/images/dev-container-stages.png b/images/dev-container-stages.png new file mode 100644 index 00000000..b4b1d774 Binary files /dev/null and b/images/dev-container-stages.png differ diff --git a/package.json b/package.json new file mode 100644 index 00000000..c0407958 --- /dev/null +++ b/package.json @@ -0,0 +1,79 @@ +{ + "name": "dev-containers-cli", + "description": "Dev Containers CLI", + "version": "0.1.0", + "bin": "cli.js", + "author": "Microsoft Corporation", + "repository": { + "type": "git", + "url": "https://github.com/microsoft/vscode-remote-containers.git" + }, + "bugs": { + "url": "https://github.com/microsoft/vscode-remote-release/issues" + }, + "license": "SEE LICENSE IN LICENSE.txt", + "engines": { + "node": ">=14.0.0" + }, + "scripts": { + "compile": "npm-run-all clean definitions tsc-b", + "tsc-b": "tsc -b", + "watch": "npm-run-all clean definitions tsc-b-w", + "tsc-b-w": "tsc -b -w", + "precommit": "node build/hygiene.js", + "definitions": "npm-run-all definitions-clean definitions-copy", + "lint": "eslint -c .eslintrc.js --rulesdir ./build/eslint --ext .ts ./src ./test", + "definitions-clean": "rimraf dist/node_modules/vscode-dev-containers", + "definitions-copy": "copyfiles --all \"node_modules/vscode-dev-containers/**/*\" dist", + "package": "npm-run-all compile npm-pack", + "npm-pack": "npm pack", + "clean": "rimraf dist", + "test": "env TS_NODE_PROJECT=src/test/tsconfig.json mocha -r ts-node/register --exit src/test/*.test.ts", + "test-container-features": "env TS_NODE_PROJECT=src/test/tsconfig.json mocha -r ts-node/register --exit src/test/container-features/**/*.test.ts", + "test-container-features-offline": "env TS_NODE_PROJECT=src/test/tsconfig.json mocha -r ts-node/register --exit src/test/container-features/**/*.offline.test.ts", + "test-container-features-online": "env TS_NODE_PROJECT=src/test/tsconfig.json mocha -r ts-node/register --exit src/test/container-features/**/*.online.test.ts" + }, + "devDependencies": { + "@types/chai": "^4.3.0", + "@types/follow-redirects": "^1.13.1", + "@types/js-yaml": "^4.0.5", + "@types/mocha": "^9.1.0", + "@types/node": "^16.11.7", + "@types/pull-stream": "^3.6.2", + "@types/semver": "^7.3.9", + "@types/shell-quote": "^1.7.1", + "@types/tar": "^6.1.1", + "@types/yargs": "^17.0.8", + "@typescript-eslint/eslint-plugin": "^4.31.2", + "@typescript-eslint/parser": "^4.31.2", + "chai": "^4.3.4", + "copyfiles": "^2.4.1", + "eslint": "^7.32.0", + "event-stream": "^4.0.1", + "gulp-eslint": "^6.0.0", + "gulp-filter": "^7.0.0", + "mocha": "^9.2.1", + "npm-run-all": "^4.1.5", + "p-all": "^4.0.0", + "rimraf": "^3.0.2", + "ts-node": "^10.4.0", + "typescript": "^4.5.5", + "typescript-formatter": "^7.2.2", + "vinyl": "^2.2.1", + "vinyl-fs": "^3.0.3" + }, + "dependencies": { + "follow-redirects": "^1.14.8", + "js-yaml": "^4.1.0", + "jsonc-parser": "^3.0.0", + "node-pty": "^0.10.1", + "pull-stream": "^3.6.14", + "semver": "^7.3.5", + "shell-quote": "^1.7.3", + "stream-to-pull-stream": "^1.7.3", + "tar": "^6.1.11", + "vscode-dev-containers": "https://github.com/microsoft/vscode-dev-containers/releases/download/v0.233.0/vscode-dev-containers-0.233.0.tgz", + "vscode-uri": "^3.0.3", + "yargs": "~17.0.1" + } +} diff --git a/schemas/devcontainer-features.schema.json b/schemas/devcontainer-features.schema.json new file mode 100644 index 00000000..d17d4c61 --- /dev/null +++ b/schemas/devcontainer-features.schema.json @@ -0,0 +1,266 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/FeaturesJson", + "definitions": { + "FeaturesJson": { + "type": "object", + "properties": { + "features": { + "type": "array", + "items": { + "$ref": "#/definitions/Feature" + } + } + }, + "required": [ + "features" + ], + "additionalProperties": false + }, + "Feature": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Identifier for this feature. Used in the devcontainer.json to refer to this feature.", + "examples": ["docker-in-docker"] + }, + "name": { + "type": "string", + "description": "Name for this feature. Show to the user in the UI.", + "examples": ["Docker in Docker"] + }, + "documentationURL": { + "type": "string", + "description": "URL for documentation on this feature.", + "examples": ["https://example.com/docs/docker-in-docker"] + }, + "options": { + "type": "object", + "description": "Options provided are written to a devcontainer-features.env file to be sourced during image build. Environment variables follow the format _BUILD_ARG__.", + "additionalProperties": { + "$ref": "#/definitions/FeatureOption" + } + }, + "buildArg": { + "type": "string", + "description": "DEPRECATED: Old property used for temporary compatibility." + }, + "containerEnv": { + "type": "object", + "description": "Container environment variables.", + "additionalProperties": { + "examples": [ { "DOCKER__BUILDKIT": "1" } ], + "type": "string", + "description": "Environment variables to inject into the Dockerfile at build-time." + } + }, + "mounts": { + "type": "array", + "description": "Mount points to set up when creating the container.", + "items": { + "$ref": "#/definitions/Mount" + } + }, + "extensions": { + "type": "array", + "description": "An array of extensions that should be installed into the container.", + "examples": [ "ms-azuretools.vscode-docker" ], + "items": { + "type": "string", + "description": "VS Code extension identifiers to install." + } + }, + "settings": { + "$ref": "vscode://schemas/settings/machine", + "type": "object", + "description": "Machine specific settings that should be copied into the container." + }, + "init": { + "type": "boolean", + "description": "Passes the --init flag at runtime." + }, + "privileged": { + "type": "boolean", + "description": "Passes the --privileged flag at runtime." + }, + "capAdd": { + "type": "array", + "description": "Passes docker capabilities to include at runtime.", + "examples": [ "SYS_PTRACE" ], + "items": { + "type": "string" + } + }, + "securityOpt": { + "type": "array", + "description": "Passes docker security options to include at runtime.", + "examples": [ "seccomp=unconfined" ], + "items": { + "type": "string" + } + }, + "entrypoint": { + "type": "string", + "description": "Provides a custom entrypoint.", + "examples": ["/usr/local/share/docker-init.sh"] + }, + "include": { + "type": "array", + "description": "Base definitions that permit this features to be composed onto it.", + "examples": [ ["ubuntu", "java"] ], + "items": { + "type": "string" + } + }, + "exclude": { + "type": "array", + "description": "Base definitions that do not permit this features to be composed onto it.", + "examples": [ ["ubuntu", "java"] ], + "items": { + "type": "string" + } + }, + "vscode": { + "type": "object", + "properties": { + "extensions": { + "type": "array", + "description": "An array of extensions that should be installed into the container.", + "examples": [ "ms-azuretools.vscode-docker" ], + "items": { + "type": "string", + "description": "VS Code extension identifiers to install." + } + }, + "settings": { + "$ref": "vscode://schemas/settings/machine", + "type": "object", + "description": "Machine specific settings that should be copied into the container." + } + } + } + }, + "required": [ + "id", + "name" + ], + "additionalProperties": false + }, + "FeatureOption": { + "anyOf": [ + { + "type": "object", + "description": "Option identifier", + "properties": { + "type": { + "type": "string", + "const": "boolean", + "description": "Option type." + }, + "default": { + "type": "boolean", + "description": "A default value if none provided." + }, + "description": { + "description": "Optional hint for the given option.", + "type": "string" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "description": "Option identifier.", + "properties": { + "type": { + "type": "string", + "const": "string", + "description": "Option type." + }, + "enum": { + "type": "array", + "description": "Enumeration of all possible values for the given option. Custom values are not accepted.", + "items": { + "type": "string" + } + }, + "default": { + "type": "string", + "description": "A default value if none provided." + }, + "description": { + "description": "Optional hint for the given option.", + "type": "string" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "description": "Option identifier.", + "properties": { + "type": { + "type": "string", + "const": "string", + "description": "Option type." + }, + "proposals": { + "type": "array", + "description": "List of suggested values for the given option. Custom values are also accepted.", + "examples": [ [ "latest", "20.10" ]], + "items": { + "type": "string" + } + }, + "default": { + "type": "string", + "description": "A default value if none provided." + }, + "description": { + "description": "Optional hint for the given option.", + "type": "string" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + } + ] + }, + "Mount": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "bind", + "volume" + ], + "description": "Mount type." + }, + "source": { + "type": "string", + "description": "Mount source." + }, + "target": { + "type": "string", + "description": "Mount target." + } + }, + "required": [ + "type", + "source", + "target" + ], + "additionalProperties": false + } + } +} \ No newline at end of file diff --git a/scripts/gitAskPass.sh b/scripts/gitAskPass.sh new file mode 100755 index 00000000..e4b1621c --- /dev/null +++ b/scripts/gitAskPass.sh @@ -0,0 +1,2 @@ +#!/bin/sh +echo "$GIT_TOKEN" \ No newline at end of file diff --git a/scripts/updateUID.Dockerfile b/scripts/updateUID.Dockerfile new file mode 100644 index 00000000..64ef383e --- /dev/null +++ b/scripts/updateUID.Dockerfile @@ -0,0 +1,33 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +ARG BASE_IMAGE +FROM $BASE_IMAGE + +USER root + +ARG REMOTE_USER +ARG NEW_UID +ARG NEW_GID +SHELL ["/bin/sh", "-c"] +RUN eval $(sed -n "s/${REMOTE_USER}:[^:]*:\([^:]*\):\([^:]*\):[^:]*:\([^:]*\).*/OLD_UID=\1;OLD_GID=\2;HOME_FOLDER=\3/p" /etc/passwd); \ + eval $(sed -n "s/\([^:]*\):[^:]*:${NEW_UID}:.*/EXISTING_USER=\1/p" /etc/passwd); \ + eval $(sed -n "s/\([^:]*\):[^:]*:${NEW_GID}:.*/EXISTING_GROUP=\1/p" /etc/group); \ + if [ -z "$OLD_UID" ]; then \ + echo "Remote user not found in /etc/passwd ($REMOTE_USER)."; \ + elif [ "$OLD_UID" = "$NEW_UID" -a "$OLD_GID" = "$NEW_GID" ]; then \ + echo "UIDs and GIDs are the same ($NEW_UID:$NEW_GID)."; \ + elif [ "$OLD_UID" != "$NEW_UID" -a -n "$EXISTING_USER" ]; then \ + echo "User with UID exists ($EXISTING_USER=$NEW_UID)."; \ + elif [ "$OLD_GID" != "$NEW_GID" -a -n "$EXISTING_GROUP" ]; then \ + echo "Group with GID exists ($EXISTING_GROUP=$NEW_GID)."; \ + else \ + echo "Updating UID:GID from $OLD_UID:$OLD_GID to $NEW_UID:$NEW_GID."; \ + sed -i -e "s/\(${REMOTE_USER}:[^:]*:\)[^:]*:[^:]*/\1${NEW_UID}:${NEW_GID}/" /etc/passwd; \ + if [ "$OLD_GID" != "$NEW_GID" ]; then \ + sed -i -e "s/\([^:]*:[^:]*:\)${OLD_GID}:/\1${NEW_GID}:/" /etc/group; \ + fi; \ + chown -R $NEW_UID:$NEW_GID $HOME_FOLDER; \ + fi; + +ARG IMAGE_USER +USER $IMAGE_USER diff --git a/src/spec-common/async.ts b/src/spec-common/async.ts new file mode 100644 index 00000000..baff3891 --- /dev/null +++ b/src/spec-common/async.ts @@ -0,0 +1,8 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export async function delay(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); +} diff --git a/src/spec-common/cliHost.ts b/src/spec-common/cliHost.ts new file mode 100644 index 00000000..d2c57aa0 --- /dev/null +++ b/src/spec-common/cliHost.ts @@ -0,0 +1,113 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as path from 'path'; +import * as net from 'net'; +import * as os from 'os'; + +import { readLocalFile, writeLocalFile, mkdirpLocal, isLocalFile, renameLocal, readLocalDir, isLocalFolder } from '../spec-utils/pfs'; +import { URI } from 'vscode-uri'; +import { ExecFunction, plainExec, plainPtyExec, PtyExecFunction } from './commonUtils'; +import { Duplex } from 'pull-stream'; + +const toPull = require('stream-to-pull-stream'); + + +export type CLIHostType = 'local' | 'wsl' | 'container' | 'ssh'; + +export interface CLIHost { + type: CLIHostType; + platform: NodeJS.Platform; + exec: ExecFunction; + ptyExec: PtyExecFunction; + cwd: string; + env: NodeJS.ProcessEnv; + path: typeof path.posix | typeof path.win32; + homedir(): Promise; + tmpdir(): Promise; + isFile(filepath: string): Promise; + isFolder(filepath: string): Promise; + readFile(filepath: string): Promise; + writeFile(filepath: string, content: Buffer): Promise; + rename(oldPath: string, newPath: string): Promise; + mkdirp(dirpath: string): Promise; + readDir(dirpath: string): Promise; + readDirWithTypes?(dirpath: string): Promise<[string, FileTypeBitmask][]>; + getuid(): Promise; + getgid(): Promise; + toCommonURI(filePath: string): Promise; + connect: ConnectFunction; + reconnect?(): Promise; + terminate?(): Promise; +} + +export type ConnectFunction = (socketPath: string) => Duplex; + +export enum FileTypeBitmask { + Unknown = 0, + File = 1, + Directory = 2, + SymbolicLink = 64 +} + +export async function getCLIHost(localCwd: string, loadNativeModule: (moduleName: string) => Promise): Promise { + const exec = plainExec(localCwd); + const ptyExec = await plainPtyExec(localCwd, loadNativeModule); + return createLocalCLIHostFromExecFunctions(localCwd, exec, ptyExec, connectLocal); +} + +function createLocalCLIHostFromExecFunctions(localCwd: string, exec: ExecFunction, ptyExec: PtyExecFunction, connect: ConnectFunction): CLIHost { + return { + type: 'local', + platform: process.platform, + exec, + ptyExec, + cwd: localCwd, + env: process.env, + path: path, + homedir: async () => os.homedir(), + tmpdir: async () => os.tmpdir(), + isFile: isLocalFile, + isFolder: isLocalFolder, + readFile: readLocalFile, + writeFile: writeLocalFile, + rename: renameLocal, + mkdirp: async (dirpath) => { + await mkdirpLocal(dirpath); + }, + readDir: readLocalDir, + getuid: async () => process.getuid(), + getgid: async () => process.getgid(), + toCommonURI: async (filePath) => URI.file(filePath), + connect, + }; +} + +function connectLocal(socketPath: string) { + if (process.platform !== 'win32' || socketPath.startsWith('\\\\.\\pipe\\')) { + return toPull.duplex(net.connect(socketPath)); + } + + const socket = new net.Socket(); + (async () => { + const buf = await readLocalFile(socketPath); + const i = buf.indexOf(0xa); + const port = parseInt(buf.slice(0, i).toString(), 10); + const guid = buf.slice(i + 1); + socket.connect(port, '127.0.0.1', () => { + socket.write(guid, err => { + if (err) { + console.error(err); + socket.destroy(); + } + }); + }); + })() + .catch(err => { + console.error(err); + socket.destroy(); + }); + return toPull.duplex(socket); +} diff --git a/src/spec-common/commonUtils.ts b/src/spec-common/commonUtils.ts new file mode 100644 index 00000000..ec5382a8 --- /dev/null +++ b/src/spec-common/commonUtils.ts @@ -0,0 +1,457 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Writable, Readable } from 'stream'; +import * as path from 'path'; +import * as fs from 'fs'; +import * as cp from 'child_process'; +import * as ptyType from 'node-pty'; +import { StringDecoder } from 'string_decoder'; + +import { toErrorText } from './errors'; +import { Disposable, Event } from '../spec-utils/event'; +import { isLocalFile } from '../spec-utils/pfs'; +import { Log } from '../spec-utils/log'; +import { ShellServer } from './shellServer'; + +export { CLIHost, getCLIHost } from './cliHost'; + +export interface Exec { + stdin: Writable; + stdout: Readable; + stderr: Readable; + exit: Promise<{ code: number | null; signal: string | null }>; + terminate(): Promise; +} + +export interface ExecParameters { + env?: NodeJS.ProcessEnv; + cwd?: string; + cmd: string; + args?: string[]; + output: Log; +} + +export interface ExecFunction { + (params: ExecParameters): Promise; +} + +export interface PtyExec { + onData: Event; + write(data: string): void; + resize(cols: number, rows: number): void; + exit: Promise<{ code: number | undefined; signal: number | undefined }>; + terminate(): Promise; +} + +export interface PtyExecParameters { + env?: NodeJS.ProcessEnv; + cwd?: string; + cmd: string; + args?: string[]; + cols?: number; + rows?: number; + output: Log; +} + +export interface PtyExecFunction { + (params: PtyExecParameters): Promise; +} + +export function equalPaths(platform: NodeJS.Platform, a: string, b: string) { + if (platform === 'linux') { + return a === b; + } + return a.toLowerCase() === b.toLowerCase(); +} + +export const tsnode = path.join(__dirname, '..', '..', 'node_modules', '.bin', 'ts-node'); +export const isTsnode = path.basename(process.argv[0]) === 'ts-node' || process.argv.indexOf('ts-node/register') !== -1; + +export async function runCommandNoPty(options: { + exec: ExecFunction; + cmd: string; + args?: string[]; + cwd?: string; + env?: NodeJS.ProcessEnv; + stdin?: Buffer | fs.ReadStream | Event; + output: Log; + print?: boolean | 'continuous' | 'onerror'; +}) { + const { exec, cmd, args, cwd, env, stdin, output, print } = options; + + const p = await exec({ + cmd, + args, + cwd, + env, + output, + }); + + return new Promise<{ stdout: Buffer; stderr: Buffer }>((resolve, reject) => { + const stdout: Buffer[] = []; + const stderr: Buffer[] = []; + + const stdoutDecoder = print === 'continuous' ? new StringDecoder() : undefined; + p.stdout.on('data', (chunk: Buffer) => { + stdout.push(chunk); + if (print === 'continuous') { + output.write(stdoutDecoder!.write(chunk)); + } + }); + p.stdout.on('error', (err: any) => { + // ENOTCONN seen with missing executable in addition to ENOENT on child_process. + if (err?.code !== 'ENOTCONN') { + throw err; + } + }); + const stderrDecoder = print === 'continuous' ? new StringDecoder() : undefined; + p.stderr.on('data', (chunk: Buffer) => { + stderr.push(chunk); + if (print === 'continuous') { + output.write(toErrorText(stderrDecoder!.write(chunk))); + } + }); + p.stderr.on('error', (err: any) => { + // ENOTCONN seen with missing executable in addition to ENOENT on child_process. + if (err?.code !== 'ENOTCONN') { + throw err; + } + }); + const subs: Disposable[] = []; + p.exit.then(({ code }) => { + try { + subs.forEach(sub => sub.dispose()); + const stdoutBuf = Buffer.concat(stdout); + const stderrBuf = Buffer.concat(stderr); + if (print === true || (code && print === 'onerror')) { + output.write(stdoutBuf.toString().replace(/\r?\n/g, '\r\n')); + output.write(toErrorText(stderrBuf.toString())); + } + if (print && code) { + output.write(`Exit code ${code}`); + } + if (code) { + reject({ + message: `Command failed: ${cmd} ${(args || []).join(' ')}`, + stdout: stdoutBuf, + stderr: stderrBuf, + code + }); + } else { + resolve({ + stdout: stdoutBuf, + stderr: stderrBuf, + }); + } + } catch (e) { + reject(e); + } + }, reject); + if (stdin instanceof Buffer) { + p.stdin.write(stdin, err => { + if (err) { + reject(err); + } + }); + p.stdin.end(); + } else if (stdin instanceof fs.ReadStream) { + stdin.pipe(p.stdin); + } else if (typeof stdin === 'function') { + subs.push(stdin(buf => p.stdin.write(buf))); + } + }); +} + +export async function runCommand(options: { + ptyExec: PtyExecFunction; + cmd: string; + args?: string[]; + cwd?: string; + env?: NodeJS.ProcessEnv; + output: Log; + resolveOn?: RegExp; + onDidInput?: Event; +}) { + const { ptyExec, cmd, args, cwd, env, output, resolveOn, onDidInput } = options; + + const p = await ptyExec({ + cmd, + args, + cwd, + env, + output: output, + }); + + return new Promise<{ cmdOutput: string }>((resolve, reject) => { + let cmdOutput = ''; + + const subs = [ + onDidInput && onDidInput(data => p.write(data)), + ]; + + p.onData(chunk => { + cmdOutput += chunk; + output.raw(chunk); + if (resolveOn && resolveOn.exec(cmdOutput)) { + resolve({ cmdOutput }); + } + }); + p.exit.then(({ code, signal }) => { + try { + subs.forEach(sub => sub?.dispose()); + if (code || signal) { + reject({ + message: `Command failed: ${cmd} ${(args || []).join(' ')}`, + cmdOutput: cmdOutput, + code, + signal, + }); + } else { + resolve({ cmdOutput }); + } + } catch (e) { + reject(e); + } + }, e => { + subs.forEach(sub => sub?.dispose()); + reject(e); + }); + }); +} + +export function plainExec(defaultCwd: string | undefined): ExecFunction { + return async function (params: ExecParameters): Promise { + const { cmd, args, output } = params; + + const text = `Run: ${cmd} ${(args || []).join(' ').replace(/\n.*/g, '')}`; + const start = output.start(text); + + const cwd = params.cwd || defaultCwd; + const env = params.env ? { ...process.env, ...params.env } : process.env; + const exec = await findLocalWindowsExecutable(cmd, cwd, env, output); + const p = cp.spawn(exec, args, { cwd, env, windowsHide: true }); + + return { + stdin: p.stdin, + stdout: p.stdout, + stderr: p.stderr, + exit: new Promise((resolve, reject) => { + p.once('error', err => { + output.stop(text, start); + reject(err); + }); + p.once('close', (code, signal) => { + output.stop(text, start); + resolve({ code, signal }); + }); + }), + async terminate() { + p.kill('SIGKILL'); + } + }; + }; +} + +export async function plainPtyExec(defaultCwd: string | undefined, loadNativeModule: (moduleName: string) => Promise): Promise { + const pty = await loadNativeModule('node-pty'); + if (!pty) { + throw new Error('Missing node-pty'); + } + + return async function(params: PtyExecParameters): Promise { + const { cmd, args, output } = params; + + const text = `Run: ${cmd} ${(args || []).join(' ').replace(/\n.*/g, '')}`; + const start = output.start(text); + + const useConpty = false; // TODO: Investigate using a shell with ConPTY. https://github.com/Microsoft/vscode-remote/issues/1234#issuecomment-485501275 + const cwd = params.cwd || defaultCwd; + const env = params.env ? { ...process.env, ...params.env } : process.env; + const exec = await findLocalWindowsExecutable(cmd, cwd, env, output); + const p = pty.spawn(exec, args || [], { + cwd, + env: env as any, + cols: output.dimensions?.columns, + rows: output.dimensions?.rows, + useConpty, + }); + const subs = [ + output.onDidChangeDimensions && output.onDidChangeDimensions(e => p.resize(e.columns, e.rows)) + ]; + + return { + onData: p.onData.bind(p), + write: p.write.bind(p), + resize: p.resize.bind(p), + exit: new Promise(resolve => { + p.onExit(({ exitCode, signal }) => { + subs.forEach(sub => sub?.dispose()); + output.stop(text, start); + resolve({ code: exitCode, signal }); + if (process.platform === 'win32') { + try { + // In some cases the process hasn't cleanly exited on Windows and the winpty-agent gets left around + // https://github.com/microsoft/node-pty/issues/333 + p.kill(); + } catch { + } + } + }); + }), + async terminate() { + p.kill('SIGKILL'); + } + }; + }; +} + +async function findLocalWindowsExecutable(command: string, cwd = process.cwd(), env: Record, output: Log): Promise { + if (process.platform !== 'win32') { + return command; + } + + // From terminalTaskSystem.ts. + + // If we have an absolute path then we take it. + if (path.isAbsolute(command)) { + return await findLocalWindowsExecutableWithExtension(command) || command; + } + if (/[/\\]/.test(command)) { + // We have a directory and the directory is relative (see above). Make the path absolute + // to the current working directory. + const fullPath = path.join(cwd, command); + return await findLocalWindowsExecutableWithExtension(fullPath) || fullPath; + } + let pathValue: string | undefined = undefined; + let paths: string[] | undefined = undefined; + // The options can override the PATH. So consider that PATH if present. + if (env) { + // Path can be named in many different ways and for the execution it doesn't matter + for (let key of Object.keys(env)) { + if (key.toLowerCase() === 'path') { + const value = env[key]; + if (typeof value === 'string') { + pathValue = value; + paths = value.split(path.delimiter) + .filter(Boolean); + paths.push(path.join(env.ProgramW6432 || 'C:\\Program Files', 'Docker\\Docker\\resources\\bin')); // Fall back when newly installed. + } + break; + } + } + } + // No PATH environment. Make path absolute to the cwd. + if (paths === void 0 || paths.length === 0) { + output.write(`findLocalWindowsExecutable: No PATH to look up exectuable '${command}'.`); + const fullPath = path.join(cwd, command); + return await findLocalWindowsExecutableWithExtension(fullPath) || fullPath; + } + // We have a simple file name. We get the path variable from the env + // and try to find the executable on the path. + for (let pathEntry of paths) { + // The path entry is absolute. + let fullPath: string; + if (path.isAbsolute(pathEntry)) { + fullPath = path.join(pathEntry, command); + } else { + fullPath = path.join(cwd, pathEntry, command); + } + const withExtension = await findLocalWindowsExecutableWithExtension(fullPath); + if (withExtension) { + return withExtension; + } + } + output.write(`findLocalWindowsExecutable: Exectuable '${command}' not found on PATH '${pathValue}'.`); + const fullPath = path.join(cwd, command); + return await findLocalWindowsExecutableWithExtension(fullPath) || fullPath; +} + +const pathext = process.env.PATHEXT; +const executableExtensions = pathext ? pathext.toLowerCase().split(';') : ['.com', '.exe', '.bat', '.cmd']; + +async function findLocalWindowsExecutableWithExtension(fullPath: string) { + if (executableExtensions.indexOf(path.extname(fullPath)) !== -1) { + return await isLocalFile(fullPath) ? fullPath : undefined; + } + for (const ext of executableExtensions) { + const withExtension = fullPath + ext; + if (await isLocalFile(withExtension)) { + return withExtension; + } + } + return undefined; +} + +export function parseVersion(str: string) { + const m = /^'?v?(\d+(\.\d+)*)/.exec(str); + if (!m) { + return undefined; + } + return m[1].split('.') + .map(i => parseInt(i, 10)); +} + +export function isEarlierVersion(left: number[], right: number[]) { + for (let i = 0, n = Math.max(left.length, right.length); i < n; i++) { + const l = left[i] || 0; + const r = right[i] || 0; + if (l !== r) { + return l < r; + } + } + return false; // Equal. +} + +export const fork = isTsnode ? (mod: string, args: readonly string[] | undefined, options: any) => { + return cp.spawn(tsnode, [mod, ...(args || [])], { ...options, windowsHide: true }); +} : cp.fork; + +export async function loadNativeModule(moduleName: string): Promise { + // Check NODE_PATH for Electron. Do this first to avoid loading a binary-incompatible version from the local node_modules during development. + if (process.env.NODE_PATH) { + for (const nodePath of process.env.NODE_PATH.split(path.delimiter)) { + if (nodePath) { + try { + return require(`${nodePath}/${moduleName}`); + } catch (err) { + // Not available. + } + } + } + } + try { + return require(moduleName); + } catch (err) { + // Not available. + } + return undefined; +} + +export type PlatformSwitch = T | { posix: T; win32: T }; + +export function platformDispatch(platform: NodeJS.Platform, platformSwitch: PlatformSwitch) { + if (typeof platformSwitch !== 'string' && 'win32' in platformSwitch) { + return platform === 'win32' ? platformSwitch.win32 : platformSwitch.posix; + } + return platformSwitch; +} + +export async function isFile(shellServer: ShellServer, location: string) { + return platformDispatch(shellServer.platform, { + posix: async () => { + try { + await shellServer.exec(`test -f '${location}'`); + return true; + } catch (err) { + return false; + } + }, + win32: async () => { + return (await shellServer.exec(`Test-Path '${location}' -PathType Leaf`)) + .stdout.trim() === 'True'; + } + })(); +} diff --git a/src/spec-common/errors.ts b/src/spec-common/errors.ts new file mode 100644 index 00000000..fe267f71 --- /dev/null +++ b/src/spec-common/errors.ts @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ContainerProperties, CommonDevContainerConfig, ResolverParameters } from './injectHeadless'; + +export { toErrorText, toWarningText } from '../spec-utils/log'; + +export interface ContainerErrorAction { + readonly id: string; + readonly title: string; + readonly isCloseAffordance?: boolean; + readonly isLastAction: boolean; + applicable: (err: ContainerError, primary: boolean) => boolean | Promise; + execute: (err: ContainerError) => Promise; +} + +interface ContainerErrorData { + reload?: boolean; + start?: boolean; + attach?: boolean; + fileWithError?: string; + learnMoreUrl?: string; +} + +interface ContainerErrorInfo { + description: string; + originalError?: any; + manageContainer?: boolean; + params?: ResolverParameters; + containerId?: string; + dockerParams?: any; // TODO + containerProperties?: ContainerProperties; + actions?: ContainerErrorAction[]; + data?: ContainerErrorData; +} + +export class ContainerError extends Error implements ContainerErrorInfo { + description!: string; + originalError?: any; + manageContainer = false; + params?: ResolverParameters; + containerId?: string; // TODO + dockerParams?: any; // TODO + volumeName?: string; + repositoryPath?: string; + folderPath?: string; + containerProperties?: ContainerProperties; + config?: CommonDevContainerConfig; + actions: ContainerErrorAction[] = []; + data: ContainerErrorData = {}; + + constructor(info: ContainerErrorInfo) { + super(info.originalError && info.originalError.message || info.description); + Object.assign(this, info); + if (this.originalError?.stack) { + this.stack = this.originalError.stack; + } + } +} diff --git a/src/spec-common/git.ts b/src/spec-common/git.ts new file mode 100644 index 00000000..dcab2036 --- /dev/null +++ b/src/spec-common/git.ts @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { runCommandNoPty, CLIHost } from './commonUtils'; +import { Log } from '../spec-utils/log'; +import { FileHost } from '../spec-utils/pfs'; + +export async function findGitRootFolder(cliHost: FileHost | CLIHost, folderPath: string, output: Log) { + if (!('exec' in cliHost)) { + for (let current = folderPath, previous = ''; current !== previous; previous = current, current = cliHost.path.dirname(current)) { + if (await cliHost.isFile(cliHost.path.join(current, '.git', 'config'))) { + return current; + } + } + return undefined; + } + try { + // Preserves symlinked paths (unlike --show-toplevel). + const { stdout } = await runCommandNoPty({ + exec: cliHost.exec, + cmd: 'git', + args: ['rev-parse', '--show-cdup'], + cwd: folderPath, + output, + }); + const cdup = stdout.toString().trim(); + return cliHost.path.resolve(folderPath, cdup); + } catch { + return undefined; + } +} + +export interface GitCloneOptions { + url: string; + tokenEnvVar?: string; + branch?: string; + recurseSubmodules?: boolean; + env?: NodeJS.ProcessEnv; + fullClone?: boolean; +} diff --git a/src/spec-common/injectHeadless.ts b/src/spec-common/injectHeadless.ts new file mode 100644 index 00000000..a7a616e8 --- /dev/null +++ b/src/spec-common/injectHeadless.ts @@ -0,0 +1,713 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as path from 'path'; +import * as fs from 'fs'; +import { StringDecoder } from 'string_decoder'; +import * as crypto from 'crypto'; +import { promisify } from 'util'; + +import { ContainerError, toErrorText, toWarningText } from './errors'; +import { launch, ShellServer } from './shellServer'; +import { ExecFunction, CLIHost, PtyExecFunction, isFile } from './commonUtils'; +import { Event, NodeEventEmitter } from '../spec-utils/event'; +import { PackageConfiguration } from '../spec-utils/product'; +import { URI } from 'vscode-uri'; +import { containerSubstitute } from './variableSubstitution'; +import { delay } from './async'; +import { Log, LogEvent, LogLevel, makeLog, nullLog } from '../spec-utils/log'; +import { buildProcessTrees, findProcesses, Process, processTreeToString } from './proc'; + +export enum ResolverProgress { + Begin, + CloningRepository, + BuildingImage, + StartingContainer, + InstallingServer, + StartingServer, + End, +} + +export interface ResolverParameters { + prebuild?: boolean; + computeExtensionHostEnv: boolean; + package: PackageConfiguration; + containerDataFolder: string | undefined; + containerSystemDataFolder: string | undefined; + appRoot: string | undefined; + extensionPath: string; + sessionId: string; + sessionStart: Date; + cliHost: CLIHost; + env: NodeJS.ProcessEnv; + cwd: string; + isLocalContainer: boolean; + progress: (current: ResolverProgress) => void; + output: Log; + allowSystemConfigChange: boolean; + defaultUserEnvProbe: UserEnvProbe; + postCreate: PostCreate; + getLogLevel: () => LogLevel; + onDidChangeLogLevel: Event; + loadNativeModule: (moduleName: string) => Promise; + shutdowns: (() => Promise)[]; + backgroundTasks: (Promise | (() => Promise))[]; + persistedFolder: string; // A path where config can be persisted and restored at a later time. Should default to tmpdir() folder if not provided. + remoteEnv: Record; +} + +export interface PostCreate { + enabled: boolean; + skipNonBlocking: boolean; + output: Log; + onDidInput: Event; + done: () => void; +} + +export function createNullPostCreate(enabled: boolean, skipNonBlocking: boolean, output: Log): PostCreate { + function listener(data: Buffer) { + emitter.fire(data.toString()); + } + const emitter = new NodeEventEmitter({ + on: () => process.stdin.on('data', listener), + off: () => process.stdin.off('data', listener), + }); + process.stdin.setEncoding('utf8'); + return { + enabled, + skipNonBlocking, + output: makeLog({ + ...output, + get dimensions() { + return output.dimensions; + }, + event: e => output.event({ + ...e, + channel: 'postCreate', + }), + }), + onDidInput: emitter.event, + done: () => { }, + }; +} + +export interface PortAttributes { + label: string | undefined; + onAutoForward: string | undefined; + elevateIfNeeded: boolean | undefined; +} + +export type UserEnvProbe = 'none' | 'loginInteractiveShell' | 'interactiveShell' | 'loginShell'; + +export type DevContainerConfigCommand = 'initializeCommand' | 'onCreateCommand' | 'updateContentCommand' | 'postCreateCommand' | 'postStartCommand' | 'postAttachCommand'; + +const defaultWaitFor: DevContainerConfigCommand = 'updateContentCommand'; + +export interface CommonDevContainerConfig { + configFilePath?: URI; + remoteEnv?: Record; + forwardPorts?: (number | string)[]; + portsAttributes?: Record; + otherPortsAttributes?: PortAttributes; + features?: Record>; + onCreateCommand?: string | string[]; + updateContentCommand?: string | string[]; + postCreateCommand?: string | string[]; + postStartCommand?: string | string[]; + postAttachCommand?: string | string[]; + waitFor?: DevContainerConfigCommand; + userEnvProbe?: UserEnvProbe; +} + +export interface OSRelease { + hardware: string; + id: string; + version: string; +} + +export interface ContainerProperties { + createdAt: string | undefined; + startedAt: string | undefined; + osRelease: OSRelease; + user: string; + gid: string | undefined; + env: NodeJS.ProcessEnv; + shell: string; + homeFolder: string; + userDataFolder: string; + remoteWorkspaceFolder?: string; + remoteExec: ExecFunction; + remotePtyExec: PtyExecFunction; + remoteExecAsRoot?: ExecFunction; + shellServer: ShellServer; + launchRootShellServer?: () => Promise; +} + +export async function getContainerProperties(options: { + params: ResolverParameters; + createdAt: string | undefined; + startedAt: string | undefined; + remoteWorkspaceFolder: string | undefined; + containerUser: string | undefined; + containerGroup: string | undefined; + containerEnv: NodeJS.ProcessEnv | undefined; + remoteExec: ExecFunction; + remotePtyExec: PtyExecFunction; + remoteExecAsRoot: ExecFunction | undefined; + rootShellServer: ShellServer | undefined; +}) { + let { params, createdAt, startedAt, remoteWorkspaceFolder, containerUser, containerGroup, containerEnv, remoteExec, remotePtyExec, remoteExecAsRoot, rootShellServer } = options; + let shellServer: ShellServer; + if (rootShellServer && containerUser === 'root') { + shellServer = rootShellServer; + } else { + shellServer = await launch(remoteExec, params.output, params.sessionId); + } + if (!containerEnv) { + const PATH = (await shellServer.exec('echo $PATH')).stdout.trim(); + containerEnv = PATH ? { PATH } : {}; + } + if (!containerUser) { + containerUser = await getUser(shellServer); + } + if (!remoteExecAsRoot && containerUser === 'root') { + remoteExecAsRoot = remoteExec; + } + const osRelease = await getOSRelease(shellServer); + const passwdUser = await getUserFromEtcPasswd(shellServer, containerUser); + if (!passwdUser) { + params.output.write(toWarningText(`User ${containerUser} not found in /etc/passwd.`)); + } + const shell = await getUserShell(containerEnv, passwdUser); + const homeFolder = await getHomeFolder(containerEnv, passwdUser); + const userDataFolder = getUserDataFolder(homeFolder, params); + let rootShellServerP: Promise | undefined; + if (rootShellServer) { + rootShellServerP = Promise.resolve(rootShellServer); + } else if (containerUser === 'root') { + rootShellServerP = Promise.resolve(shellServer); + } + const containerProperties: ContainerProperties = { + createdAt, + startedAt, + osRelease, + user: containerUser, + gid: containerGroup || passwdUser?.gid, + env: containerEnv, + shell, + homeFolder, + userDataFolder, + remoteWorkspaceFolder, + remoteExec, + remotePtyExec, + remoteExecAsRoot, + shellServer, + }; + if (rootShellServerP || remoteExecAsRoot) { + containerProperties.launchRootShellServer = () => rootShellServerP || (rootShellServerP = launch(remoteExecAsRoot!, params.output)); + } + return containerProperties; +} + +export async function getUser(shellServer: ShellServer) { + return (await shellServer.exec('id -un')).stdout.trim(); +} + +export async function getHomeFolder(containerEnv: NodeJS.ProcessEnv, passwdUser: PasswdUser | undefined) { + return containerEnv.HOME || (passwdUser && passwdUser.home) || '/root'; +} + +async function getUserShell(containerEnv: NodeJS.ProcessEnv, passwdUser: PasswdUser | undefined) { + return containerEnv.SHELL || (passwdUser && passwdUser.shell) || '/bin/sh'; +} + +export async function getUserFromEtcPasswd(shellServer: ShellServer, userNameOrId: string) { + const { stdout } = await shellServer.exec('cat /etc/passwd', { logOutput: false }); + return findUserInEtcPasswd(stdout, userNameOrId); +} + +export interface PasswdUser { + name: string; + uid: string; + gid: string; + home: string; + shell: string; +} + +export function findUserInEtcPasswd(etcPasswd: string, nameOrId: string): PasswdUser | undefined { + const users = etcPasswd + .split(/\r?\n/) + .map(line => line.split(':')) + .map(row => ({ + name: row[0], + uid: row[2], + gid: row[3], + home: row[5], + shell: row[6] + })); + return users.find(user => user.name === nameOrId || user.uid === nameOrId); +} + +export function getUserDataFolder(homeFolder: string, params: ResolverParameters) { + return path.posix.resolve(homeFolder, params.containerDataFolder || '.devcontainer'); +} + +export function getSystemVarFolder(params: ResolverParameters): string { + return params.containerSystemDataFolder || '/var/devcontainer'; +} + +export async function setupInContainer(params: ResolverParameters, containerProperties: ContainerProperties, config: CommonDevContainerConfig) { + await patchEtcEnvironment(params, containerProperties); + await patchEtcProfile(params, containerProperties); + const computeRemoteEnv = params.computeExtensionHostEnv || params.postCreate.enabled; + const remoteEnv = computeRemoteEnv ? probeRemoteEnv(params, containerProperties, config) : Promise.resolve({}); + if (params.postCreate.enabled) { + await runPostCreateCommands(params, containerProperties, config, remoteEnv, false); + } + return { + remoteEnv: params.computeExtensionHostEnv ? await remoteEnv : {}, + }; +} + +export function probeRemoteEnv(params: ResolverParameters, containerProperties: ContainerProperties, config: CommonDevContainerConfig) { + return probeUserEnv(params, containerProperties, config) + .then>(shellEnv => ({ + ...shellEnv, + ...params.remoteEnv, + ...config.remoteEnv ? containerSubstitute(params.cliHost.platform, config.configFilePath, containerProperties.env, config.remoteEnv) : {}, + } as Record)); +} + +export async function runPostCreateCommands(params: ResolverParameters, containerProperties: ContainerProperties, config: CommonDevContainerConfig, remoteEnv: Promise>, stopForPersonalization: boolean): Promise<'skipNonBlocking' | 'prebuild' | 'stopForPersonalization' | 'done'> { + const skipNonBlocking = params.postCreate.skipNonBlocking; + const waitFor = config.waitFor || defaultWaitFor; + if (skipNonBlocking && waitFor === 'initializeCommand') { + return 'skipNonBlocking'; + } + + await runPostCreateCommand(params, containerProperties, config, 'onCreateCommand', remoteEnv, false); + if (skipNonBlocking && waitFor === 'onCreateCommand') { + return 'skipNonBlocking'; + } + + await runPostCreateCommand(params, containerProperties, config, 'updateContentCommand', remoteEnv, !!params.prebuild); + if (skipNonBlocking && waitFor === 'updateContentCommand') { + return 'skipNonBlocking'; + } + + if (params.prebuild) { + return 'prebuild'; + } + + await runPostCreateCommand(params, containerProperties, config, 'postCreateCommand', remoteEnv, false); + if (skipNonBlocking && waitFor === 'postCreateCommand') { + return 'skipNonBlocking'; + } + + if (stopForPersonalization) { + return 'stopForPersonalization'; + } + + await runPostStartCommand(params, containerProperties, config, remoteEnv); + if (skipNonBlocking && waitFor === 'postStartCommand') { + return 'skipNonBlocking'; + } + + await runPostAttachCommand(params, containerProperties, config, remoteEnv); + return 'done'; +} + +export async function getOSRelease(shellServer: ShellServer) { + let hardware = 'unknown'; + let id = 'unknown'; + let version = 'unknown'; + try { + hardware = (await shellServer.exec('uname -m')).stdout.trim(); + const { stdout } = await shellServer.exec('(cat /etc/os-release || cat /usr/lib/os-release) 2>/dev/null'); + id = (stdout.match(/^ID=([^\u001b\r\n]*)/m) || [])[1] || 'notfound'; + version = (stdout.match(/^VERSION_ID=([^\u001b\r\n]*)/m) || [])[1] || 'notfound'; + } catch (err) { + console.error(err); + // Optimistically continue. + } + return { hardware, id, version }; +} + +async function runPostCreateCommand(params: ResolverParameters, containerProperties: ContainerProperties, config: CommonDevContainerConfig, postCommandName: 'onCreateCommand' | 'updateContentCommand' | 'postCreateCommand', remoteEnv: Promise>, rerun: boolean) { + const markerFile = path.posix.join(containerProperties.userDataFolder, `.${postCommandName}Marker`); + const doRun = !!containerProperties.createdAt && await updateMarkerFile(containerProperties.shellServer, markerFile, containerProperties.createdAt) || rerun; + await runPostCommand(params, containerProperties, config, postCommandName, remoteEnv, doRun); +} + +async function runPostStartCommand(params: ResolverParameters, containerProperties: ContainerProperties, config: CommonDevContainerConfig, remoteEnv: Promise>) { + const markerFile = path.posix.join(containerProperties.userDataFolder, '.postStartCommandMarker'); + const doRun = !!containerProperties.startedAt && await updateMarkerFile(containerProperties.shellServer, markerFile, containerProperties.startedAt); + await runPostCommand(params, containerProperties, config, 'postStartCommand', remoteEnv, doRun); +} + +async function updateMarkerFile(shellServer: ShellServer, location: string, content: string) { + try { + await shellServer.exec(`mkdir -p '${path.posix.dirname(location)}' && CONTENT="$(cat '${location}' 2>/dev/null || echo ENOENT)" && [ "\${CONTENT:-${content}}" != '${content}' ] && echo '${content}' > '${location}'`); + return true; + } catch (err) { + return false; + } +} + +async function runPostAttachCommand(params: ResolverParameters, containerProperties: ContainerProperties, config: CommonDevContainerConfig, remoteEnv: Promise>) { + await runPostCommand(params, containerProperties, config, 'postAttachCommand', remoteEnv, true); +} + +async function runPostCommand({ postCreate }: ResolverParameters, containerProperties: ContainerProperties, config: CommonDevContainerConfig, postCommandName: 'onCreateCommand' | 'updateContentCommand' | 'postCreateCommand' | 'postStartCommand' | 'postAttachCommand', remoteEnv: Promise>, doRun: boolean) { + const postCommand = config[postCommandName]; + if (doRun && postCommand && (typeof postCommand === 'string' ? postCommand.trim() : postCommand.length)) { + const progressName = `Running ${postCommandName}...`; + const progressDetail = typeof postCommand === 'string' ? postCommand : postCommand.join(' '); + const infoOutput = makeLog({ + event(e: LogEvent) { + postCreate.output.event(e); + if (e.type === 'raw' && e.text.includes('::endstep::')) { + postCreate.output.event({ + type: 'progress', + name: progressName, + status: 'running', + stepDetail: '' + }); + } + if (e.type === 'raw' && e.text.includes('::step::')) { + postCreate.output.event({ + type: 'progress', + name: progressName, + status: 'running', + stepDetail: `${e.text.split('::step::')[1].split('\r\n')[0]}` + }); + } + }, + get dimensions() { + return postCreate.output.dimensions; + }, + onDidChangeDimensions: postCreate.output.onDidChangeDimensions, + }, LogLevel.Info); + try { + infoOutput.event({ + type: 'progress', + name: progressName, + status: 'running', + stepDetail: progressDetail + }); + const remoteCwd = containerProperties.remoteWorkspaceFolder || containerProperties.homeFolder; + infoOutput.raw(`\x1b[1mRunning the ${postCommandName} from devcontainer.json...\x1b[0m\r\n\r\n`); + await runRemoteCommand({ ...postCreate, output: infoOutput }, containerProperties, typeof postCommand === 'string' ? ['/bin/sh', '-c', postCommand] : postCommand, remoteCwd, { remoteEnv: await remoteEnv, print: 'continuous' }); + infoOutput.raw('\r\n'); + infoOutput.event({ + type: 'progress', + name: progressName, + status: 'succeeded', + }); + } catch (err) { + infoOutput.event({ + type: 'progress', + name: progressName, + status: 'failed', + }); + if (err && (err.code === 130 || err.signal === 2)) { // SIGINT seen on darwin as code === 130, would also make sense as signal === 2. + infoOutput.raw(`\r\n\x1b[1m${postCommandName} interrupted.\x1b[0m\r\n\r\n`); + } else { + if (err?.code) { + infoOutput.write(toErrorText(`${postCommandName} failed with exit code ${err.code}. Skipping any further user-provided commands.`)); + } + throw new ContainerError({ + description: `The ${postCommandName} in the devcontainer.json failed.`, + originalError: err, + }); + } + } + } +} + +async function createFile(shellServer: ShellServer, location: string) { + try { + await shellServer.exec(createFileCommand(location)); + return true; + } catch (err) { + return false; + } +} + +function createFileCommand(location: string) { + return `test ! -f '${location}' && set -o noclobber && mkdir -p '${path.posix.dirname(location)}' && { > '${location}' ; } 2> /dev/null`; +} + +export async function runRemoteCommand(params: { output: Log; onDidInput?: Event }, { remotePtyExec }: { remotePtyExec: PtyExecFunction }, cmd: string[], cwd?: string, options: { remoteEnv?: NodeJS.ProcessEnv; stdin?: Buffer | fs.ReadStream; silent?: boolean; print?: 'off' | 'continuous' | 'end'; resolveOn?: RegExp } = {}) { + const print = options.print || (options.silent ? 'off' : 'end'); + const p = await remotePtyExec({ + env: options.remoteEnv, + cwd, + cmd: cmd[0], + args: cmd.slice(1), + output: options.silent ? nullLog : params.output, + }); + let cmdOutput = ''; + let doResolveEarly: () => void; + const resolveEarly = new Promise(resolve => { + doResolveEarly = resolve; + }); + p.onData(chunk => { + cmdOutput += chunk; + if (print === 'continuous') { + params.output.raw(chunk); + } + if (options.resolveOn && options.resolveOn.exec(cmdOutput)) { + doResolveEarly(); + } + }); + const sub = params.onDidInput && params.onDidInput(data => p.write(data)); + const exit = await Promise.race([p.exit, resolveEarly]); + if (sub) { + sub.dispose(); + } + if (print === 'end') { + params.output.raw(cmdOutput); + } + if (exit && (exit.code || exit.signal)) { + return Promise.reject({ + message: `Command failed: ${cmd.join(' ')}`, + cmdOutput, + code: exit.code, + signal: exit.signal, + }); + } + return { + cmdOutput, + }; +} + +async function runRemoteCommandNoPty(params: { output: Log }, { remoteExec }: { remoteExec: ExecFunction }, cmd: string[], cwd?: string, options: { remoteEnv?: NodeJS.ProcessEnv; stdin?: Buffer | fs.ReadStream; silent?: boolean; print?: 'off' | 'continuous' | 'end'; resolveOn?: RegExp } = {}) { + const print = options.print || (options.silent ? 'off' : 'end'); + const p = await remoteExec({ + env: options.remoteEnv, + cwd, + cmd: cmd[0], + args: cmd.slice(1), + output: options.silent ? nullLog : params.output, + }); + const stdout: Buffer[] = []; + const stderr: Buffer[] = []; + const stdoutDecoder = new StringDecoder(); + const stderrDecoder = new StringDecoder(); + let stdoutStr = ''; + let stderrStr = ''; + let doResolveEarly: () => void; + let doRejectEarly: (err: any) => void; + const resolveEarly = new Promise((resolve, reject) => { + doResolveEarly = resolve; + doRejectEarly = reject; + }); + p.stdout.on('data', (chunk: Buffer) => { + stdout.push(chunk); + const str = stdoutDecoder.write(chunk); + if (print === 'continuous') { + params.output.write(str.replace(/\r?\n/g, '\r\n')); + } + stdoutStr += str; + if (options.resolveOn && options.resolveOn.exec(stdoutStr)) { + doResolveEarly(); + } + }); + p.stderr.on('data', (chunk: Buffer) => { + stderr.push(chunk); + stderrStr += stderrDecoder.write(chunk); + }); + if (options.stdin instanceof Buffer) { + p.stdin.write(options.stdin, err => { + if (err) { + doRejectEarly(err); + } + }); + p.stdin.end(); + } else if (options.stdin instanceof fs.ReadStream) { + options.stdin.pipe(p.stdin); + } + const exit = await Promise.race([p.exit, resolveEarly]); + const stdoutBuf = Buffer.concat(stdout); + const stderrBuf = Buffer.concat(stderr); + if (print === 'end') { + params.output.write(stdoutStr.replace(/\r?\n/g, '\r\n')); + params.output.write(toErrorText(stderrStr)); + } + const cmdOutput = `${stdoutStr}\n${stderrStr}`; + if (exit && (exit.code || exit.signal)) { + return Promise.reject({ + message: `Command failed: ${cmd.join(' ')}`, + cmdOutput, + stdout: stdoutBuf, + stderr: stderrBuf, + code: exit.code, + signal: exit.signal, + }); + } + return { + cmdOutput, + stdout: stdoutBuf, + stderr: stderrBuf, + }; +} + +async function patchEtcEnvironment(params: ResolverParameters, containerProperties: ContainerProperties) { + const markerFile = path.posix.join(getSystemVarFolder(params), `.patchEtcEnvironmentMarker`); + if (params.allowSystemConfigChange && containerProperties.launchRootShellServer && !(await isFile(containerProperties.shellServer, markerFile))) { + const rootShellServer = await containerProperties.launchRootShellServer(); + if (await createFile(rootShellServer, markerFile)) { + await rootShellServer.exec(`cat >> /etc/environment <<'etcEnvrionmentEOF' +${Object.keys(containerProperties.env).map(k => `\n${k}="${containerProperties.env[k]}"`).join('')} +etcEnvrionmentEOF +`); + } + } +} + +async function patchEtcProfile(params: ResolverParameters, containerProperties: ContainerProperties) { + const markerFile = path.posix.join(getSystemVarFolder(params), `.patchEtcProfileMarker`); + if (params.allowSystemConfigChange && containerProperties.launchRootShellServer && !(await isFile(containerProperties.shellServer, markerFile))) { + const rootShellServer = await containerProperties.launchRootShellServer(); + if (await createFile(rootShellServer, markerFile)) { + await rootShellServer.exec(`sed -i -E 's/((^|\\s)PATH=)([^\\$]*)$/\\1\${PATH:-\\3}/g' /etc/profile || true`); + } + } +} + +async function probeUserEnv(params: { defaultUserEnvProbe: UserEnvProbe; allowSystemConfigChange: boolean; output: Log }, containerProperties: { shell: string; remoteExec: ExecFunction; installFolder?: string; env?: NodeJS.ProcessEnv; shellServer?: ShellServer; launchRootShellServer?: (() => Promise); user?: string }, config?: { userEnvProbe?: UserEnvProbe }) { + const env = await runUserEnvProbe(params, containerProperties, config, 'cat /proc/self/environ', '\0'); + if (env) { + return env; + } + params.output.write('userEnvProbe: falling back to printenv'); + const env2 = await runUserEnvProbe(params, containerProperties, config, 'printenv', '\n'); + return env2 || {}; +} + +async function runUserEnvProbe(params: { defaultUserEnvProbe: UserEnvProbe; allowSystemConfigChange: boolean; output: Log }, containerProperties: { shell: string; remoteExec: ExecFunction; installFolder?: string; env?: NodeJS.ProcessEnv; shellServer?: ShellServer; launchRootShellServer?: (() => Promise); user?: string }, config: { userEnvProbe?: UserEnvProbe } | undefined, cmd: string, sep: string) { + let { userEnvProbe } = config || {}; + params.output.write(`userEnvProbe: ${userEnvProbe || params.defaultUserEnvProbe}${userEnvProbe ? '' : ' (default)'}`); + if (!userEnvProbe) { + userEnvProbe = params.defaultUserEnvProbe; + } + if (userEnvProbe === 'none') { + return {}; + } + try { + // From VS Code's shellEnv.ts + + const buffer = await promisify(crypto.randomBytes)(16); + const mark = buffer.toString('hex'); + const regex = new RegExp(mark + '([^]*)' + mark); + const systemShellUnix = containerProperties.shell; + params.output.write(`userEnvProbe shell: ${systemShellUnix}`); + + // handle popular non-POSIX shells + const name = path.posix.basename(systemShellUnix); + const command = `echo -n ${mark}; ${cmd}; echo -n ${mark}`; + let shellArgs: string[]; + if (/^pwsh(-preview)?$/.test(name)) { + shellArgs = userEnvProbe === 'loginInteractiveShell' || userEnvProbe === 'loginShell' ? + ['-Login', '-Command'] : // -Login must be the first option. + ['-Command']; + } else { + shellArgs = [ + userEnvProbe === 'loginInteractiveShell' ? '-lic' : + userEnvProbe === 'loginShell' ? '-lc' : + userEnvProbe === 'interactiveShell' ? '-ic' : + '-c' + ]; + } + + const traceOutput = makeLog(params.output, LogLevel.Trace); + const resultP = runRemoteCommandNoPty({ output: traceOutput }, { remoteExec: containerProperties.remoteExec }, [systemShellUnix, ...shellArgs, command], containerProperties.installFolder); + Promise.race([resultP, delay(2000)]) + .then(async result => { + if (!result) { + let processes: Process[]; + const shellServer = containerProperties.shellServer || await launch(containerProperties.remoteExec, params.output); + try { + ({ processes } = await findProcesses(shellServer)); + } finally { + if (!containerProperties.shellServer) { + await shellServer.process.terminate(); + } + } + const shell = processes.find(p => p.cmd.startsWith(systemShellUnix) && p.cmd.indexOf(mark) !== -1); + if (shell) { + const index = buildProcessTrees(processes); + const tree = index[shell.pid]; + params.output.write(`userEnvProbe is taking longer than 2 seconds. Process tree: +${processTreeToString(tree)}`); + } else { + params.output.write(`userEnvProbe is taking longer than 2 seconds. Process not found.`); + } + } + }, () => undefined) + .catch(err => params.output.write(toErrorText(err && (err.stack || err.message) || 'Error reading process tree.'))); + const result = await Promise.race([resultP, delay(10000)]); + if (!result) { + params.output.write(toErrorText(`userEnvProbe is taking longer than 10 seconds. Avoid waiting for user input in your shell's startup scripts. Continuing.`)); + return {}; + } + const raw = result.stdout.toString(); + const match = regex.exec(raw); + const rawStripped = match ? match[1] : ''; + if (!rawStripped) { + return undefined; // assume error + } + const env = rawStripped.split(sep) + .reduce((env, e) => { + const i = e.indexOf('='); + if (i !== -1) { + env[e.substring(0, i)] = e.substring(i + 1); + } + return env; + }, {} as Record); + params.output.write(`userEnvProbe parsed: ${JSON.stringify(env, undefined, ' ')}`, LogLevel.Trace); + delete env.PWD; + + const shellPath = env.PATH; + const containerPath = containerProperties.env?.PATH; + const doMergePaths = !(params.allowSystemConfigChange && containerProperties.launchRootShellServer) && shellPath && containerPath; + if (doMergePaths) { + const user = containerProperties.user; + env.PATH = mergePaths(shellPath, containerPath!, user === 'root' || user === '0'); + } + params.output.write(`userEnvProbe PATHs: +Probe: ${typeof shellPath === 'string' ? `'${shellPath}'` : 'None'} +Container: ${typeof containerPath === 'string' ? `'${containerPath}'` : 'None'}${doMergePaths ? ` +Merged: ${typeof env.PATH === 'string' ? `'${env.PATH}'` : 'None'}` : ''}`); + + return env; + } catch (err) { + params.output.write(toErrorText(err && (err.stack || err.message) || 'Error reading shell environment.')); + return {}; + } +} + +function mergePaths(shellPath: string, containerPath: string, rootUser: boolean) { + const result = shellPath.split(':'); + let insertAt = 0; + for (const entry of containerPath.split(':')) { + const i = result.indexOf(entry); + if (i === -1) { + if (rootUser || !/\/sbin(\/|$)/.test(entry)) { + result.splice(insertAt++, 0, entry); + } + } else { + insertAt = i + 1; + } + } + return result.join(':'); +} + +export async function finishBackgroundTasks(tasks: (Promise | (() => Promise))[]) { + for (const task of tasks) { + await (typeof task === 'function' ? task() : task); + } +} \ No newline at end of file diff --git a/src/spec-common/proc.ts b/src/spec-common/proc.ts new file mode 100644 index 00000000..c18acbbb --- /dev/null +++ b/src/spec-common/proc.ts @@ -0,0 +1,69 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ShellServer } from './shellServer'; + +export interface Process { + pid: string; + ppid: string | undefined; + pgrp: string | undefined; + cwd: string; + mntNS: string; + cmd: string; + env: Record; +} + +export async function findProcesses(shellServer: ShellServer) { + const ps = 'for pid in `cd /proc && ls -d [0-9]*`; do { echo $pid ; readlink /proc/$pid/cwd ; readlink /proc/$pid/ns/mnt ; cat /proc/$pid/stat | tr "\n" " " ; echo ; xargs -0 < /proc/$pid/environ ; xargs -0 < /proc/$pid/cmdline ; } ; echo --- ; done ; readlink /proc/self/ns/mnt 2>/dev/null'; + const { stdout } = await shellServer.exec(ps, { logOutput: false }); + + const n = 6; + const sections = stdout.split('\n---\n'); + const mntNS = sections.pop()!.trim(); + const processes: Process[] = sections + .map(line => line.split('\n')) + .filter(parts => parts.length >= n) + .map(([ pid, cwd, mntNS, stat, env, cmd ]) => { + const statM: (string | undefined)[] = /.*\) [^ ]* ([^ ]*) ([^ ]*)/.exec(stat) || []; + return { + pid, + ppid: statM[1], + pgrp: statM[2], + cwd, + mntNS, + cmd, + env: env.split(' ') + .reduce((env, current) => { + const i = current.indexOf('='); + if (i !== -1) { + env[current.substr(0, i)] = current.substr(i + 1); + } + return env; + }, {} as Record), + }; + }); + return { + processes, + mntNS, + }; +} + +export interface ProcessTree { + process: Process; + childProcesses: ProcessTree[]; +} + +export function buildProcessTrees(processes: Process[]) { + const index: Record = {}; + processes.forEach(process => index[process.pid] = { process, childProcesses: [] }); + processes.filter(p => p.ppid) + .forEach(p => index[p.ppid!]?.childProcesses.push(index[p.pid])); + return index; +} + +export function processTreeToString(tree: ProcessTree, singleIndent = ' ', currentIndent = ' '): string { + return `${currentIndent}${tree.process.pid}: ${tree.process.cmd} +${tree.childProcesses.map(p => processTreeToString(p, singleIndent, currentIndent + singleIndent))}`; +} diff --git a/src/spec-common/shellServer.ts b/src/spec-common/shellServer.ts new file mode 100644 index 00000000..009567c9 --- /dev/null +++ b/src/spec-common/shellServer.ts @@ -0,0 +1,199 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as path from 'path'; + +import { StringDecoder } from 'string_decoder'; +import { ExecFunction, Exec, PlatformSwitch, platformDispatch } from './commonUtils'; +import { Log, LogLevel } from '../spec-utils/log'; + +export interface ShellServer { + exec(cmd: PlatformSwitch, options?: { logLevel?: LogLevel; logOutput?: boolean | 'continuous' | 'silent'; stdin?: Buffer }): Promise<{ stdout: string; stderr: string }>; + process: Exec; + platform: NodeJS.Platform; + path: typeof path.posix | typeof path.win32; +} + +export const EOT = '\u2404'; + +export async function launch(remoteExec: ExecFunction | Exec, output: Log, agentSessionId?: string, platform: NodeJS.Platform = 'linux', hostName: 'Host' | 'Container' = 'Container'): Promise { + const isExecFunction = typeof remoteExec === 'function'; + const isWindows = platform === 'win32'; + const p = isExecFunction ? await remoteExec({ + env: agentSessionId ? { VSCODE_REMOTE_CONTAINERS_SESSION: agentSessionId } : {}, + cmd: isWindows ? 'powershell' : '/bin/sh', + args: isWindows ? ['-NoProfile', '-Command', '-'] : [], + output, + }) : remoteExec; + if (!isExecFunction) { + // TODO: Pass in agentSessionId. + const stdinText = isWindows + ? `powershell -NoProfile -Command "powershell -NoProfile -Command -"\n` // Nested PowerShell (for some reason) avoids the echo of stdin on stdout. + : `/bin/sh -c 'echo ${EOT}; /bin/sh'\n`; + p.stdin.write(stdinText); + const eot = new Promise(resolve => { + let stdout = ''; + const stdoutDecoder = new StringDecoder(); + p.stdout.on('data', function eotListener(chunk: Buffer) { + stdout += stdoutDecoder.write(chunk); + if (stdout.includes(stdinText)) { + p.stdout.off('data', eotListener); + resolve(); + } + }); + }); + await eot; + } + + const monitor = monitorProcess(p); + + let lastExec: Promise | undefined; + async function exec(cmd: PlatformSwitch, options?: { logLevel?: LogLevel; logOutput?: boolean | 'continuous' | 'silent'; stdin?: Buffer }) { + const currentExec = lastExec = (async () => { + try { + await lastExec; + } catch (err) { + // ignore + } + return _exec(platformDispatch(platform, cmd), options); + })(); + try { + return await Promise.race([currentExec, monitor.unexpectedExit]); + } finally { + monitor.disposeStdioListeners(); + if (lastExec === currentExec) { + lastExec = undefined; + } + } + } + + async function _exec(cmd: string, options?: { logLevel?: LogLevel; logOutput?: boolean | 'continuous' | 'silent'; stdin?: Buffer }) { + const text = `Run in ${hostName.toLowerCase()}: ${cmd.replace(/\n.*/g, '')}`; + let start: number; + if (options?.logOutput !== 'silent') { + start = output.start(text, options?.logLevel); + } + if (p.stdin.destroyed) { + output.write('Stdin closed!'); + const { code, signal } = await p.exit; + return Promise.reject({ message: `Shell server terminated (code: ${code}, signal: ${signal})`, code, signal }); + } + if (platform === 'win32') { + p.stdin.write(`[Console]::Write('${EOT}'); ( ${cmd} ); [Console]::Write("${EOT}$LastExitCode ${EOT}"); [Console]::Error.Write('${EOT}')\n`); + } else { + p.stdin.write(`echo -n ${EOT}; ( ${cmd} ); echo -n ${EOT}$?${EOT}; echo -n ${EOT} >&2\n`); + } + const [stdoutP0, stdoutP] = read(p.stdout, [1, 2], options?.logOutput === 'continuous' ? (str, i, j) => { + if (i === 1 && j === 0) { + output.write(str, options?.logLevel); + } + } : () => undefined); + const stderrP = read(p.stderr, [1], options?.logOutput === 'continuous' ? (str, i, j) => { + if (i === 0 && j === 0) { + output.write(str, options?.logLevel); // TODO + } + } : () => undefined)[0]; + if (options?.stdin) { + await stdoutP0; // Wait so `cmd` has its stdin set up. + p.stdin.write(options?.stdin); + } + const [stdout, codeStr] = await stdoutP; + const [stderr] = await stderrP; + const code = parseInt(codeStr, 10) || 0; + if (options?.logOutput === undefined || options?.logOutput === true) { + output.write(stdout, options?.logLevel); + output.write(stderr, options?.logLevel); // TODO + if (code) { + output.write(`Exit code ${code}`, options?.logLevel); + } + } + if (options?.logOutput === 'continuous' && code) { + output.write(`Exit code ${code}`, options?.logLevel); + } + if (options?.logOutput !== 'silent') { + output.stop(text, start!, options?.logLevel); + } + if (code) { + return Promise.reject({ message: `Command in ${hostName.toLowerCase()} failed: ${cmd}`, code, stdout, stderr }); + } + return { stdout, stderr }; + } + + return { exec, process: p, platform, path: platformDispatch(platform, path) }; +} + +function read(stream: NodeJS.ReadableStream, numberOfResults: number[], log: (str: string, i: number, j: number) => void) { + const promises = numberOfResults.map(() => { + let cbs: { resolve: (value: string[]) => void; reject: () => void }; + const promise = new Promise((resolve, reject) => cbs = { resolve, reject }); + return { promise, ...cbs! }; + }); + const decoder = new StringDecoder('utf8'); + const strings: string[] = []; + + let j = 0; + let results: string[] = []; + function data(chunk: Buffer) { + const str = decoder.write(chunk); + consume(str); + } + function consume(str: string) { + // console.log(`consume ${numberOfResults}: '${str}'`); + const i = str.indexOf(EOT); + if (i !== -1) { + const s = str.substr(0, i); + strings.push(s); + log(s, j, results.length); + // console.log(`result ${numberOfResults}: '${strings.join('')}'`); + results.push(strings.join('')); + strings.length = 0; + if (results.length === numberOfResults[j]) { + promises[j].resolve(results); + j++; + results = []; + if (j === numberOfResults.length) { + stream.off('data', data); + } + } + if (i + 1 < str.length) { + consume(str.substr(i + 1)); + } + } else { + strings.push(str); + log(str, j, results.length); + } + } + stream.on('data', data); + + return promises.map(p => p.promise); +} + +function monitorProcess(p: Exec) { + let processExited: (err: any) => void; + const unexpectedExit = new Promise((_resolve, reject) => processExited = reject); + const stdout: Buffer[] = []; + const stderr: Buffer[] = []; + const stdoutListener = (chunk: Buffer) => stdout.push(chunk); + const stderrListener = (chunk: Buffer) => stderr.push(chunk); + p.stdout.on('data', stdoutListener); + p.stderr.on('data', stderrListener); + p.exit.then(({ code, signal }) => { + processExited(`Shell server terminated (code: ${code}, signal: ${signal}) +${Buffer.concat(stdout).toString()} +${Buffer.concat(stderr).toString()}`); + }, err => { + processExited(`Shell server failed: ${err && (err.stack || err.message)}`); + }); + const disposeStdioListeners = () => { + p.stdout.off('data', stdoutListener); + p.stderr.off('data', stderrListener); + stdout.length = 0; + stderr.length = 0; + }; + return { + unexpectedExit, + disposeStdioListeners, + }; +} diff --git a/src/spec-common/tsconfig.json b/src/spec-common/tsconfig.json new file mode 100644 index 00000000..eff31937 --- /dev/null +++ b/src/spec-common/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "references": [ + { + "path": "../spec-utils" + } + ] +} \ No newline at end of file diff --git a/src/spec-common/variableSubstitution.ts b/src/spec-common/variableSubstitution.ts new file mode 100644 index 00000000..c1dd969f --- /dev/null +++ b/src/spec-common/variableSubstitution.ts @@ -0,0 +1,136 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as path from 'path'; + +import { ContainerError } from './errors'; +import { URI } from 'vscode-uri'; + +export interface SubstitutionContext { + platform: NodeJS.Platform; + configFile: URI; + localWorkspaceFolder: string | undefined; + containerWorkspaceFolder: string | undefined; + env: NodeJS.ProcessEnv; +} + +export function substitute(context: SubstitutionContext, value: T): T { + let env: NodeJS.ProcessEnv | undefined; + const isWindows = context.platform === 'win32'; + const updatedContext = { + ...context, + get env() { + return env || (env = normalizeEnv(isWindows, context.env)); + } + }; + const replace = replaceWithContext.bind(undefined, isWindows, updatedContext); + if (context.containerWorkspaceFolder) { + updatedContext.containerWorkspaceFolder = resolveString(replace, context.containerWorkspaceFolder); + } + return substitute0(replace, value); +} + +export function containerSubstitute(platform: NodeJS.Platform, configFile: URI | undefined, containerEnv: NodeJS.ProcessEnv, value: T): T { + const isWindows = platform === 'win32'; + return substitute0(replaceContainerEnv.bind(undefined, isWindows, configFile, normalizeEnv(isWindows, containerEnv)), value); +} + +type Replace = (match: string, variable: string, argument: string | undefined) => string; + +function substitute0(replace: Replace, value: any): any { + if (typeof value === 'string') { + return resolveString(replace, value); + } else if (Array.isArray(value)) { + return value.map(s => substitute0(replace, s)); + } else if (value && typeof value === 'object') { + const result: any = Object.create(null); + Object.keys(value).forEach(key => { + result[key] = substitute0(replace, value[key]); + }); + return result; + } + return value; +} + +const VARIABLE_REGEXP = /\$\{(.*?)\}/g; + +function normalizeEnv(isWindows: boolean, originalEnv: NodeJS.ProcessEnv): NodeJS.ProcessEnv { + if (isWindows) { + const env = Object.create(null); + Object.keys(originalEnv).forEach(key => { + env[key.toLowerCase()] = originalEnv[key]; + }); + return env; + } + return originalEnv; +} + +function resolveString(replace: Replace, value: string): string { + // loop through all variables occurrences in 'value' + return value.replace(VARIABLE_REGEXP, evaluateSingleVariable.bind(undefined, replace)); +} + +function evaluateSingleVariable(replace: Replace, match: string, variable: string): string { + + // try to separate variable arguments from variable name + let argument: string | undefined; + const parts = variable.split(':'); + if (parts.length > 1) { + variable = parts[0]; + argument = parts[1]; + } + + return replace(match, variable, argument); +} + +function replaceWithContext(isWindows: boolean, context: SubstitutionContext, match: string, variable: string, argument: string | undefined) { + switch (variable) { + case 'env': + case 'localEnv': + return lookupValue(isWindows, context.env, argument, match, context.configFile); + + case 'localWorkspaceFolder': + return context.localWorkspaceFolder !== undefined ? context.localWorkspaceFolder : match; + + case 'localWorkspaceFolderBasename': + return context.localWorkspaceFolder !== undefined ? (isWindows ? path.win32 : path.posix).basename(context.localWorkspaceFolder) : match; + + case 'containerWorkspaceFolder': + return context.containerWorkspaceFolder !== undefined ? context.containerWorkspaceFolder : match; + + case 'containerWorkspaceFolderBasename': + return context.containerWorkspaceFolder !== undefined ? path.posix.basename(context.containerWorkspaceFolder) : match; + + default: + return match; + } +} + +function replaceContainerEnv(isWindows: boolean, configFile: URI | undefined, containerEnvObj: NodeJS.ProcessEnv, match: string, variable: string, argument: string | undefined) { + switch (variable) { + case 'containerEnv': + return lookupValue(isWindows, containerEnvObj, argument, match, configFile); + + default: + return match; + } +} + +function lookupValue(isWindows: boolean, envObj: NodeJS.ProcessEnv, argument: string | undefined, match: string, configFile: URI | undefined) { + if (argument) { + if (isWindows) { + argument = argument.toLowerCase(); + } + const env = envObj[argument]; + if (typeof env === 'string') { + return env; + } + // For `env` we should do the same as a normal shell does - evaluates missing envs to an empty string #46436 + return ''; + } + throw new ContainerError({ + description: `'${match}'${configFile ? ` in ${path.posix.basename(configFile.path)}` : ''} can not be resolved because no environment variable name is given.` + }); +} diff --git a/src/spec-configuration/configuration.ts b/src/spec-configuration/configuration.ts new file mode 100644 index 00000000..f10d6b18 --- /dev/null +++ b/src/spec-configuration/configuration.ts @@ -0,0 +1,232 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as path from 'path'; +import { URI } from 'vscode-uri'; +import { FileHost, parentURI, uriToFsPath } from './configurationCommonUtils'; +import { RemoteDocuments } from './editableFiles'; + +export type DevContainerConfig = DevContainerFromImageConfig | DevContainerFromDockerfileConfig | DevContainerFromDockerComposeConfig; + +export interface PortAttributes { + label: string | undefined; + onAutoForward: string | undefined; + elevateIfNeeded: boolean | undefined; +} + +export type UserEnvProbe = 'none' | 'loginInteractiveShell' | 'interactiveShell' | 'loginShell'; + +export type DevContainerConfigCommand = 'initializeCommand' | 'onCreateCommand' | 'updateContentCommand' | 'postCreateCommand' | 'postStartCommand' | 'postAttachCommand'; + +export interface HostRequirements { + cpus?: number; + memory?: string; + storage?: string; +} + +export interface DevContainerFromImageConfig { + configFilePath: URI; + image: string; + name?: string; + forwardPorts?: (number | string)[]; + appPort?: number | string | (number | string)[]; + portsAttributes?: Record; + otherPortsAttributes?: PortAttributes; + runArgs?: string[]; + shutdownAction?: 'none' | 'stopContainer'; + overrideCommand?: boolean; + initializeCommand?: string | string[]; + onCreateCommand?: string | string[]; + updateContentCommand?: string | string[]; + postCreateCommand?: string | string[]; + postStartCommand?: string | string[]; + postAttachCommand?: string | string[]; + waitFor?: DevContainerConfigCommand; + /** remote path to folder or workspace */ + workspaceFolder?: string; + workspaceMount?: string; + mounts?: string[]; + containerEnv?: Record; + remoteEnv?: Record; + containerUser?: string; + remoteUser?: string; + updateRemoteUserUID?: boolean; + userEnvProbe?: UserEnvProbe; + features?: Record>; + hostRequirements?: HostRequirements; +} + +export type DevContainerFromDockerfileConfig = { + configFilePath: URI; + name?: string; + forwardPorts?: (number | string)[]; + appPort?: number | string | (number | string)[]; + portsAttributes?: Record; + otherPortsAttributes?: PortAttributes; + runArgs?: string[]; + shutdownAction?: 'none' | 'stopContainer'; + overrideCommand?: boolean; + initializeCommand?: string | string[]; + onCreateCommand?: string | string[]; + updateContentCommand?: string | string[]; + postCreateCommand?: string | string[]; + postStartCommand?: string | string[]; + postAttachCommand?: string | string[]; + waitFor?: DevContainerConfigCommand; + /** remote path to folder or workspace */ + workspaceFolder?: string; + workspaceMount?: string; + mounts?: string[]; + containerEnv?: Record; + remoteEnv?: Record; + containerUser?: string; + remoteUser?: string; + updateRemoteUserUID?: boolean; + userEnvProbe?: UserEnvProbe; + features?: Record>; + hostRequirements?: HostRequirements; +} & ( + { + dockerFile: string; + context?: string; + build?: { + target?: string; + args?: Record; + cacheFrom?: string | string[]; + }; + } + | + { + build: { + dockerfile: string; + context?: string; + target?: string; + args?: Record; + cacheFrom?: string | string[]; + }; + } +); + +export interface DevContainerFromDockerComposeConfig { + configFilePath: URI; + dockerComposeFile: string | string[]; + service: string; + workspaceFolder: string; + name?: string; + forwardPorts?: (number | string)[]; + portsAttributes?: Record; + otherPortsAttributes?: PortAttributes; + shutdownAction?: 'none' | 'stopCompose'; + overrideCommand?: boolean; + initializeCommand?: string | string[]; + onCreateCommand?: string | string[]; + updateContentCommand?: string | string[]; + postCreateCommand?: string | string[]; + postStartCommand?: string | string[]; + postAttachCommand?: string | string[]; + waitFor?: DevContainerConfigCommand; + runServices?: string[]; + remoteEnv?: Record; + remoteUser?: string; + updateRemoteUserUID?: boolean; + userEnvProbe?: UserEnvProbe; + features?: Record>; + hostRequirements?: HostRequirements; +} + +interface DevContainerVSCodeConfig { + extensions?: string[]; + settings?: object; + devPort?: number; +} + +export function updateFromOldProperties(original: T): T { + // https://github.com/microsoft/dev-container-spec/issues/1 + if (!(original.extensions || original.settings || original.devPort !== undefined)) { + return original; + } + const copy = { ...original }; + const customizations = copy.customizations || (copy.customizations = {}); + const vscode = customizations.vscode || (customizations.vscode = {}); + if (copy.extensions) { + vscode.extensions = (vscode.extensions || []).concat(copy.extensions); + delete copy.extensions; + } + if (copy.settings) { + vscode.settings = { + ...copy.settings, + ...(vscode.settings || {}), + }; + delete copy.settings; + } + if (copy.devPort !== undefined && vscode.devPort === undefined) { + vscode.devPort = copy.devPort; + delete copy.devPort; + } + return copy; +} + +export function getConfigFilePath(cliHost: { platform: NodeJS.Platform }, config: { configFilePath: URI }, relativeConfigFilePath: string) { + return resolveConfigFilePath(cliHost, config.configFilePath, relativeConfigFilePath); +} + +export function resolveConfigFilePath(cliHost: { platform: NodeJS.Platform }, configFilePath: URI, relativeConfigFilePath: string) { + const folder = parentURI(configFilePath); + return configFilePath.with({ + path: path.posix.resolve(folder.path, (cliHost.platform === 'win32' && configFilePath.scheme !== RemoteDocuments.scheme) ? (path.win32.isAbsolute(relativeConfigFilePath) ? '/' : '') + relativeConfigFilePath.replace(/\\/g, '/') : relativeConfigFilePath) + }); +} + +export function isDockerFileConfig(config: DevContainerConfig): config is DevContainerFromDockerfileConfig { + return 'dockerFile' in config || ('build' in config && 'dockerfile' in config.build); +} + +export function getDockerfilePath(cliHost: { platform: NodeJS.Platform }, config: DevContainerFromDockerfileConfig) { + return getConfigFilePath(cliHost, config, getDockerfile(config)); +} + +export function getDockerfile(config: DevContainerFromDockerfileConfig) { + return 'dockerFile' in config ? config.dockerFile : config.build.dockerfile; +} + +export async function getDockerComposeFilePaths(cliHost: FileHost, config: DevContainerFromDockerComposeConfig, envForComposeFile?: NodeJS.ProcessEnv, cwdForDefaultFiles?: string) { + if (Array.isArray(config.dockerComposeFile)) { + if (config.dockerComposeFile.length) { + return config.dockerComposeFile.map(composeFile => uriToFsPath(getConfigFilePath(cliHost, config, composeFile), cliHost.platform)); + } + } else if (typeof config.dockerComposeFile === 'string') { + return [uriToFsPath(getConfigFilePath(cliHost, config, config.dockerComposeFile), cliHost.platform)]; + } + if (cwdForDefaultFiles) { + const envComposeFile = envForComposeFile?.COMPOSE_FILE; + if (envComposeFile) { + return envComposeFile.split(cliHost.path.delimiter) + .map(composeFile => cliHost.path.resolve(cwdForDefaultFiles, composeFile)); + } + + try { + const envPath = cliHost.path.join(cwdForDefaultFiles, '.env'); + const buffer = await cliHost.readFile(envPath); + const match = /^COMPOSE_FILE=(.+)$/m.exec(buffer.toString()); + const envFileComposeFile = match && match[1].trim(); + if (envFileComposeFile) { + return envFileComposeFile.split(cliHost.path.delimiter) + .map(composeFile => cliHost.path.resolve(cwdForDefaultFiles, composeFile)); + } + } catch (err) { + if (!(err && (err.code === 'ENOENT' || err.code === 'EISDIR'))) { + throw err; + } + } + + const defaultFiles = [cliHost.path.resolve(cwdForDefaultFiles, 'docker-compose.yml')]; + const override = cliHost.path.resolve(cwdForDefaultFiles, 'docker-compose.override.yml'); + if (await cliHost.isFile(override)) { + defaultFiles.push(override); + } + return defaultFiles; + } + return []; +} diff --git a/src/spec-configuration/configurationCommonUtils.ts b/src/spec-configuration/configurationCommonUtils.ts new file mode 100644 index 00000000..9e45d613 --- /dev/null +++ b/src/spec-configuration/configurationCommonUtils.ts @@ -0,0 +1,75 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as path from 'path'; + +import { URI } from 'vscode-uri'; + +import { CLIHostDocuments } from './editableFiles'; +import { FileHost } from '../spec-utils/pfs'; + +export { FileHost, FileTypeBitmask } from '../spec-utils/pfs'; + +const enum CharCode { + Slash = 47, + Colon = 58, + A = 65, + Z = 90, + a = 97, + z = 122, +} + +export function uriToFsPath(uri: URI, platform: NodeJS.Platform): string { + + let value: string; + if (uri.authority && uri.path.length > 1 && (uri.scheme === 'file' || uri.scheme === CLIHostDocuments.scheme)) { + // unc path: file://shares/c$/far/boo + value = `//${uri.authority}${uri.path}`; + } else if ( + uri.path.charCodeAt(0) === CharCode.Slash + && (uri.path.charCodeAt(1) >= CharCode.A && uri.path.charCodeAt(1) <= CharCode.Z || uri.path.charCodeAt(1) >= CharCode.a && uri.path.charCodeAt(1) <= CharCode.z) + && uri.path.charCodeAt(2) === CharCode.Colon + ) { + // windows drive letter: file:///c:/far/boo + value = uri.path[1].toLowerCase() + uri.path.substr(2); + } else { + // other path + value = uri.path; + } + if (platform === 'win32') { + value = value.replace(/\//g, '\\'); + } + return value; +} + +export function getWellKnownDevContainerPaths(path_: typeof path.posix | typeof path.win32, folderPath: string): string[] { + return [ + path_.join(folderPath, '.devcontainer', 'devcontainer.json'), + path_.join(folderPath, '.devcontainer.json'), + ]; +} + +export function getDefaultDevContainerConfigPath(fileHost: FileHost, configFolderPath: string) { + return URI.file(fileHost.path.join(configFolderPath, '.devcontainer', 'devcontainer.json')) + .with({ scheme: CLIHostDocuments.scheme }); +} + +export async function getDevContainerConfigPathIn(fileHost: FileHost, configFolderPath: string) { + const possiblePaths = getWellKnownDevContainerPaths(fileHost.path, configFolderPath); + + for (let possiblePath of possiblePaths) { + if (await fileHost.isFile(possiblePath)) { + return URI.file(possiblePath) + .with({ scheme: CLIHostDocuments.scheme }); + } + } + + return undefined; +} + +export function parentURI(uri: URI) { + const parent = path.posix.dirname(uri.path); + return uri.with({ path: parent }); +} diff --git a/src/spec-configuration/containerFeaturesConfiguration.ts b/src/spec-configuration/containerFeaturesConfiguration.ts new file mode 100644 index 00000000..fbc8f528 --- /dev/null +++ b/src/spec-configuration/containerFeaturesConfiguration.ts @@ -0,0 +1,830 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as jsonc from 'jsonc-parser'; +import * as path from 'path'; +import * as semver from 'semver'; +import * as URL from 'url'; +import * as tar from 'tar'; +import { DevContainerConfig } from './configuration'; +import { mkdirpLocal, readLocalFile, rmLocal, writeLocalFile } from '../spec-utils/pfs'; +import { Log, LogLevel } from '../spec-utils/log'; +import { request } from '../spec-utils/httpRequest'; + +const ASSET_NAME = 'devcontainer-features.tgz'; + +export interface Feature { + id: string; + name: string; + documentationURL?: string; + options?: Record; + buildArg?: string; // old properties for temporary compatibility + containerEnv?: Record; + mounts?: Mount[]; + init?: boolean; + privileged?: boolean; + capAdd?: string[]; + securityOpt?: string[]; + entrypoint?: string; + include?: string[]; + exclude?: string[]; + value: boolean | string | Record; // set programmatically + included: boolean; // set programmatically +} + +export type FeatureOption = { + type: 'boolean'; + default?: boolean; + description?: string; +} | { + type: 'string'; + enum?: string[]; + default?: string; + description?: string; +} | { + type: 'string'; + proposals?: string[]; + default?: string; + description?: string; +}; +export interface Mount { + type: 'bind' | 'volume'; + source: string; + target: string; + external?: boolean; +} + +export type SourceInformation = LocalCacheSourceInformation | GithubSourceInformation | DirectTarballSourceInformation | FilePathSourceInformation; + +interface BaseSourceInformation { + type: string; +} + +export interface LocalCacheSourceInformation extends BaseSourceInformation { + type: 'local-cache'; +} + +export interface GithubSourceInformation extends BaseSourceInformation { + type: 'github-repo'; + apiUri: string; + unauthenticatedUri: string; + owner: string; + repo: string; + isLatest: boolean; // 'true' indicates user didn't supply a version tag, thus we implicitly pull latest. + tag?: string; + ref?: string; + sha?: string; +} + +export interface GithubSourceInformationInput { + owner: string; + repo: string; + ref?: string; + sha?: string; + tag?: string; +} + +export interface DirectTarballSourceInformation extends BaseSourceInformation { + type: 'direct-tarball'; + tarballUri: string; +} + +export interface FilePathSourceInformation extends BaseSourceInformation { + type: 'file-path'; + filePath: string; + isRelative: boolean; // If not a relative path, then it is an absolute path. +} + +export interface FeatureSet { + features: Feature[]; + sourceInformation: SourceInformation; +} + +export interface FeaturesConfig { + featureSets: FeatureSet[]; + dstFolder?: string; // set programatically +} + +export interface GitHubApiReleaseInfo { + assets: GithubApiReleaseAsset[]; + name: string; + tag_name: string; +} + +export interface GithubApiReleaseAsset { + url: string; + name: string; + content_type: string; + size: number; + download_count: number; + updated_at: string; +} + +// Supports the `node` layer by collapsing all the individual features into a single `features` array. +// Regardless of their origin. +// Information is lost, but for the node layer we need not care about which set a given feature came from. +export interface CollapsedFeaturesConfig { + allFeatures: Feature[]; +} + +export function collapseFeaturesConfig(original: FeaturesConfig | undefined): CollapsedFeaturesConfig | undefined { + + if (!original) { + return undefined; + } + + const collapsed = { + allFeatures: original.featureSets + .map(fSet => fSet.features) + .flat() + }; + return collapsed; +} + +export const multiStageBuildExploration = false; + +const isTsnode = path.basename(process.argv[0]) === 'ts-node' || process.argv.indexOf('ts-node/register') !== -1; + +export function getContainerFeaturesFolder(_extensionPath: string | { distFolder: string }) { + if (isTsnode) { + return path.join(require.resolve('vscode-dev-containers/package.json'), '..', 'container-features'); + } + const distFolder = typeof _extensionPath === 'string' ? path.join(_extensionPath, 'dist') : _extensionPath.distFolder; + return path.join(distFolder, 'node_modules', 'vscode-dev-containers', 'container-features'); +} + +// Take a SourceInformation and condense it down into a single string +// Useful for calculating a unique build folder name for a given featureSet. +export function getSourceInfoString(srcInfo: SourceInformation): string { + const { type } = srcInfo; + switch (type) { + case 'local-cache': + return 'local-cache'; + case 'direct-tarball': + return Buffer.from(srcInfo.tarballUri).toString('base64'); + case 'github-repo': + return `github-${srcInfo.owner}-${srcInfo.repo}-${srcInfo.isLatest ? 'latest' : srcInfo.tag}`; + case 'file-path': + return Buffer.from(srcInfo.filePath).toString('base64'); + } +} + +// TODO: Move to node layer. +export function getContainerFeaturesBaseDockerFile() { + return ` +ARG BASE_IMAGE=mcr.microsoft.com/vscode/devcontainers/base:buster + +#{featureBuildStages} + +FROM $BASE_IMAGE + +USER root + +COPY . /tmp/build-features/ + +#{featureLayer} + +#{copyFeatureBuildStages} + +#{containerEnv} + +ARG IMAGE_USER=root +USER $IMAGE_USER +`; +} + +export function getFeatureLayers(featuresConfig: FeaturesConfig) { + let result = ''; + const folders = (featuresConfig.featureSets || []).map(x => getSourceInfoString(x.sourceInformation)); + folders.forEach(folder => { + result += `RUN cd /tmp/build-features/${folder} \\ +&& chmod +x ./install.sh \\ +&& ./install.sh + +`; + }); + return result; +} + +// Parses a declared feature in user's devcontainer file into +// a usable URI to download remote features. +// RETURNS +// { +// "id", <----- The ID of the feature in the feature set. +// sourceInformation <----- Source information (is this locally cached, a GitHub remote feature, etc..), including tarballUri if applicable. +// } +// +export function parseFeatureIdentifier(input: string, output: Log): { id: string; sourceInformation: SourceInformation } | undefined { + // A identifier takes this form: + // (0) + // (1) //@version + // (2) https://<../URI/..>/devcontainer-features.tgz# + // (3) ./# -or- ../# -or- /# + // + // (0) This is a locally cached feature. The function should return `undefined` for tarballUrl + // + // (1) Our "registry" is backed by GitHub public repositories (or repos visible with the environment's GITHUB_TOKEN). + // Say organization 'octocat' has a repo titled 'myfeatures' with a set of feature definitions. + // One of the [1..n] features in this repo has an id of 'helloworld'. + // + // eg: octocat/myfeatures/helloworld + // + // The above example assumes the 'latest' GitHub release, and internally will + // fetch the devcontainer-features.tgz artifact from that release. + // To specify a certain release tag, append the tag with an @ symbol + // + // eg: octocat/myfeatures/helloworld@v0.0.2 + // + // (2) A fully-qualified https URI to a devcontainer-features.tgz file can be provided instead + // of a using the GitHub registry "shorthand". Note this is identified by a + // s.StartsWith("https://" || "http://"). + // + // eg: https://example.com/../../devcontainer-features.tgz#helloworld + // + // (3) This is a local path to a directory on disk following the expected file convention + // The path can either be: + // - a relative file path to the .devcontainer file (prepended by a ./ or ../) + // - an absolute file path (prepended by a /) + // + // No version can be provided, as the directory is copied 'as is' and is inherently taking the 'latest' + + // Regexes + const allowedFeatureIdRegex = new RegExp('^[a-zA-Z0-9_-]*$'); + + // Case (0): Cached feature + if (!input.includes('/')) { + output.write(`[${input}] - No slash, must be locally cached feature.`, LogLevel.Trace); + return { + id: input, + sourceInformation: { type: 'local-cache' }, + }; + } + + // Case (2): Direct URI to a tgz + if (input.startsWith('http://') || input.startsWith('https://')) { + output.write(`[${input}] - Direct URI`, LogLevel.Trace); + + // Trim any trailing slash character to make parsing easier. + // A slash at the end of the direct tgz identifier is not important. + input = input.replace(/\/+$/, ''); + + // Parse out feature ID by splitting on final slash character. + const featureIdDelimiter = input.lastIndexOf('#'); + const id = input.substring(featureIdDelimiter + 1); + // Ensure feature id only contains the expected set of characters. + if (id === '' || !allowedFeatureIdRegex.test(id)) { + output.write(`Parse error. Specify a feature id with alphanumeric, dash, or underscore characters. Provided: ${id}.`, LogLevel.Error); + return undefined; + } + const tarballUri = + new URL.URL(input.substring(0, featureIdDelimiter)) + .toString(); + + output.write(`[${input}] - uri: ${tarballUri} , id: ${id}`, LogLevel.Trace); + return { + id, + sourceInformation: { 'type': 'direct-tarball', tarballUri } + }; + } + + // Case (3): Local disk relative/absolute path to directory + if (input.startsWith('/') || input.startsWith('./') || input.startsWith('../')) { + // Currently unimplemented. + return undefined; + + // const splitOnHash = input.split('#'); + // if (!splitOnHash || splitOnHash.length !== 2) { + // output.write(`Parse error. Relative or absolute path to directory should be of the form: #`, LogLevel.Error); + // return undefined; + // } + // const filePath = splitOnHash[0]; + // const id = splitOnHash[1]; + // if (!allowedFeatureIdRegex.test(id)) { + // output.write(`Parse error. Specify a feature id with alphanumeric, dash, or underscore characters. Provided: ${id}.`, LogLevel.Error); + // return undefined; + // } + // return { + // id, + // sourceInformation: { 'type': 'file-path', filePath, isRelative: input.startsWith('./') } + // }; + } + + // Must be case (1) - GH + let version = 'latest'; + let splitOnAt = input.split('@'); + if (splitOnAt.length > 2) { + output.write(`Parse error. Use the '@' symbol only to designate a version tag.`, LogLevel.Error); + return undefined; + } + if (splitOnAt.length === 2) { + output.write(`[${input}] has version ${splitOnAt[1]}`, LogLevel.Trace); + version = splitOnAt[1]; + } + // Remaining info must be in the first part of the split. + const featureBlob = splitOnAt[0]; + const splitOnSlash = featureBlob.split('/'); + // We expect all GitHub/registry features to follow the triple slash pattern at this point + // eg: // + if (splitOnSlash.length !== 3 || splitOnSlash.some(x => x === '') || !allowedFeatureIdRegex.test(splitOnSlash[2])) { + output.write(`Invalid parse for GitHub/registry feature identifier. Follow format: '//'`, LogLevel.Error); + return undefined; + } + const owner = splitOnSlash[0]; + const repo = splitOnSlash[1]; + const id = splitOnSlash[2]; + + // Return expected tarball URI for a latest release on the parsed repo. + const ghSrcInfo = createGitHubSourceInformation({ owner, repo, tag: version }); + return { + id, + sourceInformation: ghSrcInfo + }; +} + +export function createGitHubSourceInformation(params: GithubSourceInformationInput): GithubSourceInformation { + const { owner, repo, tag } = params; + if (tag === 'latest') { + return { + type: 'github-repo', + apiUri: `https://api.github.com/repos/${owner}/${repo}/releases/latest`, + unauthenticatedUri: `https://github.com/${owner}/${repo}/releases/latest/download/${ASSET_NAME}`, + owner, + repo, + isLatest: true + }; + } else { + // We must have a tag, return a tarball URI for the tagged version. + return { + type: 'github-repo', + apiUri: `https://api.github.com/repos/${owner}/${repo}/releases/tags/${tag}`, + unauthenticatedUri: `https://github.com/${owner}/${repo}/releases/download/${tag}/${ASSET_NAME}`, + owner, + repo, + tag, + isLatest: false + }; + } +} + + +const cleanupIterationFetchAndMerge = async (tempTarballPath: string, output: Log) => { + // Non-fatal, will just get overwritten if we don't do the cleaned up. + try { + await rmLocal(tempTarballPath, { force: true }); + } catch (e) { + output.write(`Didn't remove temporary tarball from disk with caught exception: ${e?.Message} `, LogLevel.Trace); + } +}; + +function getRequestHeaders(sourceInformation: SourceInformation, env: NodeJS.ProcessEnv, output: Log) { + let headers: { 'user-agent': string; 'Authorization'?: string; 'Accept'?: string } = { + 'user-agent': 'devcontainer' + }; + + const isGitHubUri = (srcInfo: DirectTarballSourceInformation) => { + const uri = srcInfo.tarballUri; + return uri.startsWith('https://github.com') || uri.startsWith('https://api.github.com'); + }; + + if (sourceInformation.type === 'github-repo' || (sourceInformation.type === 'direct-tarball' && isGitHubUri(sourceInformation))) { + const githubToken = env['GITHUB_TOKEN']; + if (githubToken) { + output.write('Using environment GITHUB_TOKEN.'); + headers.Authorization = `Bearer ${githubToken}`; + } else { + output.write('No environment GITHUB_TOKEN available.'); + } + } + return headers; +} + +async function fetchAndMergeRemoteFeaturesAsync(params: { extensionPath: string; output: Log; env: NodeJS.ProcessEnv }, featuresConfig: FeaturesConfig, config: DevContainerConfig) { + + const { output, env } = params; + const { dstFolder } = featuresConfig; + let buildFoldersCreatedAlready: String[] = []; + + // The requested features from the user's devcontainer + const features = config.features; + if (!features || !Object.keys(features).length) { + return undefined; + } + + // We need a dstFolder to know where to download remote resources to + if (!dstFolder) { + return undefined; + } + + const tempTarballPath = path.join(dstFolder, ASSET_NAME); + + output.write(`Preparing to parse declared features and fetch remote features.`); + + for await (const id of Object.keys(features)) { + const remoteFeatureParsed = parseFeatureIdentifier(id, output); + + if (remoteFeatureParsed === undefined) { + output.write(`Failed to parse key: ${id}`, LogLevel.Error); + // Failed to parse. + // TODO: Should be more fatal. + continue; + } + + // -- Next section handles each possible type of "SourceInformation" + + const featureName = remoteFeatureParsed.id; + const sourceInformation = remoteFeatureParsed.sourceInformation; + const sourceType = sourceInformation.type; + + if (sourceType === 'local-cache') { + output.write(`Detected local feature set. Continuing...`); + continue; + } + + const buildFolderName = getSourceInfoString(remoteFeatureParsed.sourceInformation); + // Calculate some predictable caching paths. + // Don't create the folder on-disk until we need it. + const featCachePath = path.join(dstFolder, buildFolderName); + + // Break out earlier if already copied over remote features to dstFolder + const alreadyExists = buildFoldersCreatedAlready.some(x => x === buildFolderName); + if (alreadyExists) { + output.write(`Already pulled remote resource for '${buildFolderName}'. No need to re-fetch.`); //TODO: not true, might have been updated on the repo since if we pulled `local`. Should probably use commit SHA? + continue; + } + + output.write(`Fetching: featureSet = ${buildFolderName}, feature = ${featureName}, Type = ${sourceType}`); + + if (sourceType === 'file-path') { + output.write(`Local file-path to features on disk is unimplemented. Continuing...`); + continue; + } else { + let tarballUri: string | undefined = undefined; + const headers = getRequestHeaders(sourceInformation, env, output); + + // If this is 'github-repo', we need to do an API call to fetch the appropriate asset's tarballUri + if (sourceType === 'github-repo') { + output.write('Determining tarball URI for provided github repo.', LogLevel.Trace); + if (headers.Authorization && headers.Authorization !== '') { + output.write('Authenticated. Fetching from GH API.', LogLevel.Trace); + tarballUri = await askGitHubApiForTarballUri(sourceInformation, headers, output); + headers.Accept = 'Accept: application/octet-stream'; + } else { + output.write('Not authenticated. Fetching from unauthenticated uri', LogLevel.Trace); + tarballUri = sourceInformation.unauthenticatedUri; + } + } else if (sourceType === 'direct-tarball') { + tarballUri = sourceInformation.tarballUri; + } else { + output.write(`Unhandled source type: ${sourceType}`, LogLevel.Error); + continue; // TODO: Should be more fatal? + } + + // uri direct to the tarball either acquired at this point, or failed. + if (tarballUri !== undefined && tarballUri !== '') { + const options = { + type: 'GET', + url: tarballUri, + headers + }; + output.write(`Fetching tarball at ${options.url}`); + output.write(`Headers: ${JSON.stringify(options)}`, LogLevel.Trace); + const tarball = await request(options, output); + + if (!tarball || tarball.length === 0) { + output.write(`Did not receive a response from tarball download URI`, LogLevel.Error); + // Continue loop to the next remote feature. + // TODO: Should be more fatal. + await cleanupIterationFetchAndMerge(tempTarballPath, output); + continue; + } + + // Filter what gets emitted from the tar.extract(). + const filter = (file: string, _: tar.FileStat) => { + // Don't include .dotfiles or the archive itself. + if (file.startsWith('./.') || file === `./${ASSET_NAME}` || file === './.') { + return false; + } + return true; + }; + + output.write(`Preparing to unarchive received tgz.`, LogLevel.Trace); + // Create the directory to cache this feature-set in. + await mkdirpLocal(featCachePath); + await writeLocalFile(tempTarballPath, tarball); + await tar.x( + { + file: tempTarballPath, + cwd: featCachePath, + filter + } + ); + + } else { + output.write(`Could not fetch features from constructed tarball URL`, LogLevel.Error); + // Continue loop to the next remote feature. + // TODO: Should be more fatal. + await cleanupIterationFetchAndMerge(tempTarballPath, output); + continue; + } + } + + // -- Whichever modality the feature-set was stored, at this point that process of retrieving and extracting a feature-set has completed successfully. + // Now, load in the devcontainer-features.json from the `featureCachePath` and continue merging into the featuresConfig. + + output.write('Attempting to load devcontainer-features.json', LogLevel.Trace); + let newFeaturesSet: FeatureSet | undefined = await loadFeaturesJsonFromDisk(featCachePath, output); + + if (!newFeaturesSet || !newFeaturesSet.features || newFeaturesSet.features.length === 0) { + output.write(`Unable to parse received devcontainer-features.json.`, LogLevel.Error); + // TODO: Should be more fatal? + await cleanupIterationFetchAndMerge(tempTarballPath, output); + continue; + } + output.write(`Done loading FeatureSet ${buildFolderName} into from disk into memory`, LogLevel.Trace); + + // Merge sourceInformation if the remote featureSet provides one. + // Priority is to maintain the values we had calculated previously. + if (newFeaturesSet.sourceInformation) { + newFeaturesSet = { + ...newFeaturesSet, + sourceInformation: { ...newFeaturesSet.sourceInformation, ...sourceInformation }, + }; + } + output.write(`Merged sourceInfomation`, LogLevel.Trace); + + // Add this new feature set to our featuresConfig + featuresConfig.featureSets.push(newFeaturesSet); + // Remember that we've succeeded in fetching this featureSet + buildFoldersCreatedAlready.push(buildFolderName); + + // Clean-up + await cleanupIterationFetchAndMerge(tempTarballPath, output); + output.write(`Succeeded in fetching feature set ${buildFolderName}`, LogLevel.Trace); + } + + // Return updated featuresConfig + return featuresConfig; +} + + +async function askGitHubApiForTarballUri(sourceInformation: GithubSourceInformation, headers: { 'user-agent': string; 'Authorization'?: string; 'Accept'?: string }, output: Log) { + const options = { + type: 'GET', + url: sourceInformation.apiUri, + headers + }; + const apiInfo: GitHubApiReleaseInfo = JSON.parse(((await request(options, output)).toString())); + if (apiInfo) { + const asset = apiInfo.assets.find(a => a.name === ASSET_NAME); + if (asset && asset.url) { + output.write(`Found url to fetch release artifact ${asset.name}. Asset of size ${asset.size} has been downloaded ${asset.download_count} times and was last updated at ${asset.updated_at}`); + return asset.url; + } else { + output.write('Unable to fetch release artifact URI from GitHub API', LogLevel.Error); + return undefined; + } + } + return undefined; +} + +export async function loadFeaturesJson(jsonBuffer: Buffer, output: Log): Promise { + if (jsonBuffer.length === 0) { + output.write('Parsed featureSet is empty.', LogLevel.Error); + return undefined; + } + + const featureSet: FeatureSet = jsonc.parse(jsonBuffer.toString()); + if (!featureSet?.features || featureSet.features.length === 0) { + output.write('Parsed featureSet contains no features.', LogLevel.Error); + return undefined; + } + output.write(`Loaded devcontainer-features.json declares ${featureSet.features.length} features and ${(!!featureSet.sourceInformation) ? 'contains' : 'does not contain'} explicit source info.`, + LogLevel.Trace); + + return updateFromOldProperties(featureSet); +} + +export async function loadFeaturesJsonFromDisk(pathToDirectory: string, output: Log): Promise { + const jsonBuffer: Buffer = await readLocalFile(path.join(pathToDirectory, 'devcontainer-features.json')); + return loadFeaturesJson(jsonBuffer, output); +} + +function updateFromOldProperties(original: T): T { + // https://github.com/microsoft/dev-container-spec/issues/1 + if (!original.features.find(f => f.extensions || f.settings)) { + return original; + } + return { + ...original, + features: original.features.map(f => { + if (!(f.extensions || f.settings)) { + return f; + } + const copy = { ...f }; + const customizations = copy.customizations || (copy.customizations = {}); + const vscode = customizations.vscode || (customizations.vscode = {}); + if (copy.extensions) { + vscode.extensions = (vscode.extensions || []).concat(copy.extensions); + delete copy.extensions; + } + if (copy.settings) { + vscode.settings = { + ...copy.settings, + ...(vscode.settings || {}), + }; + delete copy.settings; + } + return copy; + }), + }; +} + +// Generate a base featuresConfig object with the set of locally-cached features, +// as well as downloading and merging in remote feature definitions. +export async function generateFeaturesConfig(params: { extensionPath: string; output: Log; env: NodeJS.ProcessEnv }, dstFolder: string, config: DevContainerConfig, imageLabels: () => Promise>, getLocalFolder: (d: string) => string) { + const { output } = params; + + const userDeclaredFeatures = config.features; + if (!userDeclaredFeatures || !Object.keys(userDeclaredFeatures).length) { + return undefined; + } + + // Create the featuresConfig object. + // Initialize the featureSets object, and stash the dstFolder on the object for use later. + let featuresConfig: FeaturesConfig = { + featureSets: [], + dstFolder + }; + + let locallyCachedFeatureSet = await loadFeaturesJsonFromDisk(getLocalFolder(params.extensionPath), output); // TODO: Pass dist folder instead to also work with the devcontainer.json support package. + if (!locallyCachedFeatureSet) { + output.write('Failed to load locally cached features', LogLevel.Error); + return undefined; + } + + // Add in the locally cached features + locallyCachedFeatureSet = { + ...locallyCachedFeatureSet, + sourceInformation: { 'type': 'local-cache' }, + }; + + // Push feature set to FeaturesConfig + featuresConfig.featureSets.push(locallyCachedFeatureSet); + + // Parse, fetch, and merge information on remote features (if any). + // TODO: right now if anything fails in this method and we return `undefined`, we fallback to just the prior state of featureConfig (locally cached only). Is that what we want?? + featuresConfig = await fetchAndMergeRemoteFeaturesAsync(params, featuresConfig, config) ?? featuresConfig; + + // Run filtering and include user options into config. + featuresConfig = await doReadUserDeclaredFeatures(params, config, featuresConfig, imageLabels); + if (featuresConfig.featureSets.every(set => + set.features.every(feature => feature.value === false))) { + return undefined; + } + + return featuresConfig; +} + +const getUniqueFeatureId = (id: string, srcInfo: SourceInformation) => `${id}-${getSourceInfoString(srcInfo)}`; + +// Given an existing featuresConfig, parse the user's features as they declared them in their devcontainer. +export async function doReadUserDeclaredFeatures(params: { output: Log }, config: DevContainerConfig, featuresConfig: FeaturesConfig, imageLabels: () => Promise>) { + + const { output } = params; + const labels = await imageLabels(); + const definition = labels['com.visualstudio.code.devcontainers.id']; + const version = labels['version']; + + // Map user's declared features to its appropriate feature-set feature. + let configFeatures = config.features || {}; + let userValues: Record> = {}; + for (const feat of Object.keys(configFeatures)) { + const { id, sourceInformation } = parseFeatureIdentifier(feat, output) ?? {}; + if (id && sourceInformation) { + const uniqueId = getUniqueFeatureId(id, sourceInformation); + userValues[uniqueId] = configFeatures[feat]; + } else { + output.write(`Failed to read user declared feature ${feat}. Skipping.`, LogLevel.Error); + continue; + } + } + + const included = {} as Record; + for (const featureSet of featuresConfig.featureSets) { + for (const feature of featureSet.features) { + updateFeature(feature); // REMOVEME: Temporary migration. + const uniqueFeatureId = getUniqueFeatureId(feature.id, featureSet.sourceInformation); + + // Compare the feature to the base definition. + if (definition && (feature.exclude || []).some(e => matches(e, definition, version))) { + // The feature explicitly excludes the detected base definition + feature.included = false; + } else if ('include' in feature) { + // The feature explicitly includes one or more base definitions + // Set the included flag to true IFF we have detected a base definition, and its in the feature's list of includes + feature.included = !!definition && (feature.include || []).some(e => matches(e, definition, version)); + } else { + // The feature doesn't define any base definitions to "include" or "exclude" in which we can filter on. + // By default, include it. + feature.included = true; + } + + // Mark feature as with its state of inclusion + included[uniqueFeatureId] = included[uniqueFeatureId] || feature.included; + + // Set the user-defined values from the user's devcontainer onto the feature config. + feature.value = userValues[uniqueFeatureId] || false; + } + } + params.output.write('Feature configuration:\n' + JSON.stringify({ ...featuresConfig, imageDetails: undefined }, undefined, ' '), LogLevel.Trace); + + // Filter + for (const featureSet of featuresConfig.featureSets) { + featureSet.features = featureSet.features.filter(feature => { + const uniqueFeatureId = getUniqueFeatureId(feature.id, featureSet.sourceInformation); + // Ensure we are not including duplicate features. + // Note: Takes first feature even if f.included == false. + if (uniqueFeatureId in included && feature.included === included[uniqueFeatureId]) { // TODO: This logic should be revisited. + delete included[feature.id]; + return true; + } + return false; + }); + } + return featuresConfig; +} + +function updateFeature(feature: Feature & { type?: 'option' | 'choice'; values?: string[]; customValues?: boolean; hint?: string }) { + // Update old to new properties for temporary compatiblity. + if (feature.values) { + const options = feature.options || (feature.options = {}); + options.version = { + type: 'string', + [feature.customValues ? 'proposals' : 'enum']: feature.values, + default: feature.values[0], + description: feature.hint, + }; + } + delete feature.type; + delete feature.values; + delete feature.customValues; + delete feature.hint; +} + +function matches(spec: string, definition: string, version: string | undefined) { + const i = spec.indexOf('@'); + const [specDefinition, specVersion] = i !== -1 ? [spec.slice(0, i), spec.slice(i + 1)] : [spec, undefined]; + return definition === specDefinition && (!specVersion || !version || semver.satisfies(version, specVersion)); +} + +export function getFeatureMainProperty(feature: Feature) { + return feature.options?.version ? 'version' : undefined; +} + +export function getFeatureMainValue(feature: Feature) { + const defaultProperty = getFeatureMainProperty(feature); + if (!defaultProperty) { + return !!feature.value; + } + if (typeof feature.value === 'object') { + const value = feature.value[defaultProperty]; + if (value === undefined && feature.options) { + return feature.options[defaultProperty]?.default; + } + return value; + } + if (feature.value === undefined && feature.options) { + return feature.options[defaultProperty]?.default; + } + return feature.value; +} + +export function getFeatureValueObject(feature: Feature) { + if (typeof feature.value === 'object') { + return { + ...getFeatureValueDefaults(feature), + ...feature.value + }; + } + const mainProperty = getFeatureMainProperty(feature); + if (!mainProperty) { + return getFeatureValueDefaults(feature); + } + return { + ...getFeatureValueDefaults(feature), + [mainProperty]: feature.value, + }; +} + +function getFeatureValueDefaults(feature: Feature) { + const options = feature.options || {}; + return Object.keys(options) + .reduce((defaults, key) => { + if ('default' in options[key]) { + defaults[key] = options[key].default; + } + return defaults; + }, {} as Record); +} diff --git a/src/spec-configuration/editableFiles.ts b/src/spec-configuration/editableFiles.ts new file mode 100644 index 00000000..6e59ad00 --- /dev/null +++ b/src/spec-configuration/editableFiles.ts @@ -0,0 +1,183 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as crypto from 'crypto'; +import * as jsonc from 'jsonc-parser'; +import { promisify } from 'util'; +import { URI } from 'vscode-uri'; +import { uriToFsPath, FileHost } from './configurationCommonUtils'; +import { readLocalFile, writeLocalFile } from '../spec-utils/pfs'; + +export type Edit = jsonc.Edit; + +export interface Documents { + readDocument(uri: URI): Promise; + applyEdits(uri: URI, edits: Edit[], content: string): Promise; +} + +export const fileDocuments: Documents = { + + async readDocument(uri: URI) { + switch (uri.scheme) { + case 'file': + try { + const buffer = await readLocalFile(uri.fsPath); + return buffer.toString(); + } catch (err) { + if (err && err.code === 'ENOENT') { + return undefined; + } + throw err; + } + default: + throw new Error(`Unsupported scheme: ${uri.toString()}`); + } + }, + + async applyEdits(uri: URI, edits: Edit[], content: string) { + switch (uri.scheme) { + case 'file': + const result = jsonc.applyEdits(content, edits); + await writeLocalFile(uri.fsPath, result); + break; + default: + throw new Error(`Unsupported scheme: ${uri.toString()}`); + } + } +}; + +export class CLIHostDocuments implements Documents { + + static scheme = 'vscode-fileHost'; + + constructor(private fileHost: FileHost) { + } + + async readDocument(uri: URI) { + switch (uri.scheme) { + case CLIHostDocuments.scheme: + try { + return (await this.fileHost.readFile(uriToFsPath(uri, this.fileHost.platform))).toString(); + } catch (err) { + return undefined; + } + default: + throw new Error(`Unsupported scheme: ${uri.toString()}`); + } + } + + async applyEdits(uri: URI, edits: Edit[], content: string) { + switch (uri.scheme) { + case CLIHostDocuments.scheme: + const result = jsonc.applyEdits(content, edits); + await this.fileHost.writeFile(uriToFsPath(uri, this.fileHost.platform), Buffer.from(result)); + break; + default: + throw new Error(`Unsupported scheme: ${uri.toString()}`); + } + } +} + +export class RemoteDocuments implements Documents { + + static scheme = 'vscode-remote'; + + private static nonce: string | undefined; + + constructor(private shellServer: ShellServer) { + } + + async readDocument(uri: URI) { + switch (uri.scheme) { + case RemoteDocuments.scheme: + try { + const { stdout } = await this.shellServer.exec(`cat ${uri.path}`); + return stdout; + } catch (err) { + return undefined; + } + default: + throw new Error(`Unsupported scheme: ${uri.toString()}`); + } + } + + async applyEdits(uri: URI, edits: Edit[], content: string) { + switch (uri.scheme) { + case RemoteDocuments.scheme: + try { + if (!RemoteDocuments.nonce) { + const buffer = await promisify(crypto.randomBytes)(20); + RemoteDocuments.nonce = buffer.toString('hex'); + } + const result = jsonc.applyEdits(content, edits); + const eof = `EOF-${RemoteDocuments.nonce}`; + await this.shellServer.exec(`cat <<'${eof}' >${uri.path} +${result} +${eof} +`); + } catch (err) { + console.log(err); // XXX + } + break; + default: + throw new Error(`Unsupported scheme: ${uri.toString()}`); + } + } +} + +export class AllDocuments implements Documents { + + constructor(private documents: Record) { + } + + async readDocument(uri: URI) { + const documents = this.documents[uri.scheme]; + if (!documents) { + throw new Error(`Unsupported scheme: ${uri.toString()}`); + } + return documents.readDocument(uri); + } + + async applyEdits(uri: URI, edits: Edit[], content: string) { + const documents = this.documents[uri.scheme]; + if (!documents) { + throw new Error(`Unsupported scheme: ${uri.toString()}`); + } + return documents.applyEdits(uri, edits, content); + } +} + +export function createDocuments(fileHost: FileHost, shellServer?: ShellServer): Documents { + const documents: Record = { + file: fileDocuments, + [CLIHostDocuments.scheme]: new CLIHostDocuments(fileHost), + }; + if (shellServer) { + documents[RemoteDocuments.scheme] = new RemoteDocuments(shellServer); + } + return new AllDocuments(documents); +} + +export interface ShellServer { + exec(cmd: string, options?: { logOutput?: boolean; stdin?: Buffer }): Promise<{ stdout: string; stderr: string }>; +} + +const editQueues = new Map Promise)[]>(); + +export async function runEdit(uri: URI, edit: () => Promise) { + const uriString = uri.toString(); + let queue = editQueues.get(uriString); + if (!queue) { + editQueues.set(uriString, queue = []); + } + queue.push(edit); + if (queue.length === 1) { + while (queue.length) { + await queue[0](); + queue.shift(); + } + editQueues.delete(uriString); + } +} diff --git a/src/spec-configuration/tsconfig.json b/src/spec-configuration/tsconfig.json new file mode 100644 index 00000000..eff31937 --- /dev/null +++ b/src/spec-configuration/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "references": [ + { + "path": "../spec-utils" + } + ] +} \ No newline at end of file diff --git a/src/spec-node/configContainer.ts b/src/spec-node/configContainer.ts new file mode 100644 index 00000000..0f3084c2 --- /dev/null +++ b/src/spec-node/configContainer.ts @@ -0,0 +1,103 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as path from 'path'; + +import * as jsonc from 'jsonc-parser'; + +import { openDockerfileDevContainer } from './singleContainer'; +import { openDockerComposeDevContainer } from './dockerCompose'; +import { ResolverResult, DockerResolverParameters, isDockerFileConfig, runUserCommand, createDocuments, getWorkspaceConfiguration, BindMountConsistency, uriToFsPath, DevContainerAuthority, isDevContainerAuthority } from './utils'; +import { substitute } from '../spec-common/variableSubstitution'; +import { ContainerError } from '../spec-common/errors'; +import { Workspace, workspaceFromPath, isWorkspacePath } from '../spec-utils/workspaces'; +import { URI } from 'vscode-uri'; +import { CLIHost } from '../spec-common/commonUtils'; +import { Log } from '../spec-utils/log'; +import { getDefaultDevContainerConfigPath, getDevContainerConfigPathIn } from '../spec-configuration/configurationCommonUtils'; +import { DevContainerConfig, updateFromOldProperties } from '../spec-configuration/configuration'; + +export { getWellKnownDevContainerPaths as getPossibleDevContainerPaths } from '../spec-configuration/configurationCommonUtils'; + +export async function resolve(params: DockerResolverParameters, configFile: URI | undefined, overrideConfigFile: URI | undefined, idLabels: string[]): Promise { + if (configFile && !/\/\.?devcontainer\.json$/.test(configFile.path)) { + throw new Error(`Filename must be devcontainer.json or .devcontainer.json (${uriToFsPath(configFile, params.common.cliHost.platform)}).`); + } + const parsedAuthority = params.parsedAuthority; + if (!parsedAuthority || isDevContainerAuthority(parsedAuthority)) { + return resolveWithLocalFolder(params, parsedAuthority, configFile, overrideConfigFile, idLabels); + } else { + throw new Error(`Unexpected authority: ${JSON.stringify(parsedAuthority)}`); + } +} + +async function resolveWithLocalFolder(params: DockerResolverParameters, parsedAuthority: DevContainerAuthority | undefined, configFile: URI | undefined, overrideConfigFile: URI | undefined, idLabels: string[]): Promise { + const { common, workspaceMountConsistencyDefault } = params; + const { cliHost, output } = common; + + const cwd = cliHost.cwd; // Can be inside WSL. + const workspace = parsedAuthority && workspaceFromPath(cliHost.path, isWorkspacePath(parsedAuthority.hostPath) ? cliHost.path.join(cwd, path.basename(parsedAuthority.hostPath)) : cwd); + + const configPath = configFile ? configFile : workspace + ? (await getDevContainerConfigPathIn(cliHost, workspace.configFolderPath) + || (overrideConfigFile ? getDefaultDevContainerConfigPath(cliHost, workspace.configFolderPath) : undefined)) + : overrideConfigFile; + const configs = configPath && await readDevContainerConfigFile(cliHost, workspace, configPath, params.mountWorkspaceGitRoot, output, workspaceMountConsistencyDefault, overrideConfigFile) || undefined; + if (!configs) { + if (configPath || workspace) { + throw new ContainerError({ description: `Dev container config (${uriToFsPath(configPath || getDefaultDevContainerConfigPath(cliHost, workspace!.configFolderPath), cliHost.platform)}) not found.` }); + } else { + throw new ContainerError({ description: `No dev container config and no workspace found.` }); + } + } + const config = configs.config; + + await runUserCommand({ ...params, common: { ...common, output: common.postCreate.output } }, config.initializeCommand, common.postCreate.onDidInput); + + let result: ResolverResult; + if (isDockerFileConfig(config) || 'image' in config) { + result = await openDockerfileDevContainer(params, config, configs.workspaceConfig, idLabels); + } else if ('dockerComposeFile' in config) { + if (!workspace) { + throw new ContainerError({ description: `A Dev Container using Docker Compose requires a workspace folder.` }); + } + result = await openDockerComposeDevContainer(params, workspace, config, idLabels); + } else { + throw new ContainerError({ description: `Dev container config (${(config as DevContainerConfig).configFilePath}) is missing one of "image", "dockerFile" or "dockerComposeFile" properties.` }); + } + return result; +} + +export async function readDevContainerConfigFile(cliHost: CLIHost, workspace: Workspace | undefined, configFile: URI, mountWorkspaceGitRoot: boolean, output: Log, consistency?: BindMountConsistency, overrideConfigFile?: URI) { + const documents = createDocuments(cliHost); + const content = await documents.readDocument(overrideConfigFile ?? configFile); + if (!content) { + return undefined; + } + const raw = jsonc.parse(content) as DevContainerConfig | undefined; + const updated = raw && updateFromOldProperties(raw); + if (!updated || typeof updated !== 'object' || Array.isArray(updated)) { + throw new ContainerError({ description: `Dev container config (${uriToFsPath(configFile, cliHost.platform)}) must contain a JSON object literal.` }); + } + const workspaceConfig = await getWorkspaceConfiguration(cliHost, workspace, updated, mountWorkspaceGitRoot, output, consistency); + const config: DevContainerConfig = substitute({ + platform: cliHost.platform, + localWorkspaceFolder: workspace?.rootFolderPath, + containerWorkspaceFolder: workspaceConfig.workspaceFolder, + configFile, + env: cliHost.env, + }, updated); + if (typeof config.workspaceFolder === 'string') { + workspaceConfig.workspaceFolder = config.workspaceFolder; + } + if ('workspaceMount' in config) { + workspaceConfig.workspaceMount = config.workspaceMount; + } + config.configFilePath = configFile; + return { + config, + workspaceConfig, + }; +} diff --git a/src/spec-node/containerFeatures.ts b/src/spec-node/containerFeatures.ts new file mode 100644 index 00000000..e5ad4203 --- /dev/null +++ b/src/spec-node/containerFeatures.ts @@ -0,0 +1,237 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as path from 'path'; +import { StringDecoder } from 'string_decoder'; +import * as tar from 'tar'; + +import { DevContainerConfig } from '../spec-configuration/configuration'; +import { dockerPtyCLI, ImageDetails, toPtyExecParameters } from '../spec-shutdown/dockerUtils'; +import { LogLevel, makeLog, toErrorText } from '../spec-utils/log'; +import { FeaturesConfig, getContainerFeaturesFolder, getContainerFeaturesBaseDockerFile, getFeatureLayers, getFeatureMainValue, getFeatureValueObject, generateFeaturesConfig, getSourceInfoString, collapseFeaturesConfig, Feature, multiStageBuildExploration } from '../spec-configuration/containerFeaturesConfiguration'; +import { readLocalFile } from '../spec-utils/pfs'; +import { includeAllConfiguredFeatures } from '../spec-utils/product'; +import { createFeaturesTempFolder, DockerResolverParameters, getFolderImageName, inspectDockerImage } from './utils'; +import { CLIHost } from '../spec-common/cliHost'; + +export async function extendImage(params: DockerResolverParameters, config: DevContainerConfig, imageName: string, pullImageOnError: boolean, runArgsUser: string | undefined) { + let cache: Promise | undefined; + const imageDetails = () => cache || (cache = inspectDockerImage(params, imageName, pullImageOnError)); + const featuresConfig = await generateFeaturesConfig(params.common, (await createFeaturesTempFolder(params.common)), config, async () => (await imageDetails()).Config.Labels || {}, getContainerFeaturesFolder); + const collapsedFeaturesConfig = collapseFeaturesConfig(featuresConfig); + const updatedImageName0 = await addContainerFeatures(params, featuresConfig, imageName, imageDetails); + const updatedImageName = await updateRemoteUserUID(params, config, updatedImageName0, imageDetails, runArgsUser); + return { updatedImageName, collapsedFeaturesConfig, imageDetails }; +} + +// NOTE: only exported to enable testing. Not meant to be called outside file. +export function generateContainerEnvs(featuresConfig: FeaturesConfig) { + let result = ''; + for (const fSet of featuresConfig.featureSets) { + result += fSet.features + .filter(f => (includeAllConfiguredFeatures || f.included) && f.value) + .reduce((envs, f) => envs.concat(Object.keys(f.containerEnv || {}) + .map(k => `ENV ${k}=${f.containerEnv![k]}`)), [] as string[]) + .join('\n'); + } + return result; +} + +async function addContainerFeatures(params: DockerResolverParameters, featuresConfig: FeaturesConfig | undefined, imageName: string, imageDetails: () => Promise) { + const { common } = params; + const { cliHost, output } = common; + if (!featuresConfig) { + return imageName; + } + + const { dstFolder } = featuresConfig; + + if (!dstFolder || dstFolder === '') { + output.write('dstFolder is undefined or empty in addContainerFeatures', LogLevel.Error); + return imageName; + } + + // Calculate name of the build folder where localcache has been copied to. + const localCacheBuildFolderName = getSourceInfoString({ type : 'local-cache'}); + const imageUser = (await imageDetails()).Config.User || 'root'; + const folderImageName = getFolderImageName(common); + const updatedImageName = `${imageName.startsWith(folderImageName) ? imageName : folderImageName}-features`; + + const srcFolder = getContainerFeaturesFolder(common.extensionPath); + output.write(`local container features stored at: ${srcFolder}`); + await cliHost.mkdirp(`${dstFolder}/${localCacheBuildFolderName}`); + const create = tar.c({ + cwd: srcFolder, + filter: path => (path !== './Dockerfile' && path !== './devcontainer-features.json'), + }, ['.']); + const createExit = new Promise((resolve, reject) => { + create.on('error', reject); + create.on('finish', resolve); + }); + const extract = await cliHost.exec({ + cmd: 'tar', + args: [ + '--no-same-owner', + '-x', + '-f', '-', + ], + cwd: `${dstFolder}/${localCacheBuildFolderName}`, + output, + }); + const stdoutDecoder = new StringDecoder(); + extract.stdout.on('data', (chunk: Buffer) => { + output.write(stdoutDecoder.write(chunk)); + }); + const stderrDecoder = new StringDecoder(); + extract.stderr.on('data', (chunk: Buffer) => { + output.write(toErrorText(stderrDecoder.write(chunk))); + }); + create.pipe(extract.stdin); + await extract.exit; + await createExit; // Allow errors to surface. + + const buildStageScripts = await Promise.all(featuresConfig.featureSets + .map(featureSet => multiStageBuildExploration ? featureSet.features + .filter(f => (includeAllConfiguredFeatures || f.included) && f.value) + .reduce(async (binScripts, feature) => { + const binPath = cliHost.path.join(dstFolder, getSourceInfoString(featureSet.sourceInformation), 'features', feature.id, 'bin'); + const hasAcquire = cliHost.isFile(cliHost.path.join(binPath, 'acquire')); + const hasConfigure = cliHost.isFile(cliHost.path.join(binPath, 'configure')); + const map = await binScripts; + map[feature.id] = { + hasAcquire: await hasAcquire, + hasConfigure: await hasConfigure, + }; + return map; + }, Promise.resolve({}) as Promise>) : Promise.resolve({}))); + + const dockerfile = getContainerFeaturesBaseDockerFile() + .replace('#{featureBuildStages}', getFeatureBuildStages(cliHost, featuresConfig, buildStageScripts)) + .replace('#{featureLayer}', getFeatureLayers(featuresConfig)) + .replace('#{containerEnv}', generateContainerEnvs(featuresConfig)) + .replace('#{copyFeatureBuildStages}', getCopyFeatureBuildStages(featuresConfig, buildStageScripts)) + ; + + await cliHost.writeFile(cliHost.path.join(dstFolder, 'Dockerfile'), Buffer.from(dockerfile)); + + // Build devcontainer-features.env file(s) for each features source folder + await Promise.all([...featuresConfig.featureSets].map(async (featureSet, i) => { + const featuresEnv = ([] as string[]).concat( + ...featureSet.features + .filter(f => (includeAllConfiguredFeatures || f.included) && f.value && !buildStageScripts[i][f.id]?.hasAcquire) + .map(getFeatureEnvVariables) + ).join('\n'); + const envPath = cliHost.path.join(dstFolder, getSourceInfoString(featureSet.sourceInformation), 'devcontainer-features.env'); // next to install.sh + await Promise.all([ + cliHost.writeFile(envPath, Buffer.from(featuresEnv)), + ...featureSet.features + .filter(f => (includeAllConfiguredFeatures || f.included) && f.value && buildStageScripts[i][f.id]?.hasAcquire) + .map(f => { + const featuresEnv = [ + ...getFeatureEnvVariables(f), + `_BUILD_ARG_${getFeatureSafeId(f)}_TARGETPATH=${path.posix.join('/usr/local/devcontainer-features', getSourceInfoString(featureSet.sourceInformation), f.id)}` + ] + .join('\n'); + const envPath = cliHost.path.join(dstFolder, getSourceInfoString(featureSet.sourceInformation), 'features', f.id, 'devcontainer-features.env'); // next to bin/acquire + return cliHost.writeFile(envPath, Buffer.from(featuresEnv)); + }) + ]); + })); + + const args = [ + 'build', + '-t', updatedImageName, + '--build-arg', `BASE_IMAGE=${imageName}`, + '--build-arg', `IMAGE_USER=${imageUser}`, + dstFolder, + ]; + const infoParams = { ...toPtyExecParameters(params), output: makeLog(output, LogLevel.Info) }; + await dockerPtyCLI(infoParams, ...args); + return updatedImageName; +} + +function getFeatureBuildStages(cliHost: CLIHost, featuresConfig: FeaturesConfig, buildStageScripts: Record[]) { + return ([] as string[]).concat(...featuresConfig.featureSets + .map((featureSet, i) => featureSet.features + .filter(f => (includeAllConfiguredFeatures || f.included) && f.value && buildStageScripts[i][f.id]?.hasAcquire) + .map(f => `FROM mcr.microsoft.com/vscode/devcontainers/base:0-focal as ${getSourceInfoString(featureSet.sourceInformation)}_${f.id} +COPY ${cliHost.path.join('.', getSourceInfoString(featureSet.sourceInformation), 'features', f.id)} ${path.posix.join('/tmp/build-features', getSourceInfoString(featureSet.sourceInformation), 'features', f.id)} +COPY ${cliHost.path.join('.', getSourceInfoString(featureSet.sourceInformation), 'common')} ${path.posix.join('/tmp/build-features', getSourceInfoString(featureSet.sourceInformation), 'common')} +RUN cd ${path.posix.join('/tmp/build-features', getSourceInfoString(featureSet.sourceInformation), 'features', f.id)} && set -a && . ./devcontainer-features.env && set +a && ./bin/acquire` + ) + ) + ).join('\n\n'); +} + +function getCopyFeatureBuildStages(featuresConfig: FeaturesConfig, buildStageScripts: Record[]) { + return ([] as string[]).concat(...featuresConfig.featureSets + .map((featureSet, i) => featureSet.features + .filter(f => (includeAllConfiguredFeatures || f.included) && f.value && buildStageScripts[i][f.id]?.hasAcquire) + .map(f => { + const featurePath = path.posix.join('/usr/local/devcontainer-features', getSourceInfoString(featureSet.sourceInformation), f.id); + return `COPY --from=${getSourceInfoString(featureSet.sourceInformation)}_${f.id} ${featurePath} ${featurePath}${buildStageScripts[i][f.id]?.hasConfigure ? ` +RUN cd ${path.posix.join('/tmp/build-features', getSourceInfoString(featureSet.sourceInformation), 'features', f.id)} && set -a && . ./devcontainer-features.env && set +a && ./bin/configure` : ''}`; + }) + ) + ).join('\n\n'); +} + +function getFeatureEnvVariables(f: Feature) { + const values = getFeatureValueObject(f); + const idSafe = getFeatureSafeId(f); + const variables = []; + if (values) { + variables.push(...Object.keys(values) + .map(name => `_BUILD_ARG_${idSafe}_${name.toUpperCase()}="${values[name]}"`)); + variables.push(`_BUILD_ARG_${idSafe}=true`); + } + if (f.buildArg) { + variables.push(`${f.buildArg}=${getFeatureMainValue(f)}`); + } + return variables; +} + +function getFeatureSafeId(f: Feature) { + return f.id + .replace(/[/-]/g, '_') // Slashes and dashes are not allowed in an env. variable key + .toUpperCase(); +} + +async function updateRemoteUserUID(params: DockerResolverParameters, config: DevContainerConfig, imageName: string, imageDetails: () => Promise, runArgsUser: string | undefined) { + const { common } = params; + const { cliHost } = common; + if (params.updateRemoteUserUIDDefault === 'never' || !(typeof config.updateRemoteUserUID === 'boolean' ? config.updateRemoteUserUID : params.updateRemoteUserUIDDefault === 'on') || !(cliHost.platform === 'linux' || params.updateRemoteUserUIDOnMacOS && cliHost.platform === 'darwin')) { + return imageName; + } + const imageUser = (await imageDetails()).Config.User || 'root'; + const remoteUser = config.remoteUser || runArgsUser || imageUser; + if (remoteUser === 'root' || /^\d+$/.test(remoteUser)) { + return imageName; + } + const folderImageName = getFolderImageName(common); + const fixedImageName = `${imageName.startsWith(folderImageName) ? imageName : folderImageName}-uid`; + + const dockerfileName = 'updateUID.Dockerfile'; + const srcDockerfile = path.join(common.extensionPath, 'scripts', dockerfileName); + const version = common.package.version; + const destDockerfile = cliHost.path.join(await cliHost.tmpdir(), 'vsch', `${dockerfileName}-${version}`); + const tmpDockerfile = `${destDockerfile}-${Date.now()}`; + await cliHost.mkdirp(cliHost.path.dirname(tmpDockerfile)); + await cliHost.writeFile(tmpDockerfile, await readLocalFile(srcDockerfile)); + await cliHost.rename(tmpDockerfile, destDockerfile); + const args = [ + 'build', + '-f', destDockerfile, + '-t', fixedImageName, + '--build-arg', `BASE_IMAGE=${imageName}`, + '--build-arg', `REMOTE_USER=${remoteUser}`, + '--build-arg', `NEW_UID=${await cliHost.getuid()}`, + '--build-arg', `NEW_GID=${await cliHost.getgid()}`, + '--build-arg', `IMAGE_USER=${imageUser}`, + cliHost.path.dirname(destDockerfile) + ]; + await dockerPtyCLI(params, ...args); + return fixedImageName; +} diff --git a/src/spec-node/devContainers.ts b/src/spec-node/devContainers.ts new file mode 100644 index 00000000..6f511097 --- /dev/null +++ b/src/spec-node/devContainers.ts @@ -0,0 +1,163 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as path from 'path'; +import * as crypto from 'crypto'; + +import { DockerResolverParameters, getPackageConfig, DevContainerAuthority, UpdateRemoteUserUIDDefault, BindMountConsistency } from './utils'; +import { createNullPostCreate, finishBackgroundTasks, ResolverParameters, UserEnvProbe } from '../spec-common/injectHeadless'; +import { getCLIHost, loadNativeModule } from '../spec-common/commonUtils'; +import { resolve } from './configContainer'; +import { URI } from 'vscode-uri'; +import { promisify } from 'util'; +import { LogLevel, LogDimensions, toErrorText, createCombinedLog, createTerminalLog, Log, makeLog, LogFormat, createJSONLog } from '../spec-utils/log'; +import { dockerComposeCLIConfig } from './dockerCompose'; +import { Mount } from '../spec-configuration/containerFeaturesConfiguration'; +import { PackageConfiguration } from '../spec-utils/product'; + +export interface ProvisionOptions { + dockerPath: string | undefined; + dockerComposePath: string | undefined; + containerDataFolder: string | undefined; + containerSystemDataFolder: string | undefined; + workspaceFolder: string | undefined; + workspaceMountConsistency?: BindMountConsistency; + mountWorkspaceGitRoot: boolean; + idLabels: string[]; + configFile: URI | undefined; + overrideConfigFile: URI | undefined; + logLevel: LogLevel; + logFormat: LogFormat; + log: (text: string) => void; + terminalDimensions: LogDimensions | undefined; + defaultUserEnvProbe: UserEnvProbe; + removeExistingContainer: boolean; + buildNoCache: boolean; + expectExistingContainer: boolean; + postCreateEnabled: boolean; + skipNonBlocking: boolean; + prebuild: boolean; + persistedFolder: string | undefined; + additionalMounts: Mount[]; + updateRemoteUserUIDDefault: UpdateRemoteUserUIDDefault; + remoteEnv: Record; +} + +export async function launch(options: ProvisionOptions, disposables: (() => Promise | undefined)[]) { + const params = await createDockerParams(options, disposables); + const output = params.common.output; + const text = 'Resolving Remote'; + const start = output.start(text); + const result = await resolve(params, options.configFile, options.overrideConfigFile, options.idLabels); + output.stop(text, start); + const { dockerContainerId } = result; + return { + containerId: dockerContainerId!, + remoteUser: result.properties.user, + remoteWorkspaceFolder: result.properties.remoteWorkspaceFolder, + finishBackgroundTasks: async () => { + try { + await finishBackgroundTasks(result.params.backgroundTasks); + } catch (err) { + output.write(toErrorText(String(err && (err.stack || err.message) || err))); + } + }, + }; +} + +export async function createDockerParams(options: ProvisionOptions, disposables: (() => Promise | undefined)[]): Promise { + const { persistedFolder, additionalMounts, updateRemoteUserUIDDefault, containerDataFolder, containerSystemDataFolder, workspaceMountConsistency, mountWorkspaceGitRoot, remoteEnv } = options; + let parsedAuthority: DevContainerAuthority | undefined; + if (options.workspaceFolder) { + parsedAuthority = { hostPath: options.workspaceFolder } as DevContainerAuthority; + } + const extensionPath = path.join(__dirname, '..', '..'); + const sessionStart = new Date(); + const pkg = await getPackageConfig(extensionPath); + const output = createLog(options, pkg, sessionStart, disposables); + + const appRoot = undefined; + const cwd = options.workspaceFolder || process.cwd(); + const cliHost = await getCLIHost(cwd, loadNativeModule); + const sessionId = (await promisify(crypto.randomBytes)(20)).toString('hex'); // TODO: Somehow enable correlation. + + const common: ResolverParameters = { + prebuild: options.prebuild, + computeExtensionHostEnv: false, + package: pkg, + containerDataFolder, + containerSystemDataFolder, + appRoot, + extensionPath, // TODO: rename to packagePath + sessionId, + sessionStart, + cliHost, + env: cliHost.env, + cwd, + isLocalContainer: false, + progress: () => {}, + output, + allowSystemConfigChange: true, + defaultUserEnvProbe: options.defaultUserEnvProbe, + postCreate: createNullPostCreate(options.postCreateEnabled, options.skipNonBlocking, output), + getLogLevel: () => options.logLevel, + onDidChangeLogLevel: () => ({ dispose() { } }), + loadNativeModule, + shutdowns: [], + backgroundTasks: [], + persistedFolder: persistedFolder || await cliHost.tmpdir(), // Fallback to tmpDir(), even though that isn't 'persistent' + remoteEnv, + }; + + const dockerPath = options.dockerPath || 'docker'; + const dockerComposePath = options.dockerComposePath || 'docker-compose'; + return { + common, + parsedAuthority, + dockerCLI: dockerPath, + dockerComposeCLI: dockerComposeCLIConfig({ + exec: cliHost.exec, + env: cliHost.env, + output: common.output, + }, dockerPath, dockerComposePath), + dockerEnv: cliHost.env, + workspaceMountConsistencyDefault: workspaceMountConsistency, + mountWorkspaceGitRoot, + updateRemoteUserUIDOnMacOS: false, + cacheMount: 'bind', + removeOnStartup: options.removeExistingContainer, + buildNoCache: options.buildNoCache, + expectExistingContainer: options.expectExistingContainer, + additionalMounts, + userRepositoryConfigurationPaths: [], + updateRemoteUserUIDDefault, + }; +} + +export interface LogOptions { + logLevel: LogLevel; + logFormat: LogFormat; + log: (text: string) => void; + terminalDimensions: LogDimensions | undefined; +} + +export function createLog(options: LogOptions, pkg: PackageConfiguration, sessionStart: Date, disposables: (() => Promise | undefined)[]) { + const header = `${pkg.name} ${pkg.version}.`; + const output = createLogFrom(options, sessionStart, header); + output.dimensions = options.terminalDimensions; + disposables.push(() => output.join()); + return output; +} + +function createLogFrom({ log: write, logLevel, logFormat }: LogOptions, sessionStart: Date, header: string | undefined = undefined): Log & { join(): Promise } { + const handler = logFormat === 'json' ? createJSONLog(write, () => logLevel, sessionStart) : createTerminalLog(write, () => logLevel, sessionStart); + const log = { + ...makeLog(createCombinedLog([ handler ], header)), + join: async () => { + // TODO: wait for write() to finish. + }, + }; + return log; +} diff --git a/src/spec-node/devContainersSpecCLI.ts b/src/spec-node/devContainersSpecCLI.ts new file mode 100644 index 00000000..f7fabed3 --- /dev/null +++ b/src/spec-node/devContainersSpecCLI.ts @@ -0,0 +1,727 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as path from 'path'; +import yargs, { Argv } from 'yargs'; + +import { createDockerParams, createLog, launch, ProvisionOptions } from './devContainers'; +import { createContainerProperties, createFeaturesTempFolder, getFolderImageName, getPackageConfig, isDockerFileConfig } from './utils'; +import { URI } from 'vscode-uri'; +import { ContainerError } from '../spec-common/errors'; +import { Log, LogLevel, makeLog, mapLogLevel } from '../spec-utils/log'; +import { UnpackPromise } from '../spec-utils/types'; +import { probeRemoteEnv, runPostCreateCommands, runRemoteCommand, UserEnvProbe } from '../spec-common/injectHeadless'; +import { bailOut, buildImage, findDevContainer, findUserArg, hostFolderLabel } from './singleContainer'; +import { extendImage } from './containerFeatures'; +import { DockerCLIParameters, dockerPtyCLI, inspectContainer } from '../spec-shutdown/dockerUtils'; +import { buildDockerCompose, getProjectName, readDockerComposeConfig } from './dockerCompose'; +import { getDockerComposeFilePaths } from '../spec-configuration/configuration'; +import { workspaceFromPath } from '../spec-utils/workspaces'; +import { readDevContainerConfigFile } from './configContainer'; +import { getDefaultDevContainerConfigPath, getDevContainerConfigPathIn, uriToFsPath } from '../spec-configuration/configurationCommonUtils'; +import { getCLIHost } from '../spec-common/cliHost'; +import { loadNativeModule } from '../spec-common/commonUtils'; +import { generateFeaturesConfig, getContainerFeaturesFolder } from '../spec-configuration/containerFeaturesConfiguration'; + +const defaultDefaultUserEnvProbe: UserEnvProbe = 'loginInteractiveShell'; + +(async () => { + + const argv = process.argv.slice(2); + const restArgs = argv[0] === 'exec' && argv[1] !== '--help'; // halt-at-non-option doesn't work in subcommands: https://github.com/yargs/yargs/issues/1417 + const y = yargs([]) + .parserConfiguration({ + // By default, yargs allows `--no-myoption` to set a boolean `--myoption` to false + // Disable this to allow `--no-cache` on the `build` command to align with `docker build` syntax + 'boolean-negation': false, + 'halt-at-non-option': restArgs, + }) + .version((await getPackageConfig(path.join(__dirname, '..', '..'))).version) + .demandCommand() + .strict(); + y.wrap(Math.min(120, y.terminalWidth())); + y.command('up', 'Create and run dev container', provisionOptions, provisionHandler); + y.command('build [path]', 'Build a dev container image', buildOptions, buildHandler); + y.command('run-user-commands', 'Run user commands', runUserCommandsOptions, runUserCommandsHandler); + y.command('read-configuration', 'Read configuration', readConfigurationOptions, readConfigurationHandler); + y.command(restArgs ? ['exec', '*'] : ['exec [args..]'], 'Execute a command on a running dev container', execOptions, execHandler); + y.parse(restArgs ? argv.slice(1) : argv); + +})().catch(console.error); + +type UnpackArgv = T extends Argv ? U : T; + +const mountRegex = /^type=(bind|volume),source=([^,]+),target=([^,]+)(?:,external=(true|false))?$/; + +function provisionOptions(y: Argv) { + return y.options({ + 'docker-path': { type: 'string', description: 'Docker CLI path.' }, + 'docker-compose-path': { type: 'string', description: 'Docker Compose CLI path.' }, + 'container-data-folder': { type: 'string', description: 'Container data folder where user data inside the container will be stored.' }, + 'container-system-data-folder': { type: 'string', description: 'Container system data folder where system data inside the container will be stored.' }, + 'workspace-folder': { type: 'string', description: 'Workspace folder path. The devcontainer.json will be looked up relative to this path.' }, + 'workspace-mount-consistency': { choices: ['consistent' as 'consistent', 'cached' as 'cached', 'delegated' as 'delegated'], default: 'cached' as 'cached', description: 'Workspace mount consistency.' }, + 'mount-workspace-git-root': { type: 'boolean', default: true, description: 'Mount the workspace using its Git root.' }, + 'id-label': { type: 'string', description: 'Id label(s) of the format name=value. These will be set on the container and used to query for an existing container. If no --id-label is given, one will be inferred from the --workspace-folder path.' }, + 'config': { type: 'string', description: 'devcontainer.json path. The default is to use .devcontainer/devcontainer.json or, if that does not exist, .devcontainer.json in the workspace folder.' }, + 'override-config': { type: 'string', description: 'devcontainer.json path to override any devcontainer.json in the workspace folder (or built-in configuration). This is required when there is no devcontainer.json otherwise.' }, + 'log-level': { choices: ['info' as 'info', 'debug' as 'debug', 'trace' as 'trace'], default: 'info' as 'info', description: 'Log level for the --terminal-log-file. When set to trace, the log level for --log-file will also be set to trace.' }, + 'log-format': { choices: ['text' as 'text', 'json' as 'json'], default: 'text' as 'text', description: 'Log format.' }, + 'terminal-columns': { type: 'number', implies: ['terminal-rows'], description: 'Number of rows to render the output for. This is required for some of the subprocesses to correctly render their output.' }, + 'terminal-rows': { type: 'number', implies: ['terminal-columns'], description: 'Number of columns to render the output for. This is required for some of the subprocesses to correctly render their output.' }, + 'default-user-env-probe': { choices: ['none' as 'none', 'loginInteractiveShell' as 'loginInteractiveShell', 'interactiveShell' as 'interactiveShell', 'loginShell' as 'loginShell'], default: defaultDefaultUserEnvProbe, description: 'Default value for the devcontainer.json\'s "userEnvProbe".' }, + 'update-remote-user-uid-default': { choices: ['never' as 'never', 'on' as 'on', 'off' as 'off'], default: 'on' as 'on', description: 'Default for updating the remote user\'s UID and GID to the local user\'s one.' }, + 'remove-existing-container': { type: 'boolean', default: false, description: 'Removes the dev container if it already exists.' }, + 'build-no-cache': { type: 'boolean', default: false, description: 'Builds the image with `--no-cache` if the container does not exist.' }, + 'expect-existing-container': { type: 'boolean', default: false, description: 'Fail if the container does not exist.' }, + 'skip-post-create': { type: 'boolean', default: false, description: 'Do not run onCreateCommand, updateContentCommand, postCreateCommand, postStartCommand or postAttachCommand and do not install dotfiles.' }, + 'skip-non-blocking-commands': { type: 'boolean', default: false, description: 'Stop running user commands after running the command configured with waitFor or the updateContentCommand by default.' }, + prebuild: { type: 'boolean', default: false, description: 'Stop after onCreateCommand and updateContentCommand, rerunning updateContentCommand if it has run before.' }, + 'user-data-folder': { type: 'string', description: 'Host path to a directory that is intended to be persisted and share state between sessions.' }, + 'mount': { type: 'string', description: 'Additional mount point(s). Format: type=,source=,target=[,external=]' }, + 'remote-env': { type: 'string', description: 'Remote environment variables of the format name=value. These will be added when executing the user commands.' }, + }) + .check(argv => { + const idLabels = (argv['id-label'] && (Array.isArray(argv['id-label']) ? argv['id-label'] : [argv['id-label']])) as string[] | undefined; + if (idLabels?.some(idLabel => !/.+=.+/.test(idLabel))) { + throw new Error('Unmatched argument format: id-label must match ='); + } + if (!(argv['workspace-folder'] || argv['id-label'])) { + throw new Error('Missing required argument: workspace-folder or id-label'); + } + if (!(argv['workspace-folder'] || argv['override-config'])) { + throw new Error('Missing required argument: workspace-folder or override-config'); + } + const mounts = (argv.mount && (Array.isArray(argv.mount) ? argv.mount : [argv.mount])) as string[] | undefined; + if (mounts?.some(mount => !mountRegex.test(mount))) { + throw new Error('Unmatched argument format: mount must match type=,source=,target=[,external=]'); + } + const remoteEnvs = (argv['remote-env'] && (Array.isArray(argv['remote-env']) ? argv['remote-env'] : [argv['remote-env']])) as string[] | undefined; + if (remoteEnvs?.some(remoteEnv => !/.+=.+/.test(remoteEnv))) { + throw new Error('Unmatched argument format: remote-env must match ='); + } + return true; + }); +} + +type ProvisionArgs = UnpackArgv>; + +function provisionHandler(args: ProvisionArgs) { + (async () => provision(args))().catch(console.error); +} + +async function provision({ + 'user-data-folder': persistedFolder, + 'docker-path': dockerPath, + 'docker-compose-path': dockerComposePath, + 'container-data-folder': containerDataFolder, + 'container-system-data-folder': containerSystemDataFolder, + 'workspace-folder': workspaceFolderArg, + 'workspace-mount-consistency': workspaceMountConsistency, + 'mount-workspace-git-root': mountWorkspaceGitRoot, + 'id-label': idLabel, + config, + 'override-config': overrideConfig, + 'log-level': logLevel, + 'log-format': logFormat, + 'terminal-rows': terminalRows, + 'terminal-columns': terminalColumns, + 'default-user-env-probe': defaultUserEnvProbe, + 'update-remote-user-uid-default': updateRemoteUserUIDDefault, + 'remove-existing-container': removeExistingContainer, + 'build-no-cache': buildNoCache, + 'expect-existing-container': expectExistingContainer, + 'skip-post-create': skipPostCreate, + 'skip-non-blocking-commands': skipNonBlocking, + prebuild, + mount, + 'remote-env': addRemoteEnv, +}: ProvisionArgs) { + + const workspaceFolder = workspaceFolderArg ? path.resolve(process.cwd(), workspaceFolderArg) : undefined; + const addRemoteEnvs = addRemoteEnv ? (Array.isArray(addRemoteEnv) ? addRemoteEnv as string[] : [addRemoteEnv]) : []; + const options: ProvisionOptions = { + dockerPath, + dockerComposePath, + containerDataFolder, + containerSystemDataFolder, + workspaceFolder, + workspaceMountConsistency, + mountWorkspaceGitRoot, + idLabels: idLabel ? (Array.isArray(idLabel) ? idLabel as string[] : [idLabel]) : getDefaultIdLabels(workspaceFolder!), + configFile: config ? URI.file(path.resolve(process.cwd(), config)) : undefined, + overrideConfigFile: overrideConfig ? URI.file(path.resolve(process.cwd(), overrideConfig)) : undefined, + logLevel: mapLogLevel(logLevel), + logFormat, + log: text => process.stderr.write(text), + terminalDimensions: terminalColumns && terminalRows ? { columns: terminalColumns, rows: terminalRows } : undefined, + defaultUserEnvProbe, + removeExistingContainer, + buildNoCache, + expectExistingContainer, + postCreateEnabled: !skipPostCreate, + skipNonBlocking, + prebuild, + persistedFolder, + additionalMounts: mount ? (Array.isArray(mount) ? mount : [mount]).map(mount => { + const [, type, source, target, external] = mountRegex.exec(mount)!; + return { + type: type as 'bind' | 'volume', + source, + target, + external: external === 'true' + }; + }) : [], + updateRemoteUserUIDDefault, + remoteEnv: keyValuesToRecord(addRemoteEnvs), + }; + + const result = await doProvision(options); + const exitCode = result.outcome === 'error' ? 1 : 0; + console.log(JSON.stringify(result)); + if (result.outcome === 'success') { + await result.finishBackgroundTasks(); + } + await result.dispose(); + process.exit(exitCode); +} + +async function doProvision(options: ProvisionOptions) { + const disposables: (() => Promise | undefined)[] = []; + const dispose = async () => { + await Promise.all(disposables.map(d => d())); + }; + try { + const result = await launch(options, disposables); + return { + outcome: 'success' as 'success', + dispose, + ...result, + }; + } catch (originalError) { + const originalStack = originalError?.stack; + const err = originalError instanceof ContainerError ? originalError : new ContainerError({ + description: 'An error occurred setting up the container.', + originalError + }); + if (originalStack) { + console.error(originalStack); + } + return { + outcome: 'error' as 'error', + message: err.message, + description: err.description, + containerId: err.containerId, + dispose, + }; + } +} + +export type Result = UnpackPromise> & { backgroundProcessPID?: number }; + +function buildOptions(y: Argv) { + return y.options({ + 'user-data-folder': { type: 'string', description: 'Host path to a directory that is intended to be persisted and share state between sessions.' }, + 'docker-path': { type: 'string', description: 'Docker CLI path.' }, + 'docker-compose-path': { type: 'string', description: 'Docker Compose CLI path.' }, + 'workspace-folder': { type: 'string', required: true, description: 'Workspace folder path. The devcontainer.json will be looked up relative to this path.' }, + 'log-level': { choices: ['info' as 'info', 'debug' as 'debug', 'trace' as 'trace'], default: 'info' as 'info', description: 'Log level.' }, + 'log-format': { choices: ['text' as 'text', 'json' as 'json'], default: 'text' as 'text', description: 'Log format.' }, + 'no-cache': { type: 'boolean', default: false, description: 'Builds the image with `--no-cache`.' }, + 'image-name': { type: 'string', description: 'Image name.' }, + }); +} + +type BuildArgs = UnpackArgv>; + +function buildHandler(args: BuildArgs) { + (async () => build(args))().catch(console.error); +} + +async function build({ + 'user-data-folder': persistedFolder, + 'docker-path': dockerPath, + 'docker-compose-path': dockerComposePath, + 'workspace-folder': workspaceFolderArg, + 'log-level': logLevel, + 'log-format': logFormat, + 'no-cache': buildNoCache, + 'image-name': argImageName, +}: BuildArgs) { + const disposables: (() => Promise | undefined)[] = []; + const dispose = async () => { + await Promise.all(disposables.map(d => d())); + }; + try { + const workspaceFolder = path.resolve(process.cwd(), workspaceFolderArg); + const configFile: URI | undefined = /* config ? URI.file(path.resolve(process.cwd(), config)) : */ undefined; // TODO + const overrideConfigFile: URI | undefined = /* overrideConfig ? URI.file(path.resolve(process.cwd(), overrideConfig)) : */ undefined; + const params = await createDockerParams({ + dockerPath, + dockerComposePath, + containerDataFolder: undefined, + containerSystemDataFolder: undefined, + workspaceFolder, + mountWorkspaceGitRoot: false, + idLabels: getDefaultIdLabels(workspaceFolder), + configFile, + overrideConfigFile, + logLevel: mapLogLevel(logLevel), + logFormat, + log: text => process.stderr.write(text), + terminalDimensions: /* terminalColumns && terminalRows ? { columns: terminalColumns, rows: terminalRows } : */ undefined, // TODO + defaultUserEnvProbe: 'loginInteractiveShell', + removeExistingContainer: false, + buildNoCache, + expectExistingContainer: false, + postCreateEnabled: false, + skipNonBlocking: false, + prebuild: false, + persistedFolder, + additionalMounts: [], + updateRemoteUserUIDDefault: 'never', + remoteEnv: {}, + }, disposables); + + const { common, dockerCLI, dockerComposeCLI } = params; + const { cliHost, env, output } = common; + const workspace = workspaceFromPath(cliHost.path, workspaceFolder); + const configPath = configFile ? configFile : workspace + ? (await getDevContainerConfigPathIn(cliHost, workspace.configFolderPath) + || (overrideConfigFile ? getDefaultDevContainerConfigPath(cliHost, workspace.configFolderPath) : undefined)) + : overrideConfigFile; + const configs = configPath && await readDevContainerConfigFile(cliHost, workspace, configPath, params.mountWorkspaceGitRoot, output, undefined, overrideConfigFile) || undefined; + if (!configs) { + throw new ContainerError({ description: `Dev container config (${uriToFsPath(configFile || getDefaultDevContainerConfigPath(cliHost, workspace!.configFolderPath), cliHost.platform)}) not found.` }); + } + const { config } = configs; + + if (isDockerFileConfig(config)) { + + // Build the base image + const baseImageName = getFolderImageName(params.common); + await buildImage(params, config, baseImageName, params.buildNoCache || false); + + // Extend image with features, etc.. + const { updatedImageName } = await extendImage(params, config, baseImageName, 'image' in config, findUserArg(config.runArgs) || config.containerUser); + + if (argImageName) { + await dockerPtyCLI(params, 'tag', updatedImageName, argImageName); + } + } else if ('dockerComposeFile' in config) { + + const cwdEnvFile = cliHost.path.join(cliHost.cwd, '.env'); + const envFile = Array.isArray(config.dockerComposeFile) && config.dockerComposeFile.length === 0 && await cliHost.isFile(cwdEnvFile) ? cwdEnvFile : undefined; + const composeFiles = await getDockerComposeFilePaths(cliHost, config, cliHost.env, workspaceFolder); + + // If dockerComposeFile is an array, add -f in order. https://docs.docker.com/compose/extends/#multiple-compose-files + const composeGlobalArgs = ([] as string[]).concat(...composeFiles.map(composeFile => ['-f', composeFile])); + if (envFile) { + composeGlobalArgs.push('--env-file', envFile); + } + const projectName = await getProjectName(params, workspace, composeFiles); + + const buildParams: DockerCLIParameters = { cliHost, dockerCLI, dockerComposeCLI, env, output }; + + const composeConfig = await readDockerComposeConfig(buildParams, composeFiles, envFile); + const services = Object.keys(composeConfig.services || {}); + if (services.indexOf(config.service) === -1) { + throw new Error(`Service '${config.service}' configured in devcontainer.json not found in Docker Compose configuration.`); + } + + await buildDockerCompose(config, projectName, buildParams, composeFiles, composeGlobalArgs, [config.service], params.buildNoCache || false, undefined); + + const service = composeConfig.services[config.service]; + const originalImageName = service.image || `${projectName}_${config.service}`; + const { updatedImageName } = await extendImage(params, config, originalImageName, !service.build, service.user); + + if (argImageName) { + await dockerPtyCLI(params, 'tag', updatedImageName, argImageName); + } + } else { + + await dockerPtyCLI(params, 'pull', config.image); + const { updatedImageName } = await extendImage(params, config, config.image, 'image' in config, findUserArg(config.runArgs) || config.containerUser); + + if (argImageName) { + await dockerPtyCLI(params, 'tag', updatedImageName, argImageName); + } + } + } finally { + await dispose(); + } +} + +function runUserCommandsOptions(y: Argv) { + return y.options({ + 'user-data-folder': { type: 'string', description: 'Host path to a directory that is intended to be persisted and share state between sessions.' }, + 'docker-path': { type: 'string', description: 'Docker CLI path.' }, + 'docker-compose-path': { type: 'string', description: 'Docker Compose CLI path.' }, + 'container-data-folder': { type: 'string', description: 'Container data folder where user data inside the container will be stored.' }, + 'container-system-data-folder': { type: 'string', description: 'Container system data folder where system data inside the container will be stored.' }, + 'workspace-folder': { type: 'string', required: true, description: 'Workspace folder path. The devcontainer.json will be looked up relative to this path.' }, + 'mount-workspace-git-root': { type: 'boolean', default: true, description: 'Mount the workspace using its Git root.' }, + 'container-id': { type: 'string', description: 'Id of the container to run the user commands for.' }, + 'id-label': { type: 'string', description: 'Id label(s) of the format name=value. If no --container-id is given the id labels will be used to look up the container. If no --id-label is given, one will be inferred from the --workspace-folder path.' }, + 'config': { type: 'string', description: 'devcontainer.json path. The default is to use .devcontainer/devcontainer.json or, if that does not exist, .devcontainer.json in the workspace folder.' }, + 'override-config': { type: 'string', description: 'devcontainer.json path to override any devcontainer.json in the workspace folder (or built-in configuration). This is required when there is no devcontainer.json otherwise.' }, + 'log-level': { choices: ['info' as 'info', 'debug' as 'debug', 'trace' as 'trace'], default: 'info' as 'info', description: 'Log level for the --terminal-log-file. When set to trace, the log level for --log-file will also be set to trace.' }, + 'log-format': { choices: ['text' as 'text', 'json' as 'json'], default: 'text' as 'text', description: 'Log format.' }, + 'terminal-columns': { type: 'number', implies: ['terminal-rows'], description: 'Number of rows to render the output for. This is required for some of the subprocesses to correctly render their output.' }, + 'terminal-rows': { type: 'number', implies: ['terminal-columns'], description: 'Number of columns to render the output for. This is required for some of the subprocesses to correctly render their output.' }, + 'default-user-env-probe': { choices: ['none' as 'none', 'loginInteractiveShell' as 'loginInteractiveShell', 'interactiveShell' as 'interactiveShell', 'loginShell' as 'loginShell'], default: defaultDefaultUserEnvProbe, description: 'Default value for the devcontainer.json\'s "userEnvProbe".' }, + 'skip-non-blocking-commands': { type: 'boolean', default: false, description: 'Stop running user commands after running the command configured with waitFor or the updateContentCommand by default.' }, + prebuild: { type: 'boolean', default: false, description: 'Stop after onCreateCommand and updateContentCommand, rerunning updateContentCommand if it has run before.' }, + 'stop-for-personalization': { type: 'boolean', default: false, description: 'Stop for personalization.' }, + 'remote-env': { type: 'string', description: 'Remote environment variables of the format name=value. These will be added when executing the user commands.' }, + }) + .check(argv => { + const idLabels = (argv['id-label'] && (Array.isArray(argv['id-label']) ? argv['id-label'] : [argv['id-label']])) as string[] | undefined; + if (idLabels?.some(idLabel => !/.+=.+/.test(idLabel))) { + throw new Error('Unmatched argument format: id-label must match ='); + } + const remoteEnvs = (argv['remote-env'] && (Array.isArray(argv['remote-env']) ? argv['remote-env'] : [argv['remote-env']])) as string[] | undefined; + if (remoteEnvs?.some(remoteEnv => !/.+=.+/.test(remoteEnv))) { + throw new Error('Unmatched argument format: remote-env must match ='); + } + return true; + }); +} + +type RunUserCommandsArgs = UnpackArgv>; + +function runUserCommandsHandler(args: RunUserCommandsArgs) { + (async () => runUserCommands(args))().catch(console.error); +} + +async function runUserCommands({ + 'user-data-folder': persistedFolder, + 'docker-path': dockerPath, + 'docker-compose-path': dockerComposePath, + 'container-data-folder': containerDataFolder, + 'container-system-data-folder': containerSystemDataFolder, + 'workspace-folder': workspaceFolderArg, + 'mount-workspace-git-root': mountWorkspaceGitRoot, + 'container-id': containerId, + 'id-label': idLabel, + config: configParam, + 'override-config': overrideConfig, + 'log-level': logLevel, + 'log-format': logFormat, + 'terminal-rows': terminalRows, + 'terminal-columns': terminalColumns, + 'default-user-env-probe': defaultUserEnvProbe, + 'skip-non-blocking-commands': skipNonBlocking, + prebuild, + 'stop-for-personalization': stopForPersonalization, + 'remote-env': addRemoteEnv, +}: RunUserCommandsArgs) { + const disposables: (() => Promise | undefined)[] = []; + const dispose = async () => { + await Promise.all(disposables.map(d => d())); + }; + try { + const workspaceFolder = path.resolve(process.cwd(), workspaceFolderArg); + const idLabels = idLabel ? (Array.isArray(idLabel) ? idLabel as string[] : [idLabel]) : getDefaultIdLabels(workspaceFolder); + const addRemoteEnvs = addRemoteEnv ? (Array.isArray(addRemoteEnv) ? addRemoteEnv as string[] : [addRemoteEnv]) : []; + const configFile = configParam ? URI.file(path.resolve(process.cwd(), configParam)) : undefined; + const overrideConfigFile = overrideConfig ? URI.file(path.resolve(process.cwd(), overrideConfig)) : undefined; + const params = await createDockerParams({ + dockerPath, + dockerComposePath, + containerDataFolder, + containerSystemDataFolder, + workspaceFolder, + mountWorkspaceGitRoot, + idLabels, + configFile, + overrideConfigFile, + logLevel: mapLogLevel(logLevel), + logFormat, + log: text => process.stderr.write(text), + terminalDimensions: terminalColumns && terminalRows ? { columns: terminalColumns, rows: terminalRows } : undefined, + defaultUserEnvProbe, + removeExistingContainer: false, + buildNoCache: false, + expectExistingContainer: false, + postCreateEnabled: true, + skipNonBlocking, + prebuild, + persistedFolder, + additionalMounts: [], + updateRemoteUserUIDDefault: 'never', + remoteEnv: keyValuesToRecord(addRemoteEnvs), + }, disposables); + + const { common } = params; + const { cliHost, output } = common; + const workspace = workspaceFromPath(cliHost.path, workspaceFolder); + const configPath = configFile ? configFile : workspace + ? (await getDevContainerConfigPathIn(cliHost, workspace.configFolderPath) + || (overrideConfigFile ? getDefaultDevContainerConfigPath(cliHost, workspace.configFolderPath) : undefined)) + : overrideConfigFile; + const configs = configPath && await readDevContainerConfigFile(cliHost, workspace, configPath, params.mountWorkspaceGitRoot, output, undefined, overrideConfigFile) || undefined; + if (!configs) { + throw new ContainerError({ description: `Dev container config (${uriToFsPath(configFile || getDefaultDevContainerConfigPath(cliHost, workspace!.configFolderPath), cliHost.platform)}) not found.` }); + } + const { config, workspaceConfig } = configs; + + const container = containerId ? await inspectContainer(params, containerId) : await findDevContainer(params, idLabels); + if (!container) { + bailOut(common.output, 'Dev container not found.'); + } + const containerProperties = await createContainerProperties(params, container.Id, workspaceConfig.workspaceFolder, config.remoteUser); + const remoteEnv = probeRemoteEnv(common, containerProperties, config); + const result = await runPostCreateCommands(common, containerProperties, config, remoteEnv, stopForPersonalization); + console.log(JSON.stringify({ + outcome: 'success' as 'success', + result, + })); + } catch (err) { + console.error(err); + await dispose(); + process.exit(1); + } + await dispose(); + process.exit(0); +} + + +function readConfigurationOptions(y: Argv) { + return y.options({ + 'user-data-folder': { type: 'string', description: 'Host path to a directory that is intended to be persisted and share state between sessions.' }, + 'workspace-folder': { type: 'string', required: true, description: 'Workspace folder path. The devcontainer.json will be looked up relative to this path.' }, + 'mount-workspace-git-root': { type: 'boolean', default: true, description: 'Mount the workspace using its Git root.' }, + 'config': { type: 'string', description: 'devcontainer.json path. The default is to use .devcontainer/devcontainer.json or, if that does not exist, .devcontainer.json in the workspace folder.' }, + 'override-config': { type: 'string', description: 'devcontainer.json path to override any devcontainer.json in the workspace folder (or built-in configuration). This is required when there is no devcontainer.json otherwise.' }, + 'log-level': { choices: ['info' as 'info', 'debug' as 'debug', 'trace' as 'trace'], default: 'info' as 'info', description: 'Log level for the --terminal-log-file. When set to trace, the log level for --log-file will also be set to trace.' }, + 'log-format': { choices: ['text' as 'text', 'json' as 'json'], default: 'text' as 'text', description: 'Log format.' }, + 'terminal-columns': { type: 'number', implies: ['terminal-rows'], description: 'Number of rows to render the output for. This is required for some of the subprocesses to correctly render their output.' }, + 'terminal-rows': { type: 'number', implies: ['terminal-columns'], description: 'Number of columns to render the output for. This is required for some of the subprocesses to correctly render their output.' }, + 'include-features-configuration': { type: 'boolean', default: false, description: 'Include features configuration.' }, + }); +} + +type ReadConfigurationArgs = UnpackArgv>; + +function readConfigurationHandler(args: ReadConfigurationArgs) { + (async () => readConfiguration(args))().catch(console.error); +} + +async function readConfiguration({ + // 'user-data-folder': persistedFolder, + 'workspace-folder': workspaceFolderArg, + 'mount-workspace-git-root': mountWorkspaceGitRoot, + config: configParam, + 'override-config': overrideConfig, + 'log-level': logLevel, + 'log-format': logFormat, + 'terminal-rows': terminalRows, + 'terminal-columns': terminalColumns, + 'include-features-configuration': includeFeaturesConfig, +}: ReadConfigurationArgs) { + const disposables: (() => Promise | undefined)[] = []; + const dispose = async () => { + await Promise.all(disposables.map(d => d())); + }; + let output: Log | undefined; + try { + const workspaceFolder = path.resolve(process.cwd(), workspaceFolderArg); + const configFile = configParam ? URI.file(path.resolve(process.cwd(), configParam)) : undefined; + const overrideConfigFile = overrideConfig ? URI.file(path.resolve(process.cwd(), overrideConfig)) : undefined; + const cwd = workspaceFolder || process.cwd(); + const cliHost = await getCLIHost(cwd, loadNativeModule); + const extensionPath = path.join(__dirname, '..', '..'); + const sessionStart = new Date(); + const pkg = await getPackageConfig(extensionPath); + output = createLog({ + logLevel: mapLogLevel(logLevel), + logFormat, + log: text => process.stderr.write(text), + terminalDimensions: terminalColumns && terminalRows ? { columns: terminalColumns, rows: terminalRows } : undefined, + }, pkg, sessionStart, disposables); + + const workspace = workspaceFromPath(cliHost.path, workspaceFolder); + const configPath = configFile ? configFile : workspace + ? (await getDevContainerConfigPathIn(cliHost, workspace.configFolderPath) + || (overrideConfigFile ? getDefaultDevContainerConfigPath(cliHost, workspace.configFolderPath) : undefined)) + : overrideConfigFile; + const configs = configPath && await readDevContainerConfigFile(cliHost, workspace, configPath, mountWorkspaceGitRoot, output, undefined, overrideConfigFile) || undefined; + if (!configs) { + throw new ContainerError({ description: `Dev container config (${uriToFsPath(configFile || getDefaultDevContainerConfigPath(cliHost, workspace!.configFolderPath), cliHost.platform)}) not found.` }); + } + const featuresConfiguration = includeFeaturesConfig ? await generateFeaturesConfig({ extensionPath, output, env: cliHost.env }, (await createFeaturesTempFolder({ cliHost, package: pkg })), configs.config, async () => /* TODO: ? (await imageDetails()).Config.Labels || */ ({}), getContainerFeaturesFolder) : undefined; + await new Promise((resolve, reject) => { + process.stdout.write(JSON.stringify({ + configuration: configs.config, + workspace: configs.workspaceConfig, + featuresConfiguration, + }) + '\n', err => err ? reject(err) : resolve()); + }); + } catch (err) { + if (output) { + output.write(err && (err.stack || err.message) || String(err)); + } else { + console.error(err); + } + await dispose(); + process.exit(1); + } + await dispose(); + process.exit(0); +} + +function execOptions(y: Argv) { + return y.options({ + 'user-data-folder': { type: 'string', description: 'Host path to a directory that is intended to be persisted and share state between sessions.' }, + 'docker-path': { type: 'string', description: 'Docker CLI path.' }, + 'docker-compose-path': { type: 'string', description: 'Docker Compose CLI path.' }, + 'container-data-folder': { type: 'string', description: 'Container data folder where user data inside the container will be stored.' }, + 'container-system-data-folder': { type: 'string', description: 'Container system data folder where system data inside the container will be stored.' }, + 'workspace-folder': { type: 'string', required: true, description: 'Workspace folder path. The devcontainer.json will be looked up relative to this path.' }, + 'mount-workspace-git-root': { type: 'boolean', default: true, description: 'Mount the workspace using its Git root.' }, + 'container-id': { type: 'string', description: 'Id of the container to run the user commands for.' }, + 'id-label': { type: 'string', description: 'Id label(s) of the format name=value. If no --container-id is given the id labels will be used to look up the container. If no --id-label is given, one will be inferred from the --workspace-folder path.' }, + 'config': { type: 'string', description: 'devcontainer.json path. The default is to use .devcontainer/devcontainer.json or, if that does not exist, .devcontainer.json in the workspace folder.' }, + 'override-config': { type: 'string', description: 'devcontainer.json path to override any devcontainer.json in the workspace folder (or built-in configuration). This is required when there is no devcontainer.json otherwise.' }, + 'log-level': { choices: ['info' as 'info', 'debug' as 'debug', 'trace' as 'trace'], default: 'info' as 'info', description: 'Log level for the --terminal-log-file. When set to trace, the log level for --log-file will also be set to trace.' }, + 'log-format': { choices: ['text' as 'text', 'json' as 'json'], default: 'text' as 'text', description: 'Log format.' }, + 'terminal-columns': { type: 'number', implies: ['terminal-rows'], description: 'Number of rows to render the output for. This is required for some of the subprocesses to correctly render their output.' }, + 'terminal-rows': { type: 'number', implies: ['terminal-columns'], description: 'Number of columns to render the output for. This is required for some of the subprocesses to correctly render their output.' }, + 'default-user-env-probe': { choices: ['none' as 'none', 'loginInteractiveShell' as 'loginInteractiveShell', 'interactiveShell' as 'interactiveShell', 'loginShell' as 'loginShell'], default: defaultDefaultUserEnvProbe, description: 'Default value for the devcontainer.json\'s "userEnvProbe".' }, + 'remote-env': { type: 'string', description: 'Remote environment variables of the format name=value. These will be added when executing the user commands.' }, + }) + .positional('cmd', { + type: 'string', + description: 'Command to execute.', + demandOption: true, + }).positional('args', { + type: 'string', + array: true, + description: 'Arguments to the command.', + demandOption: true, + }) + .check(argv => { + const idLabels = (argv['id-label'] && (Array.isArray(argv['id-label']) ? argv['id-label'] : [argv['id-label']])) as string[] | undefined; + if (idLabels?.some(idLabel => !/.+=.+/.test(idLabel))) { + throw new Error('Unmatched argument format: id-label must match ='); + } + const remoteEnvs = (argv['remote-env'] && (Array.isArray(argv['remote-env']) ? argv['remote-env'] : [argv['remote-env']])) as string[] | undefined; + if (remoteEnvs?.some(remoteEnv => !/.+=.+/.test(remoteEnv))) { + throw new Error('Unmatched argument format: remote-env must match ='); + } + return true; + }); +} + +type ExecArgs = UnpackArgv>; + +function execHandler(args: ExecArgs) { + (async () => exec(args))().catch(console.error); +} + +async function exec({ + 'user-data-folder': persistedFolder, + 'docker-path': dockerPath, + 'docker-compose-path': dockerComposePath, + 'container-data-folder': containerDataFolder, + 'container-system-data-folder': containerSystemDataFolder, + 'workspace-folder': workspaceFolderArg, + 'mount-workspace-git-root': mountWorkspaceGitRoot, + 'container-id': containerId, + 'id-label': idLabel, + config: configParam, + 'override-config': overrideConfig, + 'log-level': logLevel, + 'log-format': logFormat, + 'terminal-rows': terminalRows, + 'terminal-columns': terminalColumns, + 'default-user-env-probe': defaultUserEnvProbe, + 'remote-env': addRemoteEnv, + _: restArgs, +}: ExecArgs & { _?: string[] }) { + const disposables: (() => Promise | undefined)[] = []; + const dispose = async () => { + await Promise.all(disposables.map(d => d())); + }; + try { + const workspaceFolder = path.resolve(process.cwd(), workspaceFolderArg); + const idLabels = idLabel ? (Array.isArray(idLabel) ? idLabel as string[] : [idLabel]) : getDefaultIdLabels(workspaceFolder); + const addRemoteEnvs = addRemoteEnv ? (Array.isArray(addRemoteEnv) ? addRemoteEnv as string[] : [addRemoteEnv]) : []; + const configFile = configParam ? URI.file(path.resolve(process.cwd(), configParam)) : undefined; + const overrideConfigFile = overrideConfig ? URI.file(path.resolve(process.cwd(), overrideConfig)) : undefined; + const params = await createDockerParams({ + dockerPath, + dockerComposePath, + containerDataFolder, + containerSystemDataFolder, + workspaceFolder, + mountWorkspaceGitRoot, + idLabels, + configFile, + overrideConfigFile, + logLevel: mapLogLevel(logLevel), + logFormat, + log: text => process.stderr.write(text), + terminalDimensions: terminalColumns && terminalRows ? { columns: terminalColumns, rows: terminalRows } : undefined, + defaultUserEnvProbe, + removeExistingContainer: false, + buildNoCache: false, + expectExistingContainer: false, + postCreateEnabled: true, + skipNonBlocking: false, + prebuild: false, + persistedFolder, + additionalMounts: [], + updateRemoteUserUIDDefault: 'never', + remoteEnv: keyValuesToRecord(addRemoteEnvs), + }, disposables); + + const { common } = params; + const { cliHost, output } = common; + const workspace = workspaceFromPath(cliHost.path, workspaceFolder); + const configPath = configFile ? configFile : workspace + ? (await getDevContainerConfigPathIn(cliHost, workspace.configFolderPath) + || (overrideConfigFile ? getDefaultDevContainerConfigPath(cliHost, workspace.configFolderPath) : undefined)) + : overrideConfigFile; + const configs = configPath && await readDevContainerConfigFile(cliHost, workspace, configPath, params.mountWorkspaceGitRoot, output, undefined, overrideConfigFile) || undefined; + if (!configs) { + throw new ContainerError({ description: `Dev container config (${uriToFsPath(configFile || getDefaultDevContainerConfigPath(cliHost, workspace!.configFolderPath), cliHost.platform)}) not found.` }); + } + const { config, workspaceConfig } = configs; + + const container = containerId ? await inspectContainer(params, containerId) : await findDevContainer(params, idLabels); + if (!container) { + bailOut(common.output, 'Dev container not found.'); + } + const containerProperties = await createContainerProperties(params, container.Id, workspaceConfig.workspaceFolder, config.remoteUser); + const remoteEnv = probeRemoteEnv(common, containerProperties, config); + const remoteCwd = containerProperties.remoteWorkspaceFolder || containerProperties.homeFolder; + const infoOutput = makeLog(output, LogLevel.Info); + await runRemoteCommand({ ...common, output: infoOutput }, containerProperties, restArgs || [], remoteCwd, { remoteEnv: await remoteEnv, print: 'continuous' }); + } catch (err) { + if (!err?.code) { + console.error(err); + } + await dispose(); + process.exit(err?.code || 1); + } + await dispose(); + process.exit(0); +} + +function keyValuesToRecord(keyValues: string[]): Record { + return keyValues.reduce((envs, env) => { + const i = env.indexOf('='); + if (i !== -1) { + envs[env.substring(0, i)] = env.substring(i + 1); + } + return envs; + }, {} as Record); +} + +function getDefaultIdLabels(workspaceFolder: string) { + return [`${hostFolderLabel}=${workspaceFolder}`]; +} \ No newline at end of file diff --git a/src/spec-node/dockerCompose.ts b/src/spec-node/dockerCompose.ts new file mode 100644 index 00000000..b6ff7f1e --- /dev/null +++ b/src/spec-node/dockerCompose.ts @@ -0,0 +1,466 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as yaml from 'js-yaml'; +import * as shellQuote from 'shell-quote'; + +import { createContainerProperties, startEventSeen, ResolverResult, getTunnelInformation, DockerResolverParameters } from './utils'; +import { ContainerProperties, setupInContainer, ResolverProgress } from '../spec-common/injectHeadless'; +import { ContainerError } from '../spec-common/errors'; +import { Workspace } from '../spec-utils/workspaces'; +import { equalPaths, parseVersion, isEarlierVersion } from '../spec-common/commonUtils'; +import { ContainerDetails, inspectContainer, listContainers, DockerCLIParameters, dockerCLI, dockerComposeCLI, dockerComposePtyCLI, PartialExecParameters, DockerComposeCLI, ImageDetails } from '../spec-shutdown/dockerUtils'; +import { DevContainerFromDockerComposeConfig, getDockerComposeFilePaths } from '../spec-configuration/configuration'; +import { LogLevel, makeLog, terminalEscapeSequences } from '../spec-utils/log'; +import { extendImage } from './containerFeatures'; +import { Mount, CollapsedFeaturesConfig } from '../spec-configuration/containerFeaturesConfiguration'; +import { includeAllConfiguredFeatures } from '../spec-utils/product'; + +const projectLabel = 'com.docker.compose.project'; +const serviceLabel = 'com.docker.compose.service'; + +export async function openDockerComposeDevContainer(params: DockerResolverParameters, workspace: Workspace, config: DevContainerFromDockerComposeConfig, idLabels: string[]): Promise { + const { common, dockerCLI, dockerComposeCLI } = params; + const { cliHost, env, output } = common; + const buildParams: DockerCLIParameters = { cliHost, dockerCLI, dockerComposeCLI, env, output }; + return _openDockerComposeDevContainer(params, buildParams, workspace, config, getRemoteWorkspaceFolder(config), idLabels); +} + +async function _openDockerComposeDevContainer(params: DockerResolverParameters, buildParams: DockerCLIParameters, workspace: Workspace, config: DevContainerFromDockerComposeConfig, remoteWorkspaceFolder: string, idLabels: string[]): Promise { + const { common } = params; + const { cliHost: buildCLIHost } = buildParams; + + let container: ContainerDetails | undefined; + let containerProperties: ContainerProperties | undefined; + try { + + const composeFiles = await getDockerComposeFilePaths(buildCLIHost, config, buildCLIHost.env, buildCLIHost.cwd); + const cwdEnvFile = buildCLIHost.path.join(buildCLIHost.cwd, '.env'); + const envFile = Array.isArray(config.dockerComposeFile) && config.dockerComposeFile.length === 0 && await buildCLIHost.isFile(cwdEnvFile) ? cwdEnvFile : undefined; + const projectName = await getProjectName(buildParams, workspace, composeFiles); + const containerId = await findComposeContainer(params, projectName, config.service); + if (params.expectExistingContainer && !containerId) { + throw new ContainerError({ description: 'The expected container does not exist.' }); + } + container = containerId ? await inspectContainer(params, containerId) : undefined; + + if (container && (params.removeOnStartup === true || params.removeOnStartup === container.Id)) { + const text = 'Removing existing container.'; + const start = common.output.start(text); + await dockerCLI(params, 'rm', '-f', container.Id); + common.output.stop(text, start); + container = undefined; + } + + // let collapsedFeaturesConfig: CollapsedFeaturesConfig | undefined; + if (!container || container.State.Status !== 'running') { + const res = await startContainer(params, buildParams, config, projectName, composeFiles, envFile, container, idLabels); + container = await inspectContainer(params, res.containerId); + // collapsedFeaturesConfig = res.collapsedFeaturesConfig; + // } else { + // const labels = container.Config.Labels || {}; + // const featuresConfig = await generateFeaturesConfig(params.common, (await createFeaturesTempFolder(params.common)), config, async () => labels, getContainerFeaturesFolder); + // collapsedFeaturesConfig = collapseFeaturesConfig(featuresConfig); + } + + containerProperties = await createContainerProperties(params, container.Id, remoteWorkspaceFolder, config.remoteUser); + + const { + remoteEnv: extensionHostEnv, + } = await setupInContainer(common, containerProperties, config); + + return { + params: common, + properties: containerProperties, + config, + resolvedAuthority: { + extensionHostEnv, + }, + tunnelInformation: common.isLocalContainer ? getTunnelInformation(container) : {}, + dockerParams: params, + dockerContainerId: container.Id, + }; + + } catch (originalError) { + const err = originalError instanceof ContainerError ? originalError : new ContainerError({ + description: 'An error occurred setting up the container.', + originalError + }); + if (container) { + err.manageContainer = true; + err.params = params.common; + err.containerId = container.Id; + err.dockerParams = params; + } + if (containerProperties) { + err.containerProperties = containerProperties; + } + err.config = config; + throw err; + } +} + +export function getRemoteWorkspaceFolder(config: DevContainerFromDockerComposeConfig) { + return config.workspaceFolder || '/'; +} + +export async function buildDockerCompose(config: DevContainerFromDockerComposeConfig, projectName: string, buildParams: DockerCLIParameters, localComposeFiles: string[], composeGlobalArgs: string[], runServices: string[], noCache: boolean, imageNameOverride?: string) { + const { cliHost } = buildParams; + const args = ['--project-name', projectName, ...composeGlobalArgs]; + if (imageNameOverride) { + const composeOverrideFile = cliHost.path.join(await cliHost.tmpdir(), `docker-compose.devcontainer.build-${Date.now()}.yml`); + const composeOverrideContent = `services: + ${config.service}: + image: ${imageNameOverride} +`; + await cliHost.writeFile(composeOverrideFile, Buffer.from(composeOverrideContent)); + args.push('-f', composeOverrideFile); + } + args.push('build'); + if (noCache) { + args.push('--no-cache', '--pull'); + } + if (runServices.length) { + args.push(...runServices); + if (runServices.indexOf(config.service) === -1) { + args.push(config.service); + } + } + try { + await dockerComposePtyCLI(buildParams, ...args); + } catch (err) { + throw err instanceof ContainerError ? err : new ContainerError({ description: 'An error occurred building the Docker Compose images.', originalError: err, data: { fileWithError: localComposeFiles[0] } }); + } +} + +async function startContainer(params: DockerResolverParameters, buildParams: DockerCLIParameters, config: DevContainerFromDockerComposeConfig, projectName: string, composeFiles: string[], envFile: string | undefined, container: ContainerDetails | undefined, idLabels: string[]) { + const { common } = params; + const { persistedFolder, output } = common; + const { cliHost: buildCLIHost } = buildParams; + const overrideFilePrefix = 'docker-compose.devcontainer.containerFeatures'; + + const build = !container; + common.progress(ResolverProgress.StartingContainer); + + const localComposeFiles = composeFiles; + // If dockerComposeFile is an array, add -f in order. https://docs.docker.com/compose/extends/#multiple-compose-files + const composeGlobalArgs = ([] as string[]).concat(...localComposeFiles.map(composeFile => ['-f', composeFile])); + if (envFile) { + composeGlobalArgs.push('--env-file', envFile); + } + + const infoOutput = makeLog(buildParams.output, LogLevel.Info); + const composeConfig = await readDockerComposeConfig({ ...buildParams, output: infoOutput }, localComposeFiles, envFile); + const services = Object.keys(composeConfig.services || {}); + if (services.indexOf(config.service) === -1) { + throw new ContainerError({ description: `Service '${config.service}' configured in devcontainer.json not found in Docker Compose configuration.`, data: { fileWithError: composeFiles[0] } }); + } + + let cancel: () => void; + const canceled = new Promise((_, reject) => cancel = reject); + const { started } = await startEventSeen(params, { [projectLabel]: projectName, [serviceLabel]: config.service }, canceled, common.output, common.getLogLevel() === LogLevel.Trace); // await getEvents, but only assign started. + + if (build) { + await buildDockerCompose(config, projectName, { ...buildParams, output: infoOutput }, localComposeFiles, composeGlobalArgs, config.runServices ?? [], params.buildNoCache ?? false, undefined); + } + + const service = composeConfig.services[config.service]; + const originalImageName = service.image || `${projectName}_${config.service}`; + + // Try to restore the 'third' docker-compose file and featuresConfig from persisted storage. + // This file may have been generated upon a Codespace creation. + let didRestoreFromPersistedShare = false; + let collapsedFeaturesConfig: CollapsedFeaturesConfig | undefined = undefined; + const labels = container?.Config?.Labels; + output.write(`PersistedPath=${persistedFolder}, ContainerHasLabels=${!!labels}`); + + if (persistedFolder && labels) { + const configFiles = labels['com.docker.compose.project.config_files']; + output.write(`Container was created with these config files: ${configFiles}`); + + // Parse out the full name of the 'containerFeatures' configFile + const files = configFiles?.split(',') ?? []; + const containerFeaturesConfigFile = files.find((f) => f.indexOf(overrideFilePrefix) > -1); + if (containerFeaturesConfigFile) { + const composeFileExists = await buildCLIHost.isFile(containerFeaturesConfigFile); + + if (composeFileExists) { + output.write(`Restoring ${containerFeaturesConfigFile} from persisted storage`); + didRestoreFromPersistedShare = true; + + // Push path to compose arguments + composeGlobalArgs.push('-f', containerFeaturesConfigFile); + } else { + output.write(`Expected ${containerFeaturesConfigFile} to exist, but it did not`, LogLevel.Error); + } + } else { + output.write(`Expected to find a docker-compose file prefixed with ${overrideFilePrefix}, but did not.`, LogLevel.Error); + } + } + + // If features/override docker-compose file hasn't been created yet or a cached version could not be found, generate the file now. + if (!didRestoreFromPersistedShare) { + output.write('Generating composeOverrideFile...'); + + const { updatedImageName, collapsedFeaturesConfig, imageDetails } = await extendImage(params, config, originalImageName, !service.build, service.user); + const composeOverrideContent = await generateFeaturesComposeOverrideContent(updatedImageName, originalImageName, collapsedFeaturesConfig, config, buildParams, composeFiles, imageDetails, service, idLabels, params.additionalMounts); + + const overrideFileHasContents = !!composeOverrideContent && composeOverrideContent.length > 0 && composeOverrideContent.trim() !== ''; + + if (overrideFileHasContents) { + output.write(`Docker Compose override file:\n${composeOverrideContent}`, LogLevel.Trace); + + // Save override docker-compose file to disk. + // Persisted folder is a path that will be maintained between sessions + // Note: As a fallback, persistedFolder is set to the build's tmpDir() directory + + const fileName = `${overrideFilePrefix}-${Date.now()}.yml`; + const composeFolder = buildCLIHost.path.join(persistedFolder, 'docker-compose'); + const composeOverrideFile = buildCLIHost.path.join(composeFolder, fileName); + output.write(`Writing ${fileName} to ${composeFolder}`); + await buildCLIHost.mkdirp(composeFolder); + await buildCLIHost.writeFile(composeOverrideFile, Buffer.from(composeOverrideContent)); + + // Add file path to override file as parameter + composeGlobalArgs.push('-f', composeOverrideFile); + } else { + output.write('Override file was generated, but was empty and thus not persisted or included in the docker-compose arguments.'); + } + } + + const args = ['--project-name', projectName, ...composeGlobalArgs]; + args.push('up', '-d'); + if (params.expectExistingContainer) { + args.push('--no-recreate'); + } + if (config.runServices && config.runServices.length) { + args.push(...config.runServices); + if (config.runServices.indexOf(config.service) === -1) { + args.push(config.service); + } + } + try { + await dockerComposePtyCLI({ ...buildParams, output: infoOutput }, ...args); + } catch (err) { + cancel!(); + throw new ContainerError({ description: 'An error occurred starting Docker Compose up.', originalError: err, data: { fileWithError: localComposeFiles[0] } }); + } + + await started; + return { + containerId: (await findComposeContainer(params, projectName, config.service))!, + collapsedFeaturesConfig, + }; +} + +async function generateFeaturesComposeOverrideContent( + updatedImageName: string, + originalImageName: string, + collapsedFeaturesConfig: CollapsedFeaturesConfig | undefined, + config: DevContainerFromDockerComposeConfig, + buildParams: DockerCLIParameters, + composeFiles: string[], + imageDetails: () => Promise, + service: any, + additionalLabels: string[], + additionalMounts: Mount[], +) { + + const { cliHost: buildCLIHost } = buildParams; + let composeOverrideContent: string = ''; + + const overrideImage = updatedImageName !== originalImageName; + + const featureCaps = [...new Set(([] as string[]).concat(...(collapsedFeaturesConfig?.allFeatures || []) + .filter(f => (includeAllConfiguredFeatures || f.included) && f.value) + .map(f => f.capAdd || [])))]; + const featureSecurityOpts = [...new Set(([] as string[]).concat(...(collapsedFeaturesConfig?.allFeatures || []) + .filter(f => (includeAllConfiguredFeatures || f.included) && f.value) + .map(f => f.securityOpt || [])))]; + const featureMounts = ([] as Mount[]).concat( + ...(collapsedFeaturesConfig?.allFeatures || []) + .map(f => (includeAllConfiguredFeatures || f.included) && f.value && f.mounts) + .filter(Boolean) as Mount[][], + additionalMounts, + ); + const volumeMounts = featureMounts.filter(m => m.type === 'volume'); + const customEntrypoints = (collapsedFeaturesConfig?.allFeatures || []) + .map(f => (includeAllConfiguredFeatures || f.included) && f.value && f.entrypoint) + .filter(Boolean) as string[]; + const composeEntrypoint: string[] | undefined = typeof service.entrypoint === 'string' ? shellQuote.parse(service.entrypoint) : service.entrypoint; + const composeCommand: string[] | undefined = typeof service.command === 'string' ? shellQuote.parse(service.command) : service.command; + const userEntrypoint = config.overrideCommand ? [] : composeEntrypoint /* $ already escaped. */ + || ((await imageDetails()).Config.Entrypoint || []).map(c => c.replace(/\$/g, '$$$$')); // $ > $$ to escape docker-compose.yml's interpolation. + const userCommand = config.overrideCommand ? [] : composeCommand /* $ already escaped. */ + || (composeEntrypoint ? [/* Ignore image CMD per docker-compose.yml spec. */] : ((await imageDetails()).Config.Cmd || []).map(c => c.replace(/\$/g, '$$$$'))); // $ > $$ to escape docker-compose.yml's interpolation. + + composeOverrideContent = `services: + '${config.service}':${overrideImage ? ` + image: ${updatedImageName}` : ''} + entrypoint: ["/bin/sh", "-c", "echo Container started\\n +trap \\"exit 0\\" 15\\n +${customEntrypoints.join('\\n\n')}\\n +exec \\"$$@\\"\\n +while sleep 1 & wait $$!; do :; done", "-"${userEntrypoint.map(a => `, ${JSON.stringify(a)}`).join('')}]${userCommand !== composeCommand ? ` + command: ${JSON.stringify(userCommand)}` : ''}${(collapsedFeaturesConfig?.allFeatures || []).some(f => (includeAllConfiguredFeatures || f.included) && f.value && f.init) ? ` + init: true` : ''}${(collapsedFeaturesConfig?.allFeatures || []).some(f => (includeAllConfiguredFeatures || f.included) && f.value && f.privileged) ? ` + privileged: true` : ''}${featureCaps.length ? ` + cap_add:${featureCaps.map(cap => ` + - ${cap}`).join('')}` : ''}${featureSecurityOpts.length ? ` + security_opt:${featureSecurityOpts.map(securityOpt => ` + - ${securityOpt}`).join('')}` : ''}${additionalLabels.length ? ` + labels:${additionalLabels.map(label => ` + - ${label.replace(/\$/g, '$$$$')}`).join('')}` : ''}${featureMounts.length ? ` + volumes:${featureMounts.map(m => ` + - ${m.source}:${m.target}`).join('')}` : ''}${volumeMounts.length ? ` +volumes:${volumeMounts.map(m => ` + ${m.source}:${m.external ? '\n external: true' : ''}`).join('')}` : ''} +`; + const firstComposeFile = (await buildCLIHost.readFile(composeFiles[0])).toString(); + const version = (/^\s*(version:.*)$/m.exec(firstComposeFile) || [])[1]; + if (version) { + composeOverrideContent = `${version} + +${composeOverrideContent}`; + } + return composeOverrideContent; +} + +export async function readDockerComposeConfig(params: DockerCLIParameters, composeFiles: string[], envFile: string | undefined) { + try { + const composeGlobalArgs = ([] as string[]).concat(...composeFiles.map(composeFile => ['-f', composeFile])); + if (envFile) { + composeGlobalArgs.push('--env-file', envFile); + } + const composeCLI = await params.dockerComposeCLI(); + if ((parseVersion(composeCLI.version) || [])[0] >= 2) { + composeGlobalArgs.push('--profile', '*'); + } + try { + const { stdout } = await dockerComposeCLI(params, ...composeGlobalArgs, 'config'); + const stdoutStr = stdout.toString(); + params.output.write(stdoutStr); + return yaml.load(stdoutStr) || {} as any; + } catch (err) { + if (!Buffer.isBuffer(err?.stderr) || err?.stderr.toString().indexOf('UnicodeEncodeError') === -1) { + throw err; + } + // Upstream issues. https://github.com/microsoft/vscode-remote-release/issues/5308 + if (params.cliHost.platform === 'win32') { + const { cmdOutput } = await dockerComposePtyCLI({ + ...params, + output: makeLog({ + event: params.output.event, + dimensions: { + columns: 999999, + rows: 1, + }, + }, LogLevel.Info), + }, ...composeGlobalArgs, 'config'); + return yaml.load(cmdOutput.replace(terminalEscapeSequences, '')) || {} as any; + } + const { stdout } = await dockerComposeCLI({ + ...params, + env: { + ...params.env, + LANG: 'en_US.UTF-8', + LC_CTYPE: 'en_US.UTF-8', + } + }, ...composeGlobalArgs, 'config'); + const stdoutStr = stdout.toString(); + params.output.write(stdoutStr); + return yaml.load(stdoutStr) || {} as any; + } + } catch (err) { + throw err instanceof ContainerError ? err : new ContainerError({ description: 'An error occurred retrieving the Docker Compose configuration.', originalError: err, data: { fileWithError: composeFiles[0] } }); + } +} + +export async function findComposeContainer(params: DockerCLIParameters | DockerResolverParameters, projectName: string, serviceName: string): Promise { + const list = await listContainers(params, true, [ + `${projectLabel}=${projectName}`, + `${serviceLabel}=${serviceName}` + ]); + return list && list[0]; +} + +export async function getProjectName(params: DockerCLIParameters | DockerResolverParameters, workspace: Workspace, composeFiles: string[]) { + const { cliHost } = 'cliHost' in params ? params : params.common; + const newProjectName = await useNewProjectName(params); + const envName = toProjectName(cliHost.env.COMPOSE_PROJECT_NAME || '', newProjectName); + if (envName) { + return envName; + } + try { + const envPath = cliHost.path.join(cliHost.cwd, '.env'); + const buffer = await cliHost.readFile(envPath); + const match = /^COMPOSE_PROJECT_NAME=(.+)$/m.exec(buffer.toString()); + const value = match && match[1].trim(); + const envFileName = toProjectName(value || '', newProjectName); + if (envFileName) { + return envFileName; + } + } catch (err) { + if (!(err && (err.code === 'ENOENT' || err.code === 'EISDIR'))) { + throw err; + } + } + const configDir = workspace.configFolderPath; + const workingDir = composeFiles[0] ? cliHost.path.dirname(composeFiles[0]) : cliHost.cwd; // From https://github.com/docker/compose/blob/79557e3d3ab67c3697641d9af91866d7e400cfeb/compose/config/config.py#L290 + if (equalPaths(cliHost.platform, workingDir, cliHost.path.join(configDir, '.devcontainer'))) { + return toProjectName(`${cliHost.path.basename(configDir)}_devcontainer`, newProjectName); + } + return toProjectName(cliHost.path.basename(workingDir), newProjectName); +} + +function toProjectName(basename: string, newProjectName: boolean) { + // From https://github.com/docker/compose/blob/79557e3d3ab67c3697641d9af91866d7e400cfeb/compose/cli/command.py#L152 + if (!newProjectName) { + return basename.toLowerCase().replace(/[^a-z0-9]/g, ''); + } + return basename.toLowerCase().replace(/[^-_a-z0-9]/g, ''); +} + +async function useNewProjectName(params: DockerCLIParameters | DockerResolverParameters) { + try { + const version = parseVersion((await params.dockerComposeCLI()).version); + if (!version) { + return true; // Optimistically continue. + } + return !isEarlierVersion(version, [1, 21, 0]); // 1.21.0 changed allowed characters in project names (added hyphen and underscore). + } catch (err) { + return true; // Optimistically continue. + } +} + +export function dockerComposeCLIConfig(params: Omit, dockerCLICmd: string, dockerComposeCLICmd: string) { + let result: Promise; + return () => { + return result || (result = (async () => { + let v2 = false; + let stdout: Buffer; + try { + stdout = (await dockerComposeCLI({ + ...params, + cmd: dockerComposeCLICmd, + }, 'version', '--short')).stdout; + } catch (err) { + if (err?.code !== 'ENOENT') { + throw err; + } + stdout = (await dockerComposeCLI({ + ...params, + cmd: dockerCLICmd, + }, 'compose', 'version', '--short')).stdout; + v2 = true; + } + return { + version: stdout.toString().trim(), + cmd: v2 ? dockerCLICmd : dockerComposeCLICmd, + args: v2 ? ['compose'] : [], + }; + })()); + }; +} diff --git a/src/spec-node/singleContainer.ts b/src/spec-node/singleContainer.ts new file mode 100644 index 00000000..c78225c6 --- /dev/null +++ b/src/spec-node/singleContainer.ts @@ -0,0 +1,320 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +import { createContainerProperties, startEventSeen, ResolverResult, getTunnelInformation, getDockerfilePath, getDockerContextPath, DockerResolverParameters, isDockerFileConfig, uriToWSLFsPath, WorkspaceConfiguration, getFolderImageName } from './utils'; +import { ContainerProperties, setupInContainer, ResolverProgress } from '../spec-common/injectHeadless'; +import { ContainerError, toErrorText } from '../spec-common/errors'; +import { ContainerDetails, listContainers, DockerCLIParameters, inspectContainers, dockerCLI, dockerPtyCLI, toPtyExecParameters, ImageDetails } from '../spec-shutdown/dockerUtils'; +import { DevContainerConfig, DevContainerFromDockerfileConfig, DevContainerFromImageConfig } from '../spec-configuration/configuration'; +import { LogLevel, Log, makeLog } from '../spec-utils/log'; +import { extendImage } from './containerFeatures'; +import { Mount, CollapsedFeaturesConfig } from '../spec-configuration/containerFeaturesConfiguration'; +import { includeAllConfiguredFeatures } from '../spec-utils/product'; + +export const hostFolderLabel = 'devcontainer.local_folder'; // used to label containers created from a workspace/folder + +export async function openDockerfileDevContainer(params: DockerResolverParameters, config: DevContainerFromDockerfileConfig | DevContainerFromImageConfig, workspaceConfig: WorkspaceConfiguration, idLabels: string[]): Promise { + const { common } = params; + // let collapsedFeaturesConfig: () => Promise; + + let container: ContainerDetails | undefined; + let containerProperties: ContainerProperties | undefined; + + try { + container = await findExistingContainer(params, idLabels); + if (container) { + // let _collapsedFeatureConfig: Promise; + // collapsedFeaturesConfig = async () => { + // return _collapsedFeatureConfig || (_collapsedFeatureConfig = (async () => { + // const allLabels = container?.Config.Labels || {}; + // const featuresConfig = await generateFeaturesConfig(params.common, (await createFeaturesTempFolder(params.common)), config, async () => allLabels, getContainerFeaturesFolder); + // return collapseFeaturesConfig(featuresConfig); + // })()); + // }; + await startExistingContainer(params, idLabels, container); + } else { + const imageName = await buildNamedImage(params, config); + + const res = await extendImage(params, config, imageName, 'image' in config, findUserArg(config.runArgs) || config.containerUser); + // collapsedFeaturesConfig = async () => res.collapsedFeaturesConfig; + + try { + await spawnDevContainer(params, config, res.collapsedFeaturesConfig, res.updatedImageName, idLabels, workspaceConfig.workspaceMount, res.imageDetails); + } finally { + // In 'finally' because 'docker run' can fail after creating the container. + // Trying to get it here, so we can offer 'Rebuild Container' as an action later. + container = await findDevContainer(params, idLabels); + } + if (!container) { + return bailOut(common.output, 'Dev container not found.'); + } + } + + containerProperties = await createContainerProperties(params, container.Id, workspaceConfig.workspaceFolder, config.remoteUser); + return await setupContainer(container, params, containerProperties, config); + + } catch (e) { + throw createSetupError(e, container, params, containerProperties, config); + } +} + +function createSetupError(originalError: any, container: ContainerDetails | undefined, params: DockerResolverParameters, containerProperties: ContainerProperties | undefined, config: DevContainerConfig | undefined): ContainerError { + const err = originalError instanceof ContainerError ? originalError : new ContainerError({ + description: 'An error occurred setting up the container.', + originalError + }); + if (container) { + err.manageContainer = true; + err.params = params.common; + err.containerId = container.Id; + err.dockerParams = params; + } + if (containerProperties) { + err.containerProperties = containerProperties; + } + if (config) { + err.config = config; + } + return err; +} + +async function setupContainer(container: ContainerDetails, params: DockerResolverParameters, containerProperties: ContainerProperties, config: DevContainerFromDockerfileConfig | DevContainerFromImageConfig): Promise { + const { common } = params; + const { + remoteEnv: extensionHostEnv, + } = await setupInContainer(common, containerProperties, config); + + return { + params: common, + properties: containerProperties, + config, + resolvedAuthority: { + extensionHostEnv, + }, + tunnelInformation: common.isLocalContainer ? getTunnelInformation(container) : {}, + dockerParams: params, + dockerContainerId: container.Id, + }; +} + +async function buildNamedImage(params: DockerResolverParameters, config: DevContainerFromDockerfileConfig | DevContainerFromImageConfig) { + const imageName = 'image' in config ? config.image : getFolderImageName(params.common); + if (isDockerFileConfig(config)) { + params.common.progress(ResolverProgress.BuildingImage); + await buildImage(params, config, imageName, params.buildNoCache ?? false); + } + return imageName; +} + +export function findUserArg(runArgs: string[] = []) { + for (let i = runArgs.length - 1; i >= 0; i--) { + const runArg = runArgs[i]; + if ((runArg === '-u' || runArg === '--user') && i + 1 < runArgs.length) { + return runArgs[i + 1]; + } + if (runArg.startsWith('-u=') || runArg.startsWith('--user=')) { + return runArg.substr(runArg.indexOf('=') + 1); + } + } + return undefined; +} + +export async function findExistingContainer(params: DockerResolverParameters, labels: string[]) { + const { common } = params; + let container = await findDevContainer(params, labels); + if (params.expectExistingContainer && !container) { + throw new ContainerError({ description: 'The expected container does not exist.' }); + } + if (container && (params.removeOnStartup === true || params.removeOnStartup === container.Id)) { + const text = 'Removing Existing Container'; + const start = common.output.start(text); + await dockerCLI(params, 'rm', '-f', container.Id); + common.output.stop(text, start); + container = undefined; + } + return container; +} + +async function startExistingContainer(params: DockerResolverParameters, labels: string[], container: ContainerDetails) { + const { common } = params; + const start = container.State.Status !== 'running'; + if (start) { + const starting = 'Starting container'; + const start = common.output.start(starting); + await dockerCLI(params, 'start', container.Id); + common.output.stop(starting, start); + let startedContainer = await findDevContainer(params, labels); + if (!startedContainer) { + bailOut(common.output, 'Dev container not found.'); + } + } + return start; +} + +export async function findDevContainer(params: DockerCLIParameters | DockerResolverParameters, labels: string[]): Promise { + const ids = await listContainers(params, true, labels); + const details = await inspectContainers(params, ids); + return details.filter(container => container.State.Status !== 'removing')[0]; +} + +export async function buildImage(buildParams: DockerResolverParameters | DockerCLIParameters, config: DevContainerFromDockerfileConfig, baseImageName: string, noCache: boolean) { + const { cliHost, output } = 'cliHost' in buildParams ? buildParams : buildParams.common; + const dockerfileUri = getDockerfilePath(cliHost, config); + const dockerfilePath = await uriToWSLFsPath(dockerfileUri, cliHost); + if (!cliHost.isFile(dockerfilePath)) { + throw new ContainerError({ description: `Dockerfile (${dockerfilePath}) not found.` }); + } + + const args = ['build', '-f', dockerfilePath, '-t', baseImageName]; + const target = config.build?.target; + if (target) { + args.push('--target', target); + } + if (noCache) { + args.push('--no-cache', '--pull'); + } else if (config.build && config.build.cacheFrom) { + if (typeof config.build.cacheFrom === 'string') { + args.push('--cache-from', config.build.cacheFrom); + } else { + for (let index = 0; index < config.build.cacheFrom.length; index++) { + const cacheFrom = config.build.cacheFrom[index]; + args.push('--cache-from', cacheFrom); + } + } + } + const buildArgs = config.build?.args; + if (buildArgs) { + for (const key in buildArgs) { + args.push('--build-arg', `${key}=${buildArgs[key]}`); + } + } + args.push(await uriToWSLFsPath(getDockerContextPath(cliHost, config), cliHost)); + try { + const infoParams = { ...toPtyExecParameters(buildParams), output: makeLog(output, LogLevel.Info) }; + await dockerPtyCLI(infoParams, ...args); + } catch (err) { + throw new ContainerError({ description: 'An error occurred building the image.', originalError: err, data: { fileWithError: dockerfilePath } }); + } +} + +export async function spawnDevContainer(params: DockerResolverParameters, config: DevContainerFromDockerfileConfig | DevContainerFromImageConfig, collapsedFeaturesConfig: CollapsedFeaturesConfig | undefined, imageName: string, labels: string[], workspaceMount: string | undefined, imageDetails: (() => Promise) | undefined) { + const { common } = params; + common.progress(ResolverProgress.StartingContainer); + + const appPort = config.appPort; + const exposedPorts = typeof appPort === 'number' || typeof appPort === 'string' ? [appPort] : appPort || []; + const exposed = ([]).concat(...exposedPorts.map(port => ['-p', typeof port === 'number' ? `127.0.0.1:${port}:${port}` : port])); + + const cwdMount = workspaceMount ? ['--mount', workspaceMount] : []; + + const mounts = config.mounts ? ([] as string[]).concat(...config.mounts.map(m => ['--mount', m])) : []; + + const envObj = config.containerEnv; + const containerEnv = envObj ? Object.keys(envObj) + .reduce((args, key) => { + args.push('-e', `${key}=${envObj[key]}`); + return args; + }, [] as string[]) : []; + + const containerUser = config.containerUser ? ['-u', config.containerUser] : []; + + const featureArgs: string[] = []; + if ((collapsedFeaturesConfig?.allFeatures || []).some(f => (includeAllConfiguredFeatures || f.included) && f.value && f.init)) { + featureArgs.push('--init'); + } + if ((collapsedFeaturesConfig?.allFeatures || []).some(f => (includeAllConfiguredFeatures || f.included) && f.value && f.privileged)) { + featureArgs.push('--privileged'); + } + const caps = new Set(([] as string[]).concat(...(collapsedFeaturesConfig?.allFeatures || []) + .filter(f => (includeAllConfiguredFeatures || f.included) && f.value) + .map(f => f.capAdd || []))); + for (const cap of caps) { + featureArgs.push('--cap-add', cap); + } + const securityOpts = new Set(([] as string[]).concat(...(collapsedFeaturesConfig?.allFeatures || []) + .filter(f => (includeAllConfiguredFeatures || f.included) && f.value) + .map(f => f.securityOpt || []))); + for (const securityOpt of securityOpts) { + featureArgs.push('--security-opt', securityOpt); + } + + const featureMounts = ([] as string[]).concat( + ...([] as Mount[]).concat( + ...(collapsedFeaturesConfig?.allFeatures || []) + .map(f => (includeAllConfiguredFeatures || f.included) && f.value && f.mounts) + .filter(Boolean) as Mount[][], + params.additionalMounts, + ).map(m => ['--mount', `type=${m.type},src=${m.source},dst=${m.target}`]) + ); + + const customEntrypoints = (collapsedFeaturesConfig?.allFeatures || []) + .map(f => (includeAllConfiguredFeatures || f.included) && f.value && f.entrypoint) + .filter(Boolean) as string[]; + const entrypoint = ['--entrypoint', '/bin/sh']; + const cmd = ['-c', `echo Container started +trap "exit 0" 15 +${customEntrypoints.join('\n')} +exec "$@" +while sleep 1 & wait $!; do :; done`, '-']; // `wait $!` allows for the `trap` to run (synchronous `sleep` would not). + if (config.overrideCommand === false && imageDetails) { + const details = await imageDetails(); + cmd.push(...details.Config.Entrypoint || []); + cmd.push(...details.Config.Cmd || []); + } + + const args = [ + 'run', + '--sig-proxy=false', + '-a', 'STDOUT', + '-a', 'STDERR', + ...exposed, + ...cwdMount, + ...mounts, + ...featureMounts, + ...getLabels(labels), + ...containerEnv, + ...containerUser, + ...(config.runArgs || []), + ...featureArgs, + ...entrypoint, + imageName, + ...cmd + ]; + + let cancel: () => void; + const canceled = new Promise((_, reject) => cancel = reject); + const { started } = await startEventSeen(params, getLabelsAsRecord(labels), canceled, common.output, common.getLogLevel() === LogLevel.Trace); + + const text = 'Starting container'; + const start = common.output.start(text); + + const infoParams = { ...toPtyExecParameters(params), output: makeLog(params.common.output, LogLevel.Info) }; + const result = dockerPtyCLI(infoParams, ...args); + result.then(cancel!, cancel!); + + await started; + common.output.stop(text, start); +} + +function getLabels(labels: string[]): string[] { + let result: string[] = []; + labels.forEach(each => result.push('-l', each)); + return result; +} + +function getLabelsAsRecord(labels: string[]): Record { + let result: Record = {}; + labels.forEach(each => { + let pair = each.split('='); + result[pair[0]] = pair[1]; + }); + return result; +} + +export function bailOut(output: Log, message: string): never { + output.write(toErrorText(message)); + throw new Error(message); +} diff --git a/src/spec-node/tsconfig.json b/src/spec-node/tsconfig.json new file mode 100644 index 00000000..888e5189 --- /dev/null +++ b/src/spec-node/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.base.json", + "references": [ + { + "path": "../spec-utils" + }, + { + "path": "../spec-common" + }, + { + "path": "../spec-configuration" + }, + { + "path": "../spec-shutdown" + } + ] +} \ No newline at end of file diff --git a/src/spec-node/typings/node-pty.d.ts b/src/spec-node/typings/node-pty.d.ts new file mode 100644 index 00000000..a933f736 --- /dev/null +++ b/src/spec-node/typings/node-pty.d.ts @@ -0,0 +1,202 @@ +/** + * Copyright (c) 2017, Daniel Imms (MIT License). + * Copyright (c) 2018, Microsoft Corporation (MIT License). + */ + +declare module 'node-pty' { + /** + * Forks a process as a pseudoterminal. + * @param file The file to launch. + * @param args The file's arguments as argv (string[]) or in a pre-escaped CommandLine format + * (string). Note that the CommandLine option is only available on Windows and is expected to be + * escaped properly. + * @param options The options of the terminal. + * @see CommandLineToArgvW https://msdn.microsoft.com/en-us/library/windows/desktop/bb776391(v=vs.85).aspx + * @see Parsing C++ Comamnd-Line Arguments https://msdn.microsoft.com/en-us/library/17w5ykft.aspx + * @see GetCommandLine https://msdn.microsoft.com/en-us/library/windows/desktop/ms683156.aspx + */ + export function spawn(file: string, args: string[] | string, options: IPtyForkOptions | IWindowsPtyForkOptions): IPty; + + export interface IBasePtyForkOptions { + + /** + * Name of the terminal to be set in environment ($TERM variable). + */ + name?: string; + + /** + * Number of intial cols of the pty. + */ + cols?: number; + + /** + * Number of initial rows of the pty. + */ + rows?: number; + + /** + * Working directory to be set for the slave program. + */ + cwd?: string; + + /** + * Environment to be set for the slave program. + */ + env?: { [key: string]: string }; + + /** + * String encoding of the underlying pty. + * If set, incoming data will be decoded to strings and outgoing strings to bytes applying this encoding. + * If unset, incoming data will be delivered as raw bytes (Buffer type). + * By default 'utf8' is assumed, to unset it explicitly set it to `null`. + */ + encoding?: string | null; + + /** + * (EXPERIMENTAL) + * Whether to enable flow control handling (false by default). If enabled a message of `flowControlPause` + * will pause the socket and thus blocking the slave program execution due to buffer back pressure. + * A message of `flowControlResume` will resume the socket into flow mode. + * For performance reasons only a single message as a whole will match (no message part matching). + * If flow control is enabled the `flowControlPause` and `flowControlResume` messages are not forwarded to + * the underlying pseudoterminal. + */ + handleFlowControl?: boolean; + + /** + * (EXPERIMENTAL) + * The string that should pause the pty when `handleFlowControl` is true. Default is XOFF ('\x13'). + */ + flowControlPause?: string; + + /** + * (EXPERIMENTAL) + * The string that should resume the pty when `handleFlowControl` is true. Default is XON ('\x11'). + */ + flowControlResume?: string; + } + + export interface IPtyForkOptions extends IBasePtyForkOptions { + /** + * Security warning: use this option with great caution, as opened file descriptors + * with higher privileges might leak to the slave program. + */ + uid?: number; + gid?: number; + } + + export interface IWindowsPtyForkOptions extends IBasePtyForkOptions { + /** + * Whether to use the ConPTY system on Windows. When this is not set, ConPTY will be used when + * the Windows build number is >= 18309 (instead of winpty). Note that ConPTY is available from + * build 17134 but is too unstable to enable by default. + * + * This setting does nothing on non-Windows. + */ + useConpty?: boolean; + + /** + * Whether to use PSEUDOCONSOLE_INHERIT_CURSOR in conpty. + * @see https://docs.microsoft.com/en-us/windows/console/createpseudoconsole + */ + conptyInheritCursor?: boolean; + } + + /** + * An interface representing a pseudoterminal, on Windows this is emulated via the winpty library. + */ + export interface IPty { + /** + * The process ID of the outer process. + */ + readonly pid: number; + + /** + * The column size in characters. + */ + readonly cols: number; + + /** + * The row size in characters. + */ + readonly rows: number; + + /** + * The title of the active process. + */ + readonly process: string; + + /** + * (EXPERIMENTAL) + * Whether to handle flow control. Useful to disable/re-enable flow control during runtime. + * Use this for binary data that is likely to contain the `flowControlPause` string by accident. + */ + handleFlowControl: boolean; + + /** + * Adds an event listener for when a data event fires. This happens when data is returned from + * the pty. + * @returns an `IDisposable` to stop listening. + */ + readonly onData: IEvent; + + /** + * Adds an event listener for when an exit event fires. This happens when the pty exits. + * @returns an `IDisposable` to stop listening. + */ + readonly onExit: IEvent<{ exitCode: number; signal?: number }>; + + /** + * Adds a listener to the data event, fired when data is returned from the pty. + * @param event The name of the event. + * @param listener The callback function. + * @deprecated Use IPty.onData + */ + on(event: 'data', listener: (data: string) => void): void; + + /** + * Adds a listener to the exit event, fired when the pty exits. + * @param event The name of the event. + * @param listener The callback function, exitCode is the exit code of the process and signal is + * the signal that triggered the exit. signal is not supported on Windows. + * @deprecated Use IPty.onExit + */ + on(event: 'exit', listener: (exitCode: number, signal?: number) => void): void; + + /** + * Resizes the dimensions of the pty. + * @param columns THe number of columns to use. + * @param rows The number of rows to use. + */ + resize(columns: number, rows: number): void; + + /** + * Writes data to the pty. + * @param data The data to write. + */ + write(data: string): void; + + /** + * Kills the pty. + * @param signal The signal to use, defaults to SIGHUP. This parameter is not supported on + * Windows. + * @throws Will throw when signal is used on Windows. + */ + kill(signal?: string): void; + } + + /** + * An object that can be disposed via a dispose function. + */ + export interface IDisposable { + dispose(): void; + } + + /** + * An event that can be listened to. + * @returns an `IDisposable` to stop listening. + */ + export interface IEvent { + (listener: (e: T) => any): IDisposable; + } +} \ No newline at end of file diff --git a/src/spec-node/utils.ts b/src/spec-node/utils.ts new file mode 100644 index 00000000..fba1ba05 --- /dev/null +++ b/src/spec-node/utils.ts @@ -0,0 +1,318 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as path from 'path'; +import * as crypto from 'crypto'; + +import { ContainerError, toErrorText } from '../spec-common/errors'; +import { CLIHost, runCommandNoPty, runCommand } from '../spec-common/commonUtils'; +import { Log, LogLevel, makeLog, nullLog } from '../spec-utils/log'; + +import { ContainerProperties, getContainerProperties, ResolverParameters } from '../spec-common/injectHeadless'; +import { Workspace } from '../spec-utils/workspaces'; +import { URI } from 'vscode-uri'; +import { ShellServer } from '../spec-common/shellServer'; +import { inspectContainer, inspectImage, getEvents, ContainerDetails, DockerCLIParameters, dockerExecFunction, dockerPtyCLI, dockerPtyExecFunction, toDockerImageName, DockerComposeCLI } from '../spec-shutdown/dockerUtils'; +import { getRemoteWorkspaceFolder } from './dockerCompose'; +import { findGitRootFolder } from '../spec-common/git'; +import { parentURI, uriToFsPath } from '../spec-configuration/configurationCommonUtils'; +import { DevContainerConfig, DevContainerFromDockerfileConfig, getConfigFilePath, getDockerfilePath } from '../spec-configuration/configuration'; +import { StringDecoder } from 'string_decoder'; +import { Event } from '../spec-utils/event'; +import { Mount } from '../spec-configuration/containerFeaturesConfiguration'; +import { PackageConfiguration } from '../spec-utils/product'; + +export { getConfigFilePath, getDockerfilePath, isDockerFileConfig, resolveConfigFilePath } from '../spec-configuration/configuration'; +export { uriToFsPath, parentURI } from '../spec-configuration/configurationCommonUtils'; +export { CLIHostDocuments, Documents, createDocuments, Edit, fileDocuments, RemoteDocuments } from '../spec-configuration/editableFiles'; +export { getPackageConfig } from '../spec-utils/product'; + + +export type BindMountConsistency = 'consistent' | 'cached' | 'delegated' | undefined; + +export async function uriToWSLFsPath(uri: URI, cliHost: CLIHost): Promise { + if (uri.scheme === 'file' && cliHost.type === 'wsl'){ + // convert local path (e.g. repository-container Dockerfile) to WSL path + const { stdout } = await runCommandNoPty({ + exec: cliHost.exec, + cmd: 'wslpath', + args: ['-u', uri.fsPath], + output: nullLog, + }); + const cliHostPath = stdout.toString().trim(); + return cliHostPath; + } + return uriToFsPath(uri, cliHost.platform); +} + +export type ParsedAuthority = DevContainerAuthority; + +export type UpdateRemoteUserUIDDefault = 'never' | 'on' | 'off'; + +export interface DockerResolverParameters { + common: ResolverParameters; + parsedAuthority: ParsedAuthority | undefined; + dockerCLI: string; + dockerComposeCLI: () => Promise; + dockerEnv: NodeJS.ProcessEnv; + workspaceMountConsistencyDefault: BindMountConsistency; + mountWorkspaceGitRoot: boolean; + updateRemoteUserUIDOnMacOS: boolean; + cacheMount: 'volume' | 'bind' | 'none'; + removeOnStartup?: boolean | string; + buildNoCache?: boolean; + expectExistingContainer?: boolean; + userRepositoryConfigurationPaths: string[]; + additionalMounts: Mount[]; + updateRemoteUserUIDDefault: UpdateRemoteUserUIDDefault; +} + +export interface ResolverResult { + params: ResolverParameters; + properties: ContainerProperties; + config: DevContainerConfig | undefined; + resolvedAuthority: { extensionHostEnv?: { [key: string]: string | null } }; + tunnelInformation: { environmentTunnels?: { remoteAddress: { port: number; host: string }; localAddress: string }[] }; + isTrusted?: boolean; + dockerParams: DockerResolverParameters | undefined; + dockerContainerId: string | undefined; +} + +export async function startEventSeen(params: DockerResolverParameters, labels: Record, canceled: Promise, output: Log, trace: boolean) { + const eventsProcess = await getEvents(params, { event: ['start'] }); + return { + started: new Promise((resolve, reject) => { + canceled.catch(err => { + eventsProcess.terminate(); + reject(err); + }); + const decoder = new StringDecoder('utf8'); + let startPart = ''; + eventsProcess.stdout.on('data', async chunk => { + if (chunk) { + const part = decoder.write(chunk); + if (trace) { + output.write(`Log: startEventSeen#data ${part.trim().replace(/\r?\n/g, '\r\n')}\r\n`); + } + const lines = (startPart + part).split('\n'); + startPart = lines.pop()!; + for (const line of lines) { + if (line.trim()) { + try { + const info = JSON.parse(line); + // Docker uses 'status', Podman 'Status'. + if ((info.status || info.Status) === 'start' && await hasLabels(params, info, labels)) { + eventsProcess.terminate(); + resolve(); + } + } catch (e) { + // Ignore invalid JSON. + console.error(e); + console.error(line); + } + } + } + } + }); + }) + }; +} + +async function hasLabels(params: DockerResolverParameters, info: any, expectedLabels: Record) { + const actualLabels = info.Actor?.Attributes + // Docker uses 'id', Podman 'ID'. + || (await inspectContainer(params, info.id || info.ID)).Config.Labels + || {}; + return Object.keys(expectedLabels) + .every(name => actualLabels[name] === expectedLabels[name]); +} + +export async function inspectDockerImage(params: DockerResolverParameters, imageName: string, pullImageOnError: boolean) { + try { + return await inspectImage(params, imageName); + } catch (err) { + if (!pullImageOnError) { + throw err; + } + try { + await dockerPtyCLI(params, 'pull', imageName); + } catch (_err) { + if (err.stdout) { + params.common.output.write(err.stdout.toString()); + } + if (err.stderr) { + params.common.output.write(toErrorText(err.stderr.toString())); + } + throw err; + } + return inspectImage(params, imageName); + } +} + +export interface DevContainerAuthority { + hostPath: string; // local path of the folder or workspace file +} + +export function isDevContainerAuthority(authority: ParsedAuthority): authority is DevContainerAuthority { + return (authority as DevContainerAuthority).hostPath !== undefined; +} + +export async function getHostMountFolder(cliHost: CLIHost, folderPath: string, mountWorkspaceGitRoot: boolean, output: Log): Promise { + return mountWorkspaceGitRoot && await findGitRootFolder(cliHost, folderPath, output) || folderPath; +} + +export interface WorkspaceConfiguration { + workspaceMount: string | undefined; + workspaceFolder: string | undefined; +} + +export async function getWorkspaceConfiguration(cliHost: CLIHost, workspace: Workspace | undefined, config: DevContainerConfig, mountWorkspaceGitRoot: boolean, output: Log, consistency?: BindMountConsistency): Promise { + if ('dockerComposeFile' in config) { + return { + workspaceFolder: getRemoteWorkspaceFolder(config), + workspaceMount: undefined, + }; + } + let { workspaceFolder, workspaceMount } = config; + if (workspace && (!workspaceFolder || !('workspaceMount' in config))) { + const hostMountFolder = await getHostMountFolder(cliHost, workspace.rootFolderPath, mountWorkspaceGitRoot, output); + if (!workspaceFolder) { + const rel = cliHost.path.relative(cliHost.path.dirname(hostMountFolder), workspace.rootFolderPath); + workspaceFolder = `/workspaces/${cliHost.platform === 'win32' ? rel.replace(/\\/g, '/') : rel}`; + } + if (!('workspaceMount' in config)) { + const containerMountFolder = `/workspaces/${cliHost.path.basename(hostMountFolder)}`; + const cons = cliHost.platform !== 'linux' ? `,consistency=${consistency || 'consistent'}` : ''; // Podman does not tolerate consistency= + const srcQuote = hostMountFolder.indexOf(',') !== -1 ? '"' : ''; + const tgtQuote = containerMountFolder.indexOf(',') !== -1 ? '"' : ''; + workspaceMount = `type=bind,${srcQuote}source=${hostMountFolder}${srcQuote},${tgtQuote}target=${containerMountFolder}${tgtQuote}${cons}`; + } + } + return { + workspaceFolder, + workspaceMount, + }; +} + +export function getTunnelInformation(container: ContainerDetails) /*: vscode.TunnelInformation */ { + return { + environmentTunnels: container.Ports.filter(staticPort => !!staticPort.PublicPort) + .map((port) => { + return { + remoteAddress: { + port: port.PrivatePort, + host: port.IP + }, + localAddress: port.IP + ':' + port.PublicPort + }; + }) + }; +} + +export function getDockerContextPath(cliHost: { platform: NodeJS.Platform }, config: DevContainerFromDockerfileConfig) { + const context = 'dockerFile' in config ? config.context : config.build.context; + if (context) { + return getConfigFilePath(cliHost, config, context); + } + return parentURI(getDockerfilePath(cliHost, config)); +} + +export async function createContainerProperties(params: DockerResolverParameters, containerId: string, remoteWorkspaceFolder: string | undefined, remoteUser: string | undefined, rootShellServer?: ShellServer) { + const { common } = params; + const inspecting = 'Inspecting container'; + const start = common.output.start(inspecting); + const containerInfo = await inspectContainer(params, containerId); + common.output.stop(inspecting, start); + const containerUser = remoteUser || containerInfo.Config.User || 'root'; + const [, user, , group ] = /([^:]*)(:(.*))?/.exec(containerUser) as (string | undefined)[]; + const containerEnv = envListToObj(containerInfo.Config.Env); + const remoteExec = dockerExecFunction(params, containerId, containerUser); + const remotePtyExec = await dockerPtyExecFunction(params, containerId, containerUser, common.loadNativeModule); + const remoteExecAsRoot = dockerExecFunction(params, containerId, 'root'); + return getContainerProperties({ + params: common, + createdAt: containerInfo.Created, + startedAt: containerInfo.State.StartedAt, + remoteWorkspaceFolder, + containerUser: user === '0' ? 'root' : user, + containerGroup: group, + containerEnv, + remoteExec, + remotePtyExec, + remoteExecAsRoot, + rootShellServer, + }); +} + +function envListToObj(list: string[] | null) { + // Handle Env is null (https://github.com/microsoft/vscode-remote-release/issues/2058). + return (list || []).reduce((obj, pair) => { + const i = pair.indexOf('='); + if (i !== -1) { + obj[pair.substr(0, i)] = pair.substr(i + 1); + } + return obj; + }, {} as NodeJS.ProcessEnv); +} + +export async function runUserCommand(params: DockerResolverParameters, command: string | string[] | undefined, onDidInput?: Event) { + if (!command) { + return; + } + const { common, dockerEnv } = params; + const { cliHost, output } = common; + const isWindows = cliHost.platform === 'win32'; + const shell = isWindows ? [cliHost.env.ComSpec || 'cmd.exe', '/c'] : ['/bin/sh', '-c']; + const updatedCommand = isWindows && Array.isArray(command) && command.length ? + [ (command[0] || '').replace(/\//g, '\\'), ...command.slice(1) ] : + command; + const args = typeof updatedCommand === 'string' ? [...shell, updatedCommand] : updatedCommand; + if (!args.length) { + return; + } + const postCommandName = 'initializeCommand'; + const infoOutput = makeLog(output, LogLevel.Info); + try { + infoOutput.raw(`\x1b[1mRunning the ${postCommandName} from devcontainer.json...\x1b[0m\r\n\r\n`); + await runCommand({ + ptyExec: cliHost.ptyExec, + cmd: args[0], + args: args.slice(1), + env: dockerEnv, + output: infoOutput, + onDidInput, + }); + infoOutput.raw('\r\n'); + } catch (err) { + if (err && (err.code === 130 || err.signal === 2)) { // SIGINT seen on darwin as code === 130, would also make sense as signal === 2. + infoOutput.raw(`\r\n\x1b[1m${postCommandName} interrupted.\x1b[0m\r\n\r\n`); + } else { + throw new ContainerError({ + description: `The ${postCommandName} in the devcontainer.json failed.`, + originalError: err, + }); + } + } +} + +export function getFolderImageName(params: ResolverParameters | DockerCLIParameters) { + const {cwd} = 'cwd' in params ? params : params.cliHost; + const folderHash = getFolderHash(cwd); + const baseName = path.basename(cwd); + return toDockerImageName(`vsc-${baseName}-${folderHash}`); +} + +export function getFolderHash(fsPath: string): string { + return crypto.createHash('md5').update(fsPath).digest('hex'); +} + +export async function createFeaturesTempFolder(params: { cliHost: CLIHost; package: PackageConfiguration }): Promise { + const { cliHost } = params; + const { version } = params.package; + // Create temp folder + const tmpFolder: string = cliHost.path.join(await cliHost.tmpdir(), 'vsch', 'container-features', `${version}-${Date.now()}`); + await cliHost.mkdirp(tmpFolder); + return tmpFolder; +} diff --git a/src/spec-shutdown/dockerUtils.ts b/src/spec-shutdown/dockerUtils.ts new file mode 100644 index 00000000..56b7caa7 --- /dev/null +++ b/src/spec-shutdown/dockerUtils.ts @@ -0,0 +1,416 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CLIHost, runCommand, runCommandNoPty, ExecFunction, ExecParameters, Exec, PtyExecFunction, PtyExec, PtyExecParameters } from '../spec-common/commonUtils'; +import { toErrorText } from '../spec-common/errors'; +import * as ptyType from 'node-pty'; +import { Log, makeLog } from '../spec-utils/log'; +import { Event } from '../spec-utils/event'; + +export interface ContainerDetails { + Id: string; + Created: string; + Name: string; + State: { + Status: string; + StartedAt: string; + FinishedAt: string; + }; + Config: { + Image: string; + User: string; + Env: string[] | null; + Labels: Record | null; + }; + Mounts: { + Type: string; + Name?: string; + Source: string; + Destination: string; + }[]; + NetworkSettings: { + Ports: Record; + }; + Ports: { + IP: string; + PrivatePort: number; + PublicPort: number; + Type: string; + }[]; +} + +export interface DockerCLIParameters { + cliHost: CLIHost; + dockerCLI: string; + dockerComposeCLI: () => Promise; + env: NodeJS.ProcessEnv; + output: Log; +} + +export interface PartialExecParameters { + exec: ExecFunction; + cmd: string; + args?: string[]; + env: NodeJS.ProcessEnv; + output: Log; + print?: boolean; +} + +export interface PartialPtyExecParameters { + ptyExec: PtyExecFunction; + cmd: string; + args?: string[]; + env: NodeJS.ProcessEnv; + output: Log; + onDidInput?: Event; +} + +interface DockerResolverParameters { + dockerCLI: string; + dockerComposeCLI: () => Promise; + dockerEnv: NodeJS.ProcessEnv; + common: { + cliHost: CLIHost; + output: Log; + }; +} + +export interface DockerComposeCLI { + version: string; + cmd: string; + args: string[]; +} + +export async function inspectContainer(params: DockerCLIParameters | PartialExecParameters | DockerResolverParameters, id: string): Promise { + return (await inspectContainers(params, [id]))[0]; +} + +export async function inspectContainers(params: DockerCLIParameters | PartialExecParameters | DockerResolverParameters, ids: string[]): Promise { + const results = await inspect(params, 'container', ids); + for (const result of results) { + result.Ports = []; + const rawPorts = result.NetworkSettings.Ports; + for (const privatePortAndType in rawPorts) { + const [ PrivatePort, Type ] = privatePortAndType.split('/'); + for (const targetPort of rawPorts[privatePortAndType] || []) { + const { HostIp: IP, HostPort: PublicPort } = targetPort; + result.Ports.push({ + IP, + PrivatePort: parseInt(PrivatePort), + PublicPort: parseInt(PublicPort), + Type + }); + } + } + } + return results; +} + +export interface ImageDetails { + Id: string; + Config: { + User: string; + Env: string[] | null; + Labels: Record | null; + Entrypoint: string[] | null; + Cmd: string[] | null; + }; +} + +export async function inspectImage(params: DockerCLIParameters | PartialExecParameters | DockerResolverParameters, id: string): Promise { + return (await inspect(params, 'image', [id]))[0]; +} + +export interface VolumeDetails { + Name: string; + CreatedAt: string; + Labels: Record | null; +} + +export async function inspectVolume(params: DockerCLIParameters | PartialExecParameters | DockerResolverParameters, name: string): Promise { + return (await inspect(params, 'volume', [name]))[0]; +} + +export async function inspectVolumes(params: DockerCLIParameters | PartialExecParameters | DockerResolverParameters, names: string[]): Promise { + return inspect(params, 'volume', names); +} + +async function inspect(params: DockerCLIParameters | PartialExecParameters | DockerResolverParameters, type: 'container' | 'image' | 'volume', ids: string[]): Promise { + if (!ids.length) { + return []; + } + const partial = toExecParameters(params); + const result = await runCommandNoPty({ + ...partial, + args: (partial.args || []).concat(['inspect', '--type', type, ...ids]), + }); + try { + return JSON.parse(result.stdout.toString()); + } catch (err) { + console.error({ + stdout: result.stdout.toString(), + stderr: result.stderr.toString(), + }); + throw err; + } +} + +export async function listContainers(params: DockerCLIParameters | PartialExecParameters | DockerResolverParameters, all = false, labels: string[] = []) { + const filterArgs = []; + if (all) { + filterArgs.push('-a'); + } + for (const label of labels) { + filterArgs.push('--filter', `label=${label}`); + } + const result = await dockerCLI(params, 'ps', '-q', ...filterArgs); + return result.stdout + .toString() + .split(/\r?\n/) + .filter(s => !!s); +} + +export async function listVolumes(params: DockerCLIParameters | PartialExecParameters | DockerResolverParameters, labels: string[] = []) { + const filterArgs = []; + for (const label of labels) { + filterArgs.push('--filter', `label=${label}`); + } + const result = await dockerCLI(params, 'volume', 'ls', '-q', ...filterArgs); + return result.stdout + .toString() + .split(/\r?\n/) + .filter(s => !!s); +} + +export async function createVolume(params: DockerCLIParameters | PartialExecParameters | DockerResolverParameters, name: string, labels: string[]) { + const labelArgs: string[] = []; + for (const label of labels) { + labelArgs.push('--label', label); + } + await dockerCLI(params, 'volume', 'create', ...labelArgs, name); +} + +export async function getEvents(params: DockerCLIParameters | DockerResolverParameters, filters?: Record) { + const { exec, cmd, args, env, output } = toExecParameters(params); + const filterArgs = []; + for (const filter in filters) { + for (const value of filters[filter]) { + filterArgs.push('--filter', `${filter}=${value}`); + } + } + const format = await isPodman(params) ? 'json' : '{{json .}}'; // https://github.com/containers/libpod/issues/5981 + const combinedArgs = (args || []).concat(['events', '--format', format, ...filterArgs]); + + const p = await exec({ + cmd, + args: combinedArgs, + env, + output, + }); + + const stderr: Buffer[] = []; + p.stderr.on('data', data => stderr.push(data)); + + p.exit.then(({ code, signal }) => { + if (stderr.length) { + output.write(toErrorText(Buffer.concat(stderr).toString())); + } + if (code || (signal && signal !== 'SIGKILL')) { + output.write(toErrorText(`Docker events terminated (code: ${code}, signal: ${signal}).`)); + } + }, err => { + output.write(toErrorText(err && (err.stack || err.message))); + }); + + return p; +} + +export async function dockerCLI(params: DockerCLIParameters | PartialExecParameters | DockerResolverParameters, ...args: string[]) { + const partial = toExecParameters(params); + return runCommandNoPty({ + ...partial, + args: (partial.args || []).concat(args), + }); +} + +export async function dockerContext(params: DockerCLIParameters) { + try { + // 'docker context show' is only available as an addon from the 'compose-cli'. 'docker context inspect' connects to the daemon making it slow. Using 'docker context ls' instead. + const { stdout } = await dockerCLI(params, 'context', 'ls', '--format', '{{json .}}'); + const json = `[${ + stdout.toString() + .trim() + .split(/\r?\n/) + .join(',') + }]`; + const contexts = JSON.parse(json) as { Current: boolean; Name: string }[]; + const current = contexts.find(c => c.Current)?.Name; + return current; + } catch { + // Docker is not installed or Podman does not have contexts. + return undefined; + } +} + +export async function isPodman(params: DockerCLIParameters | DockerResolverParameters) { + const cliHost = 'cliHost' in params ? params.cliHost : params.common.cliHost; + if (cliHost.platform !== 'linux') { + return false; + } + try { + const { stdout } = await dockerCLI(params, '-v'); + return stdout.toString().toLowerCase().indexOf('podman') !== -1; + } catch (err) { + return false; + } +} + +export async function dockerPtyCLI(params: PartialPtyExecParameters | DockerResolverParameters, ...args: string[]) { + const partial = toPtyExecParameters(params); + return runCommand({ + ...partial, + args: (partial.args || []).concat(args), + }); +} + +export async function dockerComposeCLI(params: DockerCLIParameters | PartialExecParameters | DockerResolverParameters, ...args: string[]) { + const partial = toExecParameters(params, 'dockerComposeCLI' in params ? await params.dockerComposeCLI() : undefined); + return runCommandNoPty({ + ...partial, + args: (partial.args || []).concat(args), + }); +} + +export async function dockerComposePtyCLI(params: DockerCLIParameters | PartialPtyExecParameters | DockerResolverParameters, ...args: string[]) { + const partial = toPtyExecParameters(params, 'dockerComposeCLI' in params ? await params.dockerComposeCLI() : undefined); + return runCommand({ + ...partial, + args: (partial.args || []).concat(args), + }); +} + +export function dockerExecFunction(params: DockerCLIParameters | PartialExecParameters | DockerResolverParameters, containerName: string, user: string | undefined): ExecFunction { + return async function (execParams: ExecParameters): Promise { + const { exec, cmd, args, env } = toExecParameters(params); + const { argsPrefix, args: execArgs } = toDockerExecArgs(containerName, user, execParams, false); + return exec({ + cmd, + args: (args || []).concat(execArgs), + env, + output: replacingDockerExecLog(execParams.output, cmd, argsPrefix), + }); + }; +} + +export async function dockerPtyExecFunction(params: PartialPtyExecParameters | DockerResolverParameters, containerName: string, user: string | undefined, loadNativeModule: (moduleName: string) => Promise): Promise { + const pty = await loadNativeModule('node-pty'); + if (!pty) { + throw new Error('Missing node-pty'); + } + + return async function (execParams: PtyExecParameters): Promise { + const { ptyExec, cmd, args, env } = toPtyExecParameters(params); + const { argsPrefix, args: execArgs } = toDockerExecArgs(containerName, user, execParams, true); + return ptyExec({ + cmd, + args: (args || []).concat(execArgs), + env, + output: replacingDockerExecLog(execParams.output, cmd, argsPrefix), + }); + }; +} + +function replacingDockerExecLog(original: Log, cmd: string, args: string[]) { + return replacingLog(original, `Run: ${cmd} ${(args || []).join(' ').replace(/\n.*/g, '')}`, 'Run in container:'); +} + +function replacingLog(original: Log, search: string, replace: string) { + const searchR = new RegExp(search.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'); + const wrapped = makeLog({ + ...original, + get dimensions() { + return original.dimensions; + }, + event: e => original.event('text' in e ? { + ...e, + text: e.text.replace(searchR, replace), + } : e), + }); + return wrapped; +} + +function toDockerExecArgs(containerName: string, user: string | undefined, params: ExecParameters | PtyExecParameters, pty: boolean) { + const { env, cwd, cmd, args } = params; + const execArgs = ['exec', '-i']; + if (pty) { + execArgs.push('-t'); + } + if (user) { + execArgs.push('-u', user); + } + if (env) { + Object.keys(env) + .forEach(key => execArgs.push('-e', `${key}=${env[key]}`)); + } + if (cwd) { + execArgs.push('-w', cwd); + } + execArgs.push(containerName); + const argsPrefix = execArgs.slice(); + execArgs.push(cmd); + if (args) { + execArgs.push(...args); + } + return { argsPrefix, args: execArgs }; +} + +export function toExecParameters(params: DockerCLIParameters | PartialExecParameters | DockerResolverParameters, compose?: DockerComposeCLI): PartialExecParameters { + return 'dockerEnv' in params ? { + exec: params.common.cliHost.exec, + cmd: compose ? compose.cmd : params.dockerCLI, + args: compose ? compose.args : [], + env: params.dockerEnv, + output: params.common.output, + } : 'cliHost' in params ? { + exec: params.cliHost.exec, + cmd: compose ? compose.cmd : params.dockerCLI, + args: compose ? compose.args : [], + env: params.env, + output: params.output, + } : { + ...params, + env: params.env, + }; +} + +export function toPtyExecParameters(params: DockerCLIParameters | PartialPtyExecParameters | DockerResolverParameters, compose?: DockerComposeCLI): PartialPtyExecParameters { + return 'dockerEnv' in params ? { + ptyExec: params.common.cliHost.ptyExec, + cmd: compose ? compose.cmd : params.dockerCLI, + args: compose ? compose.args : [], + env: params.dockerEnv, + output: params.common.output, + } : 'cliHost' in params ? { + ptyExec: params.cliHost.ptyExec, + cmd: compose ? compose.cmd : params.dockerCLI, + args: compose ? compose.args : [], + env: params.env, + output: params.output, + } : { + ...params, + env: params.env, + }; +} + +export function toDockerImageName(name: string) { + // https://docs.docker.com/engine/reference/commandline/tag/#extended-description + return name + .toLowerCase() + .replace(/[^a-z0-9\._-]+/g, '') + .replace(/(\.[\._-]|_[\.-]|__[\._-]|-+[\._])[\._-]*/g, (_, a) => a.substr(0, a.length -1)); +} \ No newline at end of file diff --git a/src/spec-shutdown/shutdownUtils.ts b/src/spec-shutdown/shutdownUtils.ts new file mode 100644 index 00000000..2f9625e7 --- /dev/null +++ b/src/spec-shutdown/shutdownUtils.ts @@ -0,0 +1,16 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ShellServer } from '../spec-common/shellServer'; +import { findProcesses } from '../spec-common/proc'; + +export async function findSessions(shellServer: ShellServer) { + const { processes } = await findProcesses(shellServer); + return processes.filter(proc => 'VSCODE_REMOTE_CONTAINERS_SESSION' in proc.env) // TODO: Remove VS Code reference. + .map(proc => ({ + ...proc, + sessionId: proc.env.VSCODE_REMOTE_CONTAINERS_SESSION + })); +} diff --git a/src/spec-shutdown/tsconfig.json b/src/spec-shutdown/tsconfig.json new file mode 100644 index 00000000..f3384514 --- /dev/null +++ b/src/spec-shutdown/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "references": [ + { + "path": "../spec-common" + }, + { + "path": "../spec-utils" + } + ] +} \ No newline at end of file diff --git a/src/spec-utils/event.ts b/src/spec-utils/event.ts new file mode 100644 index 00000000..505e9c09 --- /dev/null +++ b/src/spec-utils/event.ts @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { EventEmitter } from 'events'; + +export interface Event { + (listener: (e: T) => void): Disposable; +} + +export class NodeEventEmitter { + + private nodeEmitter = new EventEmitter(); + + constructor(private register?: { on: () => void; off: () => void }) {} + event: Event = (listener: (e: T) => void): Disposable => { + this.nodeEmitter.on('event', listener); + if (this.register && this.nodeEmitter.listenerCount('event') === 1) { + this.register.on(); + } + return { + dispose: () => { + if (this.register && this.nodeEmitter.listenerCount('event') === 1) { + this.register.off(); + } + this.nodeEmitter.off('event', listener); + } + }; + }; + + fire(data: T) { + this.nodeEmitter.emit('event', data); + } + dispose() { + this.nodeEmitter.removeAllListeners(); + } +} + +export interface ResultEvent { + (listener: (e: E) => R): Disposable; +} + +export class ResultEventEmitter { + + private nodeEmitter = new EventEmitter(); + + event: ResultEvent = (listener: (e: E) => R): Disposable => { + const wrapper = (e: { data: E; results: R[] }) => e.results.push(listener(e.data)); + this.nodeEmitter.on('event', wrapper); + return { + dispose: () => { + this.nodeEmitter.off('event', wrapper); + } + }; + }; + + fire(data: E) { + const results: R[] = []; + this.nodeEmitter.emit('event', { + data, + results, + }); + return results; + } + dispose() { + this.nodeEmitter.removeAllListeners(); + } +} + +export interface Disposable { + dispose(): void; +} diff --git a/src/spec-utils/httpRequest.ts b/src/spec-utils/httpRequest.ts new file mode 100644 index 00000000..7340d56c --- /dev/null +++ b/src/spec-utils/httpRequest.ts @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { https } from 'follow-redirects'; +import * as url from 'url'; +import { Log, LogLevel } from './log'; + +export function request(options: { type: string; url: string; headers: Record; data?: Buffer }, output?: Log) { + return new Promise((resolve, reject) => { + const parsed = new url.URL(options.url); + const reqOptions = { + hostname: parsed.hostname, + port: parsed.port, + path: parsed.pathname + parsed.search, + method: options.type, + headers: options.headers, + }; + const req = https.request(reqOptions, res => { + if (res.statusCode! < 200 || res.statusCode! > 299) { + reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`)); + if (output) { + output.write(`HTTP request failed with status code ${res.statusCode}: : ${res.statusMessage}`, LogLevel.Error); + } + } else { + res.on('error', reject); + const chunks: Buffer[] = []; + res.on('data', chunk => chunks.push(chunk as Buffer)); + res.on('end', () => resolve(Buffer.concat(chunks))); + } + }); + req.on('error', reject); + if (options.data) { + req.write(options.data); + } + req.end(); + }); +} \ No newline at end of file diff --git a/src/spec-utils/log.ts b/src/spec-utils/log.ts new file mode 100644 index 00000000..9770a33b --- /dev/null +++ b/src/spec-utils/log.ts @@ -0,0 +1,297 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as os from 'os'; + +import { Event } from './event'; + +export enum LogLevel { + Trace = 1, + Debug = 2, + Info = 3, + Warning = 4, + Error = 5, + Critical = 6, + Off = 7 +} + +export type LogFormat = 'text' | 'json'; + +const logLevelMap = { + info: LogLevel.Info, + debug: LogLevel.Debug, + trace: LogLevel.Trace, +}; + +type logLevelString = keyof typeof logLevelMap; + +const logLevelReverseMap = (Object.keys(logLevelMap) as logLevelString[]) + .reduce((arr, cur) => { + arr[logLevelMap[cur]] = cur; + return arr; + }, [] as logLevelString[]); + +export function mapLogLevel(text: logLevelString) { + return logLevelMap[text] || LogLevel.Info; +} + +export function reverseMapLogLevel(level: LogLevel) { + return logLevelReverseMap[level] || LogLevel.Info; +} + +export type LogEvent = { + type: 'text' | 'raw' | 'start' | 'stop' | 'progress'; + channel?: string; +} & ( + { + type: 'text' | 'raw' | 'start'; + level: LogLevel; // TODO: Change to string for stringifycation. + timestamp: number; + text: string; + } | + { + type: 'stop'; + level: LogLevel; + timestamp: number; + text: string; + startTimestamp: number; + } | + { + type: 'progress'; + name: string; + status: 'running' | 'succeeded' | 'failed'; + stepDetail?: string; + } +); + +export interface LogHandler { + event(e: LogEvent): void; + dimensions?: LogDimensions; + onDidChangeDimensions?: Event; +} + +export interface Log { + write(text: string, level?: LogLevel): void; + raw(text: string, level?: LogLevel): void; + start(text: string, level?: LogLevel): number; + stop(text: string, start: number, level?: LogLevel): void; + event(e: LogEvent): void; + dimensions?: LogDimensions; + onDidChangeDimensions?: Event; +} + +export interface LogDimensions { + columns: number; + rows: number; +} + +export const nullLog: Log = { + write: () => undefined, + raw: () => undefined, + start: () => Date.now(), + stop: () => undefined, + event: () => undefined, +}; + +export const terminalEscapeSequences = /(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]/g; // https://stackoverflow.com/questions/14693701/how-can-i-remove-the-ansi-escape-sequences-from-a-string-in-python/33925425#33925425 + +export function createCombinedLog(logs: LogHandler[], header?: string): LogHandler { + let sendHeader = !!header; + return { + event: e => { + if (sendHeader) { + sendHeader = false; + logs.forEach(log => log.event({ + type: 'text', + level: LogLevel.Info, + timestamp: Date.now(), + text: header!, + })); + } + logs.forEach(log => log.event(e)); + } + }; +} + +export function createPlainLog(write: (text: string) => void, getLogLevel: () => LogLevel): LogHandler { + return { + event(e) { + const text = logEventToFileText(e, getLogLevel()); + if (text) { + write(text); + } + }, + }; +} + +export function createTerminalLog(write: (text: string) => void, _getLogLevel: () => LogLevel, _sessionStart: Date): LogHandler { + return { + event(e) { + const text = logEventToTerminalText(e, _getLogLevel(), _sessionStart.getTime()); + if (text) { + write(text); + } + } + }; +} + +export function createJSONLog(write: (text: string) => void, _getLogLevel: () => LogLevel, _sessionStart: Date): LogHandler { + return { + event(e) { + write(JSON.stringify(e) + '\n'); + } + }; +} + +export function makeLog(log: LogHandler, defaultLogEventLevel = LogLevel.Debug): Log { + return { + event: log.event, + write(text: string, level = defaultLogEventLevel) { + log.event({ + type: 'text', + level, + timestamp: Date.now(), + text, + }); + }, + raw(text: string, level = defaultLogEventLevel) { + log.event({ + type: 'raw', + level, + timestamp: Date.now(), + text, + }); + }, + start(text: string, level = defaultLogEventLevel) { + const timestamp = Date.now(); + log.event({ + type: 'start', + level, + timestamp, + text, + }); + return timestamp; + }, + stop(text: string, startTimestamp: number, level = defaultLogEventLevel) { + log.event({ + type: 'stop', + level, + timestamp: Date.now(), + text, + startTimestamp, + }); + }, + get dimensions() { + return log.dimensions; + }, + onDidChangeDimensions: log.onDidChangeDimensions, + }; +} + +export function logEventToTerminalText(e: LogEvent, logLevel: LogLevel, startTimestamp: number) { + if (!('level' in e) || e.level < logLevel) { + return undefined; + } + switch (e.type) { + case 'text': return `[${color(timestampColor, `${e.timestamp - startTimestamp} ms`)}] ${toTerminalText(e.text)}`; + case 'raw': return e.text; + case 'start': + if (LogLevel.Trace >= logLevel) { + return `${color(startColor, `[${e.timestamp - startTimestamp} ms] Start`)}: ${toTerminalText(e.text)}`; + } + return `[${color(timestampColor, `${e.timestamp - startTimestamp} ms`)}] Start: ${toTerminalText(e.text)}`; + case 'stop': + if (LogLevel.Trace >= logLevel) { + return `${color(stopColor, `[${e.timestamp - startTimestamp} ms] Stop`)} (${e.timestamp - e.startTimestamp} ms): ${toTerminalText(e.text)}`; + } + return undefined; + default: throw neverLogEventError(e); + } +} + +function toTerminalText(text: string) { + return colorize(text) + .replace(/\r?\n/g, '\r\n').replace(/(\r?\n)?$/, '\r\n'); +} + +function logEventToFileText(e: LogEvent, logLevel: LogLevel) { + if (!('level' in e) || e.level < logLevel) { + return undefined; + } + switch (e.type) { + case 'text': + case 'raw': return `[${new Date(e.timestamp).toISOString()}] ${toLogFileText(e.text)}`; + case 'start': return `[${new Date(e.timestamp).toISOString()}] Start: ${toLogFileText(e.text)}`; + case 'stop': + if (LogLevel.Debug >= logLevel) { + return `[${new Date(e.timestamp).toISOString()}] Stop (${e.timestamp - e.startTimestamp} ms): ${toLogFileText(e.text)}`; + } + return undefined; + default: throw neverLogEventError(e); + } +} + +function toLogFileText(text: string) { + return text.replace(terminalEscapeSequences, '') + .replace(/(\r?\n)?$/, os.EOL); +} + +function neverLogEventError(e: never) { + return new Error(`Unknown log event type: ${(e as LogEvent).type}`); +} + +// foreground 38;2;;; (https://stackoverflow.com/questions/4842424/list-of-ansi-color-escape-sequences) +const red = '38;2;143;99;79'; +const green = '38;2;99;143;79'; +const blue = '38;2;86;156;214'; +export const stopColor = red; +export const startColor = green; +export const timestampColor = green; +export const numberColor = blue; +export function color(color: string, str: string) { + return str.split('\n') + .map(line => `[${color}m${line}`) + .join('\n'); +} + +export function colorize(text: string) { + let m: RegExpExecArray | null; + let lastIndex = 0; + const fragments: string[] = []; + terminalEscapeSequences.lastIndex = 0; + while (m = terminalEscapeSequences.exec(text)) { + fragments.push(colorizePlainText(text.substring(lastIndex, m.index))); + fragments.push(m[0]); + lastIndex = terminalEscapeSequences.lastIndex; + } + fragments.push(colorizePlainText(text.substr(lastIndex))); + return fragments.join(''); +} + +function colorizePlainText(text: string) { + const num = /(?<=^|[^A-Za-z0-9_\-\.])[0-9]+(\.[0-9]+)*(?=$|[^A-Za-z0-9_\-\.])/g; + let m: RegExpExecArray | null; + let lastIndex = 0; + const fragments: string[] = []; + while (m = num.exec(text)) { + fragments.push(text.substring(lastIndex, m.index)); + fragments.push(color(numberColor, m[0])); + lastIndex = num.lastIndex; + } + fragments.push(text.substr(lastIndex)); + return fragments.join(''); +} + +export function toErrorText(str: string) { + return str.split(/\r?\n/) + .map(line => `${line}`) + .join('\r\n') + '\r\n'; +} + +export function toWarningText(str: string) { + return str.split(/\r?\n/) + .map(line => `${line}`) + .join('\r\n') + '\r\n'; +} diff --git a/src/spec-utils/net.ts b/src/spec-utils/net.ts new file mode 100644 index 00000000..437b957d --- /dev/null +++ b/src/spec-utils/net.ts @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as http from 'http'; +import * as https from 'https'; + +export function httpGet(url: string, headers: {} = {}) { + return new Promise((resolve, reject) => { + const httpx = url.startsWith('https:') ? https : http; + + let requestOptions: https.RequestOptions | undefined = undefined; + if (Object.keys(headers).length > 0) { + const parsedUrl = new URL(url); + requestOptions = { + 'headers': headers, + 'host': parsedUrl.host, + 'path': parsedUrl.pathname, + }; + } + + const req = httpx.get(requestOptions ?? url, res => { + if (res.statusCode! < 200 || res.statusCode! > 299) { + + // Redirect + if (res.statusCode! === 302) { + const location = res.headers?.location; + if (location) { + resolve(httpGet(location, headers)); + } + } + reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`)); + } else { + res.on('error', reject); + const chunks: Buffer[] = []; + res.on('data', chunk => chunks.push(chunk)); + res.on('end', () => resolve(Buffer.concat(chunks))); + } + }); + req.on('error', reject); + }); +} diff --git a/src/spec-utils/pfs.ts b/src/spec-utils/pfs.ts new file mode 100644 index 00000000..9fd36ebc --- /dev/null +++ b/src/spec-utils/pfs.ts @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as fs from 'fs'; +import { promisify } from 'util'; +import * as path from 'path'; + +import { URI } from 'vscode-uri'; + +export function isLocalFile(filepath: string): Promise { + return new Promise(r => fs.stat(filepath, (err, stat) => r(!err && stat.isFile()))); +} + +export function isLocalFolder(filepath: string): Promise { + return new Promise(r => fs.stat(filepath, (err, stat) => r(!err && stat.isDirectory()))); +} + +export const readLocalFile = promisify(fs.readFile); +export const writeLocalFile = promisify(fs.writeFile); +export const appendLocalFile = promisify(fs.appendFile); +export const renameLocal = promisify(fs.rename); +export const readLocalDir = promisify(fs.readdir); +export const unlinkLocal = promisify(fs.unlink); +export const mkdirpLocal = (path: string) => new Promise((res, rej) => fs.mkdir(path, { recursive: true }, err => err ? rej(err) : res())); +export const rmdirLocal = promisify(fs.rmdir); +export const rmLocal = promisify(fs.rm); +export const cpLocal = promisify(fs.copyFile); + +export interface FileHost { + platform: NodeJS.Platform; + path: typeof path.posix | typeof path.win32; + isFile(filepath: string): Promise; + readFile(filepath: string): Promise; + writeFile(filepath: string, content: Buffer): Promise; + readDir(dirpath: string): Promise; + readDirWithTypes?(dirpath: string): Promise<[string, FileTypeBitmask][]>; + mkdirp(dirpath: string): Promise; + toCommonURI(filePath: string): Promise; +} + +export enum FileTypeBitmask { + Unknown = 0, + File = 1, + Directory = 2, + SymbolicLink = 64 +} diff --git a/src/spec-utils/product.ts b/src/spec-utils/product.ts new file mode 100644 index 00000000..c9d7e85f --- /dev/null +++ b/src/spec-utils/product.ts @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as path from 'path'; +import { readLocalFile } from './pfs'; + +export interface PackageConfiguration { + name: string; + publisher?: string; + version: string; + aiKey?: string; +} + +export async function getPackageConfig(packageFolder: string): Promise { + const raw = await readLocalFile(path.join(packageFolder, 'package.json'), 'utf8'); + return JSON.parse(raw); +} +export const includeAllConfiguredFeatures = true; diff --git a/src/spec-utils/tsconfig.json b/src/spec-utils/tsconfig.json new file mode 100644 index 00000000..8e412c0e --- /dev/null +++ b/src/spec-utils/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.base.json" +} \ No newline at end of file diff --git a/src/spec-utils/types.ts b/src/spec-utils/types.ts new file mode 100644 index 00000000..034f1857 --- /dev/null +++ b/src/spec-utils/types.ts @@ -0,0 +1,6 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export type UnpackPromise = T extends Promise ? U : T; diff --git a/src/spec-utils/workspaces.ts b/src/spec-utils/workspaces.ts new file mode 100644 index 00000000..43ce6bfe --- /dev/null +++ b/src/spec-utils/workspaces.ts @@ -0,0 +1,106 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as path from 'path'; + +import { parse } from 'jsonc-parser'; +import { URI } from 'vscode-uri'; // avoid vscode.Uri reference for tests + +export interface Workspace { + readonly isWorkspaceFile: boolean; + readonly workspaceOrFolderPath: string; + readonly rootFolderPath: string; + readonly configFolderPath: string; +} + +export function workspaceFromPath(path_: typeof path.posix | typeof path.win32, workspaceOrFolderPath: string): Workspace { + if (isWorkspacePath(workspaceOrFolderPath)) { + const workspaceFolder = path_.dirname(workspaceOrFolderPath); + return { + isWorkspaceFile: true, + workspaceOrFolderPath, + rootFolderPath: workspaceFolder, // use workspaceFolder as root folder + configFolderPath: workspaceFolder, // have config file in workspaceFolder (to be discussed...) + }; + } + return { + isWorkspaceFile: false, + workspaceOrFolderPath, + rootFolderPath: workspaceOrFolderPath, + configFolderPath: workspaceOrFolderPath, + }; +} + +export function isWorkspacePath(workspaceOrFolderPath: string) { + return path.extname(workspaceOrFolderPath) === '.code-workspace'; // TODO: Remove VS Code specific code. +} + +export async function canUseWorkspacePathInRemote(cliHost: { platform: NodeJS.Platform; path: typeof path.posix | typeof path.win32; readFile(filepath: string): Promise }, workspace: Workspace): Promise { + if (!workspace.isWorkspaceFile) { + return undefined; + } + try { + const rootFolder = workspace.rootFolderPath; + const workspaceFileContent = (await cliHost.readFile(workspace.workspaceOrFolderPath)).toString(); + const workspaceFile = parse(workspaceFileContent); + const folders = workspaceFile['folders']; + if (folders && folders.length > 0) { + for (const folder of folders) { + const folderPath = folder['path']; + let fullPath; + if (!folderPath) { + const folderURI = folder['uri']; + if (!folderURI) { + return `Workspace contains a folder that defines neither a path nor a URI.`; + } + const uri = URI.parse(folderURI); + if (uri.scheme !== 'file') { + return `Workspace contains folder '${folderURI}' not on the local file system.`; + } + return `Workspace contains an absolute folder path '${folderURI}'.`; + } else { + if (cliHost.path.isAbsolute(folderPath)) { + return `Workspace contains an absolute folder path '${folderPath}'.`; + } + fullPath = cliHost.path.resolve(rootFolder, folderPath); + } + if (!isEqualOrParent(cliHost, fullPath, rootFolder)) { + return `Folder '${fullPath}' is not a subfolder of shared root folder '${rootFolder}'.`; + } + } + return; + } + return `Workspace does not define any folders`; + } catch (e) { + return `Problems loading workspace file ${workspace.workspaceOrFolderPath}: ${e && (e.message || e.toString())}`; + } +} + +export function isEqualOrParent(cliHost: { platform: NodeJS.Platform; path: typeof path.posix | typeof path.win32 }, c: string, parent: string): boolean { + if (c === parent) { + return true; + } + + if (!c || !parent) { + return false; + } + + if (parent.length > c.length) { + return false; + } + + if (c.length > parent.length && c.charAt(parent.length) !== cliHost.path.sep) { + return false; + } + + return equalPaths(cliHost.platform, parent, c.substr(0, parent.length)); +} + +function equalPaths(platform: NodeJS.Platform, a: string, b: string) { + if (platform === 'linux') { + return a === b; + } + return a.toLowerCase() === b.toLowerCase(); +} diff --git a/src/test/cli.test.ts b/src/test/cli.test.ts new file mode 100644 index 00000000..06fe4a28 --- /dev/null +++ b/src/test/cli.test.ts @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import * as path from 'path'; +import * as cp from 'child_process'; + +const pkg = require('../../package.json'); + +describe('Dev Containers CLI', function () { + this.timeout(1 * 60 * 1000); + + const tmp = path.relative(process.cwd(), path.join(__dirname, 'tmp')); + const cli = `npx --prefix ${tmp} dev-containers-cli`; + + before('Install', async () => { + await shellExec(`rm -rf ${tmp}/node_modules`); + await shellExec(`mkdir -p ${tmp}`); + await shellExec(`npm --prefix ${tmp} install dev-containers-cli-${pkg.version}.tgz`); + }); + + it('Global --help', async () => { + const res = await shellExec(`${cli} --help`); + assert.ok(res.stdout.indexOf('run-user-commands'), 'Help text is not mentioning run-user-commands.'); + }); + + it('Command up', async () => { + const res = await shellExec(`${cli} up --workspace-folder ${__dirname}/configs/image`); + const containerId: string = JSON.parse(res.stdout).containerId; + assert.ok(containerId, 'Container id not found.'); + await shellExec(`docker rm -f ${containerId}`); + }); +}); + +interface ExecResult { + error: Error | null; + stdout: string; + stderr: string; +} + +function shellExec(command: string, options: cp.ExecOptions = {}) { + return new Promise((resolve, reject) => { + cp.exec(command, options, (error, stdout, stderr) => { + console.log(stdout); + console.error(stderr); + (error ? reject : resolve)({ error, stdout, stderr }); + }); + }); +} diff --git a/src/test/configs/image/.devcontainer.json b/src/test/configs/image/.devcontainer.json new file mode 100644 index 00000000..e35d6aee --- /dev/null +++ b/src/test/configs/image/.devcontainer.json @@ -0,0 +1,3 @@ +{ + "image": "ubuntu:latest" +} diff --git a/src/test/container-features/example-features-sets/simple/devcontainer-features.json b/src/test/container-features/example-features-sets/simple/devcontainer-features.json new file mode 100644 index 00000000..04de6e52 --- /dev/null +++ b/src/test/container-features/example-features-sets/simple/devcontainer-features.json @@ -0,0 +1,58 @@ +{ + "features": + [ + { + "id": "first", + "name": "First Feature", + "containerEnv": { + "MYKEYONE": "MYRESULTONE" + }, + "options": { + "version": { + "type": "string", + "proposals": ["latest", "1.0", "2.0"], + "default": "latest", + "description": "Version option." + }, + "option1": { + "type": "boolean", + "default": true, + "description": "Boolean option." + }, + "option2": { + "type": "string", + "enum": ["yes", "no", "maybe"], + "default": "maybe", + "description": "Enum option." + } + } + }, + { + "id": "second", + "name": "Second Feature", + "privileged": true, + "options": { + "version": { + "type": "string", + "proposals": ["latest", "1.0", "2.0"], + "default": "latest", + "description": "Version option." + } + } + }, + { + "id": "third", + "name": "Third Feature", + "containerEnv": { + "MYKEYTHREE": "MYRESULTHREE" + }, + "options": { + "option1": { + "type": "boolean", + "default": true, + "description": "Boolean option." + } + } + } + ] +} \ No newline at end of file diff --git a/src/test/container-features/example-features-sets/simple/install.sh b/src/test/container-features/example-features-sets/simple/install.sh new file mode 100644 index 00000000..f0e75c64 --- /dev/null +++ b/src/test/container-features/example-features-sets/simple/install.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +echo 'test123' \ No newline at end of file diff --git a/src/test/container-features/generateFeaturesConfig.offline.test.ts b/src/test/container-features/generateFeaturesConfig.offline.test.ts new file mode 100644 index 00000000..f72ad300 --- /dev/null +++ b/src/test/container-features/generateFeaturesConfig.offline.test.ts @@ -0,0 +1,96 @@ +import { assert } from 'chai'; +import { generateFeaturesConfig, getFeatureLayers } from '../../spec-configuration/containerFeaturesConfiguration'; +import { createPlainLog, LogLevel, makeLog } from '../../spec-utils/log'; +import * as os from 'os'; +import * as path from 'path'; +import { mkdirpLocal } from '../../spec-utils/pfs'; +import { DevContainerConfig } from '../../spec-configuration/configuration'; +import { URI } from 'vscode-uri'; + +export const output = makeLog(createPlainLog(text => process.stdout.write(text), () => LogLevel.Trace)); + +// Test fetching/generating the devcontainer-features.json config +describe('validate (offline) generateFeaturesConfig()', function () { + + // Setup + const env = { 'SOME_KEY': 'SOME_VAL'}; + const params = { extensionPath: '', output, env, persistedFolder: '' }; + + // Mocha executes with the root of the project as the cwd. + const localFeaturesFolder = (_: string) => { + return './src/test/container-features/example-features-sets/simple'; + }; + + const labels = async () => { + const record: Record = { + 'com.visualstudio.code.devcontainers.id': 'ubuntu', + 'com.visualstudio.code.devcontainers.release': 'v0.194.2', + 'com.visualstudio.code.devcontainers.source': 'https://github.com/microsoft/vscode-dev-containers/', + 'com.visualstudio.code.devcontainers.timestamp': 'Fri, 03 Sep 2021 03:00:16 GMT', + 'com.visualstudio.code.devcontainers.variant': 'focal', + }; + return record; + }; + + it('should correctly return a featuresConfig with just local features', async function () { + + const version = 'unittest'; + const tmpFolder: string = path.join(os.tmpdir(), 'vsch', 'container-features', `${version}-${Date.now()}`); + await mkdirpLocal(tmpFolder); + + const config: DevContainerConfig = { + configFilePath: URI.from({ 'scheme': 'https' }), + dockerFile: '.', + features: { + first: { + 'version': 'latest', + 'option1': true + }, + third: 'latest' + }, + }; + + const featuresConfig = await generateFeaturesConfig(params, tmpFolder, config, labels, localFeaturesFolder); + if (!featuresConfig) { + assert.fail(); + } + + const localFeatureSet = (featuresConfig?.featureSets.find(set => set.sourceInformation.type === 'local-cache')); + assert.exists(localFeatureSet); + assert.strictEqual(localFeatureSet?.features.length, 3); + + const first = localFeatureSet?.features.find((f) => f.id === 'first'); + assert.exists(first); + + const second = localFeatureSet?.features.find((f) => f.id === 'second'); + assert.exists(second); + + const third = localFeatureSet?.features.find((f) => f.id === 'third'); + assert.exists(third); + + assert.isObject(first?.value); + assert.isBoolean(second?.value); + assert.isString(third?.value); + + // -- Test containerFeatures.ts helper functions + + // generateContainerEnvs +// TODO +// const actualEnvs = generateContainerEnvs(featuresConfig); +// const expectedEnvs = `ENV MYKEYONE=MYRESULTONE +// ENV MYKEYTHREE=MYRESULTHREE`; +// assert.strictEqual(actualEnvs, expectedEnvs); + + // getFeatureLayers + const actualLayers = await getFeatureLayers(featuresConfig); + const expectedLayers = `RUN cd /tmp/build-features/local-cache \\ +&& chmod +x ./install.sh \\ +&& ./install.sh + +`; + assert.strictEqual(actualLayers, expectedLayers); + }); + + + +}); diff --git a/src/test/container-features/generateFeaturesConfig.online.test.ts b/src/test/container-features/generateFeaturesConfig.online.test.ts new file mode 100644 index 00000000..02aa9e4c --- /dev/null +++ b/src/test/container-features/generateFeaturesConfig.online.test.ts @@ -0,0 +1,111 @@ +import { assert } from 'chai'; +import { generateFeaturesConfig } from '../../spec-configuration/containerFeaturesConfiguration'; +import { createPlainLog, LogLevel, makeLog } from '../../spec-utils/log'; +import * as os from 'os'; +import * as path from 'path'; +import { mkdirpLocal } from '../../spec-utils/pfs'; +import { DevContainerConfig } from '../../spec-configuration/configuration'; +import { URI } from 'vscode-uri'; + +export const output = makeLog(createPlainLog(text => process.stdout.write(text), () => LogLevel.Trace)); + +// Test fetching/generating the devcontainer-features.json config +describe('validate online functionality of generateFeaturesConfig() ', function () { + + // Setup + const env = { 'SOME_KEY': 'SOME_VAL' }; + const params = { extensionPath: '', output, env, persistedFolder: '' }; + + // Mocha executes with the root of the project as the cwd. + const localFeaturesFolder = (_: string) => { + return './src/test/container-features/example-features-sets/simple'; + }; + + const labels = async () => { + const record: Record = { + 'com.visualstudio.code.devcontainers.id': 'ubuntu', + 'com.visualstudio.code.devcontainers.release': 'v0.194.2', + 'com.visualstudio.code.devcontainers.source': 'https://github.com/microsoft/vscode-dev-containers/', + 'com.visualstudio.code.devcontainers.timestamp': 'Fri, 03 Sep 2021 03:00:16 GMT', + 'com.visualstudio.code.devcontainers.variant': 'focal', + }; + return record; + }; + + it('should correct return a featuresConfig fetched from a remote tgz', async function() { + const version = 'unittest2'; + const tmpFolder: string = path.join(os.tmpdir(), 'vsch', 'container-features', `${version}-${Date.now()}`); + await mkdirpLocal(tmpFolder); + + const config: DevContainerConfig = { + configFilePath: URI.from({ 'scheme': 'https' }), + dockerFile: '.', + features: { + 'https://github.com/codspace/myfeatures/releases/latest/download/devcontainer-features.tgz#helloworld': { + 'greeting': 'howdy' + }, + third: 'latest' + }, + }; + const featuresConfig = await generateFeaturesConfig(params, tmpFolder, config, labels, localFeaturesFolder); + + assert.exists(featuresConfig); + + assert.strictEqual(featuresConfig?.featureSets + .map(x => x.features) + .flat() + .length, 4); + + // Get the sets + const localSet = featuresConfig?.featureSets.find(x => x.sourceInformation.type === 'local-cache'); + assert.exists(localSet); + assert.exists(localSet?.features.find(x => x.id === 'third')); + const tarballSet = featuresConfig?.featureSets.find(x => x.sourceInformation.type === 'direct-tarball'); + assert.exists(tarballSet); + assert.exists(tarballSet?.features.find(x => x.id === 'helloworld')); + }); + + it('should correctly return a featuresConfig with github-hosted remote features from two remote repos', async function () { + + const version = 'unittest3'; + const tmpFolder: string = path.join(os.tmpdir(), 'vsch', 'container-features', `${version}-${Date.now()}`); + await mkdirpLocal(tmpFolder); + + const config: DevContainerConfig = { + configFilePath: URI.from({ 'scheme': 'https' }), + dockerFile: '.', + features: { + 'codspace/myfeatures/helloworld': { + 'greeting': 'howdy' + }, + 'codspace/myotherfeatures/helloworld': { + 'greeting': 'heythere' + }, + third: 'latest' + }, + }; + + const featuresConfig = await generateFeaturesConfig(params, tmpFolder, config, labels, localFeaturesFolder); + + assert.exists(featuresConfig); + // 3 local features + 1 from codspace/myfeatures + 1 from codspace/myotherfeatures == 5 + assert.strictEqual(featuresConfig?.featureSets + .map(x => x.features) + .flat() + .length, 5); + + // Get the sets + const localSet = featuresConfig?.featureSets.find(x => x.sourceInformation.type === 'local-cache'); + assert.exists(localSet); + const myfeaturesSet = featuresConfig?.featureSets + .find(x => + x.sourceInformation.type === 'github-repo' && + x.sourceInformation.repo === 'myfeatures'); + assert.exists(myfeaturesSet); + const myotherFeaturesSet = featuresConfig?.featureSets + .find(x => + x.sourceInformation.type === 'github-repo' && + x.sourceInformation.repo === 'myotherfeatures'); + assert.exists(myotherFeaturesSet); + }); +}); \ No newline at end of file diff --git a/src/test/container-features/helpers.offline.test.ts b/src/test/container-features/helpers.offline.test.ts new file mode 100644 index 00000000..3f974c97 --- /dev/null +++ b/src/test/container-features/helpers.offline.test.ts @@ -0,0 +1,161 @@ +import { assert } from 'chai'; +import { getSourceInfoString, parseFeatureIdentifier, SourceInformation } from '../../spec-configuration/containerFeaturesConfiguration'; +import { createPlainLog, LogLevel, makeLog } from '../../spec-utils/log'; +export const output = makeLog(createPlainLog(text => process.stdout.write(text), () => LogLevel.Trace)); + +describe('validate function parseRemoteFeatureToDownloadUri', function () { + + // -- Valid + + it('should parse local features and return an undefined tarballUrl', async function () { + const result = parseFeatureIdentifier('helloworld', output); + assert.exists(result); + assert.strictEqual(result?.id, 'helloworld'); + assert.strictEqual(result?.sourceInformation.type, 'local-cache'); + }); + + it('should parse gitHub without version', async function () { + const result = parseFeatureIdentifier('octocat/myfeatures/helloworld', output); + assert.exists(result); + assert.strictEqual(result?.id, 'helloworld'); + assert.deepEqual(result?.sourceInformation, { type: 'github-repo', + owner: 'octocat', + repo: 'myfeatures', + apiUri: 'https://api.github.com/repos/octocat/myfeatures/releases/latest', + unauthenticatedUri: 'https://github.com/octocat/myfeatures/releases/latest/download/devcontainer-features.tgz', + isLatest: true + }); + }); + + it('should parse gitHub with version', async function () { + const result = parseFeatureIdentifier('octocat/myfeatures/helloworld@v0.0.4', output); + assert.exists(result); + assert.strictEqual(result?.id, 'helloworld'); + assert.deepEqual(result?.sourceInformation, { type: 'github-repo', + owner: 'octocat', + repo: 'myfeatures', + tag: 'v0.0.4', + apiUri: 'https://api.github.com/repos/octocat/myfeatures/releases/tags/v0.0.4', + unauthenticatedUri: 'https://github.com/octocat/myfeatures/releases/download/v0.0.4/devcontainer-features.tgz', + isLatest: false + }); + }); + + it('should parse generic tar', async function () { + const result = parseFeatureIdentifier('https://example.com/some/long/path/devcontainer-features.tgz#helloworld', output); + assert.exists(result); + assert.strictEqual(result?.id, 'helloworld'); + assert.deepEqual(result?.sourceInformation, { type: 'direct-tarball', tarballUri: 'https://example.com/some/long/path/devcontainer-features.tgz' }); + }); + + it('should parse when provided a local-filesystem relative path', async function () { + const result = parseFeatureIdentifier('./some/long/path/to/features#helloworld', output); + assert.notExists(result); + // assert.exists(result); + // assert.strictEqual(result?.id, 'helloworld'); + // assert.deepEqual(result?.sourceInformation, { type: 'file-path', filePath: './some/long/path/to/features', isRelative: true }); + }); + + it('should parse when provided a local-filesystem relative path, starting with ../', async function () { + const result = parseFeatureIdentifier('../some/long/path/to/features#helloworld', output); + assert.notExists(result); + // assert.exists(result); + // assert.strictEqual(result?.id, 'helloworld'); + // assert.deepEqual(result?.sourceInformation, { type: 'file-path', filePath: '../some/long/path/to/features', isRelative: true }); + }); + + it('should parse when provided a local-filesystem absolute path', async function () { + const result = parseFeatureIdentifier('/some/long/path/to/features#helloworld', output); + assert.notExists(result); + // assert.exists(result); + // assert.strictEqual(result?.id, 'helloworld'); + // assert.deepEqual(result?.sourceInformation, { type: 'file-path', filePath: '/some/long/path/to/features', isRelative: false }); + }); + + + // -- Invalid + + it('should fail parsing a generic tar with no feature and trailing slash', async function () { + const result = parseFeatureIdentifier('https://example.com/some/long/path/devcontainer-features.tgz/', output); + assert.notExists(result); + }); + + it('should not parse gitHub without triple slash', async function () { + const result = parseFeatureIdentifier('octocat/myfeatures#helloworld', output); + assert.notExists(result); + }); + + it('should fail parsing a generic tar with no feature and no trailing slash', async function () { + const result = parseFeatureIdentifier('https://example.com/some/long/path/devcontainer-features.tgz', output); + assert.notExists(result); + }); + + it('should fail parsing a generic tar with a hash but no feature', async function () { + const result = parseFeatureIdentifier('https://example.com/some/long/path/devcontainer-features.tgz#', output); + assert.notExists(result); + }); + + it('should fail parsing a marketplace shorthand with only two segments and a hash with no feature', async function () { + const result = parseFeatureIdentifier('octocat/myfeatures#', output); + assert.notExists(result); + }); + + it('should fail parsing a marketplace shorthand with only two segments (no feature)', async function () { + const result = parseFeatureIdentifier('octocat/myfeatures', output); + assert.notExists(result); + }); + + it('should fail parsing a marketplace shorthand with an invalid feature name (1)', async function () { + const result = parseFeatureIdentifier('octocat/myfeatures/@mycoolfeature', output); + assert.notExists(result); + }); + + it('should fail parsing a marketplace shorthand with an invalid feature name (2)', async function () { + const result = parseFeatureIdentifier('octocat/myfeatures/MY_$UPER_COOL_FEATURE', output); + assert.notExists(result); + }); + + it('should fail parsing a marketplace shorthand with only two segments, no hash, and with a version', async function () { + const result = parseFeatureIdentifier('octocat/myfeatures@v0.0.1', output); + assert.notExists(result); + }); +}); + + +describe('validate function getSourceInfoString', function () { + + it('should work for local-cache', async function () { + const srcInfo: SourceInformation = { + type : 'local-cache' + }; + const output = getSourceInfoString(srcInfo); + assert.strictEqual(output, 'local-cache'); + }); + + it('should work for github-repo without a tag (implicit latest)', async function () { + const srcInfo: SourceInformation = { + type : 'github-repo', + owner: 'bob', + repo: 'mobileapp', + isLatest: true, + apiUri: 'https://api.github.com/repos/bob/mobileapp/releases/latest', + unauthenticatedUri: 'https://github.com/bob/mobileapp/releases/latest/download/devcontainer-features.tgz' + }; + const output = getSourceInfoString(srcInfo); + assert.strictEqual(output, 'github-bob-mobileapp-latest'); + }); + + it('should work for github-repo with a tag', async function () { + const srcInfo: SourceInformation = { + type : 'github-repo', + owner: 'bob', + repo: 'mobileapp', + tag: 'v0.0.4', + isLatest: false, + apiUri: 'https://api.github.com/repos/bob/mobileapp/releases/tags/v0.0.4', + unauthenticatedUri: 'https://github.com/bob/mobileapp/releases/download/v0.0.4/devcontainer-features.tgz' + }; + const output = getSourceInfoString(srcInfo); + assert.strictEqual(output, 'github-bob-mobileapp-v0.0.4'); + }); +}); \ No newline at end of file diff --git a/src/test/tsconfig.json b/src/test/tsconfig.json new file mode 100644 index 00000000..915d6c22 --- /dev/null +++ b/src/test/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "resolveJsonModule": true + }, + "references": [ + { + "path": "../spec-configuration" + }, + { + "path": "../spec-utils" + } + ] +} \ No newline at end of file diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 00000000..77b55edf --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "composite": true, + "rootDir": "./src", + "outDir": "./dist", + "target": "es2019", + "module": "commonjs", + "esModuleInterop": true, + "strict": true, + "alwaysStrict": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "useUnknownInCatchVariables": false, + "newLine": "LF" + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..7914116f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "references": [ + { + "path": "./src/spec-common" + }, + { + "path": "./src/spec-configuration" + }, + { + "path": "./src/spec-node" + }, + { + "path": "./src/spec-shutdown" + }, + { + "path": "./src/spec-utils" + } + ], + "files": [] +} \ No newline at end of file diff --git a/tsfmt.json b/tsfmt.json new file mode 100644 index 00000000..72a3195d --- /dev/null +++ b/tsfmt.json @@ -0,0 +1,16 @@ +{ + "tabSize": 4, + "indentSize": 4, + "convertTabsToSpaces": false, + "insertSpaceAfterCommaDelimiter": true, + "insertSpaceAfterSemicolonInForStatements": true, + "insertSpaceBeforeAndAfterBinaryOperators": true, + "insertSpaceAfterKeywordsInControlFlowStatements": true, + "insertSpaceAfterFunctionKeywordForAnonymousFunctions": true, + "insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": false, + "insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": false, + "insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": false, + "insertSpaceBeforeFunctionParenthesis": false, + "placeOpenBraceOnNewLineForFunctions": false, + "placeOpenBraceOnNewLineForControlBlocks": false +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 00000000..92c5a9ad --- /dev/null +++ b/yarn.lock @@ -0,0 +1,3168 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/code-frame@7.12.11": + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" + integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw== + dependencies: + "@babel/highlight" "^7.10.4" + +"@babel/code-frame@^7.0.0": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658" + integrity sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g== + dependencies: + "@babel/highlight" "^7.12.13" + +"@babel/helper-validator-identifier@^7.12.11": + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed" + integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw== + +"@babel/highlight@^7.10.4", "@babel/highlight@^7.12.13": + version "7.13.10" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.13.10.tgz#a8b2a66148f5b27d666b15d81774347a731d52d1" + integrity sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg== + dependencies: + "@babel/helper-validator-identifier" "^7.12.11" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@cspotcode/source-map-consumer@0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b" + integrity sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg== + +"@cspotcode/source-map-support@0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz#4789840aa859e46d2f3173727ab707c66bf344f5" + integrity sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA== + dependencies: + "@cspotcode/source-map-consumer" "0.8.0" + +"@eslint/eslintrc@^0.4.3": + version "0.4.3" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c" + integrity sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw== + dependencies: + ajv "^6.12.4" + debug "^4.1.1" + espree "^7.3.0" + globals "^13.9.0" + ignore "^4.0.6" + import-fresh "^3.2.1" + js-yaml "^3.13.1" + minimatch "^3.0.4" + strip-json-comments "^3.1.1" + +"@humanwhocodes/config-array@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9" + integrity sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg== + dependencies: + "@humanwhocodes/object-schema" "^1.2.0" + debug "^4.1.1" + minimatch "^3.0.4" + +"@humanwhocodes/object-schema@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz#87de7af9c231826fdd68ac7258f77c429e0e5fcf" + integrity sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w== + +"@nodelib/fs.scandir@2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz#d4b3549a5db5de2683e0c1071ab4f140904bbf69" + integrity sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA== + dependencies: + "@nodelib/fs.stat" "2.0.4" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.4", "@nodelib/fs.stat@^2.0.2": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz#a3f2dd61bab43b8db8fa108a121cfffe4c676655" + integrity sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz#cce9396b30aa5afe9e3756608f5831adcb53d063" + integrity sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow== + dependencies: + "@nodelib/fs.scandir" "2.1.4" + fastq "^1.6.0" + +"@tsconfig/node10@^1.0.7": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9" + integrity sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg== + +"@tsconfig/node12@^1.0.7": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.8.tgz#a883d62f049a64fea1e56a6bbe66828d11c6241b" + integrity sha512-LM6XwBhjZRls1qJGpiM/It09SntEwe9M0riXRfQ9s6XlJQG0JPGl92ET18LtGeYh/GuOtafIXqwZeqLOd0FNFQ== + +"@tsconfig/node14@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.1.tgz#95f2d167ffb9b8d2068b0b235302fafd4df711f2" + integrity sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg== + +"@tsconfig/node16@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e" + integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA== + +"@types/chai@^4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.0.tgz#23509ebc1fa32f1b4d50d6a66c4032d5b8eaabdc" + integrity sha512-/ceqdqeRraGolFTcfoXNiqjyQhZzbINDngeoAq9GoHa8PPK1yNzTaxWjA6BFWp5Ua9JpXEMSS4s5i9tS0hOJtw== + +"@types/follow-redirects@^1.13.1": + version "1.13.1" + resolved "https://registry.yarnpkg.com/@types/follow-redirects/-/follow-redirects-1.13.1.tgz#b3dde0c7fcff69c497c99daab21a8b366f09270a" + integrity sha512-WPzi4QUu0rXeRmcssiXmNVCSbV9elxQJoVfp1N4SThfbKAKEo/xrhzpZVVk/XSCzbwUg70W8wEUC/TCR05QMBQ== + dependencies: + "@types/node" "*" + +"@types/js-yaml@^4.0.5": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.5.tgz#738dd390a6ecc5442f35e7f03fa1431353f7e138" + integrity sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA== + +"@types/json-schema@^7.0.7": + version "7.0.7" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad" + integrity sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA== + +"@types/minimatch@^3.0.3": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.4.tgz#f0ec25dbf2f0e4b18647313ac031134ca5b24b21" + integrity sha512-1z8k4wzFnNjVK/tlxvrWuK5WMt6mydWWP7+zvH5eFep4oj+UkrfiJTRtjCeBXNpwaA/FYqqtb4/QS4ianFpIRA== + +"@types/minipass@*": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@types/minipass/-/minipass-2.2.0.tgz#51ad404e8eb1fa961f75ec61205796807b6f9651" + integrity sha512-wuzZksN4w4kyfoOv/dlpov4NOunwutLA/q7uc00xU02ZyUY+aoM5PWIXEKBMnm0NHd4a+N71BMjq+x7+2Af1fg== + dependencies: + "@types/node" "*" + +"@types/mocha@^9.1.0": + version "9.1.0" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.1.0.tgz#baf17ab2cca3fcce2d322ebc30454bff487efad5" + integrity sha512-QCWHkbMv4Y5U9oW10Uxbr45qMMSzl4OzijsozynUAgx3kEHUdXB00udx2dWDQ7f2TU2a2uuiFaRZjCe3unPpeg== + +"@types/node@*": + version "14.14.41" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.41.tgz#d0b939d94c1d7bd53d04824af45f1139b8c45615" + integrity sha512-dueRKfaJL4RTtSa7bWeTK1M+VH+Gns73oCgzvYfHZywRCoPSd8EkXBL0mZ9unPTveBn+D9phZBaxuzpwjWkW0g== + +"@types/node@^16.11.7": + version "16.11.21" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.21.tgz#474d7589a30afcf5291f59bd49cca9ad171ffde4" + integrity sha512-Pf8M1XD9i1ksZEcCP8vuSNwooJ/bZapNmIzpmsMaL+jMI+8mEYU3PKvs+xDNuQcJWF/x24WzY4qxLtB0zNow9A== + +"@types/pull-stream@^3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@types/pull-stream/-/pull-stream-3.6.2.tgz#184165017b0764b9a44aff0b555c795ad6cdf9f9" + integrity sha512-s5jYmaJH68IQb9JjsemWUZCpaQdotd7B4xfXQtcKvGmQxcBXD/mvSQoi3TzPt2QqpDLjImxccS4en8f8E8O0FA== + +"@types/semver@^7.3.9": + version "7.3.9" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.9.tgz#152c6c20a7688c30b967ec1841d31ace569863fc" + integrity sha512-L/TMpyURfBkf+o/526Zb6kd/tchUP3iBDEPjqjb+U2MAJhVRxxrmr2fwpe08E7QsV7YLcpq0tUaQ9O9x97ZIxQ== + +"@types/shell-quote@^1.7.1": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@types/shell-quote/-/shell-quote-1.7.1.tgz#2d059091214a02c29f003f591032172b2aff77e8" + integrity sha512-SWZ2Nom1pkyXCDohRSrkSKvDh8QOG9RfAsrt5/NsPQC4UQJ55eG0qClA40I+Gkez4KTQ0uDUT8ELRXThf3J5jw== + +"@types/tar@^6.1.1": + version "6.1.1" + resolved "https://registry.yarnpkg.com/@types/tar/-/tar-6.1.1.tgz#ab341ec1f149d7eb2a4f4ded56ff85f0d4fe7cb5" + integrity sha512-8mto3YZfVpqB1CHMaYz1TUYIQfZFbh/QbEq5Hsn6D0ilCfqRVCdalmc89B7vi3jhl9UYIk+dWDABShNfOkv5HA== + dependencies: + "@types/minipass" "*" + "@types/node" "*" + +"@types/yargs-parser@*": + version "20.2.0" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.0.tgz#dd3e6699ba3237f0348cd085e4698780204842f9" + integrity sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA== + +"@types/yargs@^17.0.8": + version "17.0.8" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.8.tgz#d23a3476fd3da8a0ea44b5494ca7fa677b9dad4c" + integrity sha512-wDeUwiUmem9FzsyysEwRukaEdDNcwbROvQ9QGRKaLI6t+IltNzbn4/i4asmB10auvZGQCzSQ6t0GSczEThlUXw== + dependencies: + "@types/yargs-parser" "*" + +"@typescript-eslint/eslint-plugin@^4.31.2": + version "4.31.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.31.2.tgz#9f41efaee32cdab7ace94b15bd19b756dd099b0a" + integrity sha512-w63SCQ4bIwWN/+3FxzpnWrDjQRXVEGiTt9tJTRptRXeFvdZc/wLiz3FQUwNQ2CVoRGI6KUWMNUj/pk63noUfcA== + dependencies: + "@typescript-eslint/experimental-utils" "4.31.2" + "@typescript-eslint/scope-manager" "4.31.2" + debug "^4.3.1" + functional-red-black-tree "^1.0.1" + regexpp "^3.1.0" + semver "^7.3.5" + tsutils "^3.21.0" + +"@typescript-eslint/experimental-utils@4.31.2": + version "4.31.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.31.2.tgz#98727a9c1e977dd5d20c8705e69cd3c2a86553fa" + integrity sha512-3tm2T4nyA970yQ6R3JZV9l0yilE2FedYg8dcXrTar34zC9r6JB7WyBQbpIVongKPlhEMjhQ01qkwrzWy38Bk1Q== + dependencies: + "@types/json-schema" "^7.0.7" + "@typescript-eslint/scope-manager" "4.31.2" + "@typescript-eslint/types" "4.31.2" + "@typescript-eslint/typescript-estree" "4.31.2" + eslint-scope "^5.1.1" + eslint-utils "^3.0.0" + +"@typescript-eslint/parser@^4.31.2": + version "4.31.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.31.2.tgz#54aa75986e3302d91eff2bbbaa6ecfa8084e9c34" + integrity sha512-EcdO0E7M/sv23S/rLvenHkb58l3XhuSZzKf6DBvLgHqOYdL6YFMYVtreGFWirxaU2mS1GYDby3Lyxco7X5+Vjw== + dependencies: + "@typescript-eslint/scope-manager" "4.31.2" + "@typescript-eslint/types" "4.31.2" + "@typescript-eslint/typescript-estree" "4.31.2" + debug "^4.3.1" + +"@typescript-eslint/scope-manager@4.31.2": + version "4.31.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.31.2.tgz#1d528cb3ed3bcd88019c20a57c18b897b073923a" + integrity sha512-2JGwudpFoR/3Czq6mPpE8zBPYdHWFGL6lUNIGolbKQeSNv4EAiHaR5GVDQaLA0FwgcdcMtRk+SBJbFGL7+La5w== + dependencies: + "@typescript-eslint/types" "4.31.2" + "@typescript-eslint/visitor-keys" "4.31.2" + +"@typescript-eslint/types@4.31.2": + version "4.31.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.31.2.tgz#2aea7177d6d744521a168ed4668eddbd912dfadf" + integrity sha512-kWiTTBCTKEdBGrZKwFvOlGNcAsKGJSBc8xLvSjSppFO88AqGxGNYtF36EuEYG6XZ9vT0xX8RNiHbQUKglbSi1w== + +"@typescript-eslint/typescript-estree@4.31.2": + version "4.31.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.31.2.tgz#abfd50594d8056b37e7428df3b2d185ef2d0060c" + integrity sha512-ieBq8U9at6PvaC7/Z6oe8D3czeW5d//Fo1xkF/s9394VR0bg/UaMYPdARiWyKX+lLEjY3w/FNZJxitMsiWv+wA== + dependencies: + "@typescript-eslint/types" "4.31.2" + "@typescript-eslint/visitor-keys" "4.31.2" + debug "^4.3.1" + globby "^11.0.3" + is-glob "^4.0.1" + semver "^7.3.5" + tsutils "^3.21.0" + +"@typescript-eslint/visitor-keys@4.31.2": + version "4.31.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.31.2.tgz#7d5b4a4705db7fe59ecffb273c1d082760f635cc" + integrity sha512-PrBId7EQq2Nibns7dd/ch6S6/M4/iwLM9McbgeEbCXfxdwRUNxJ4UNreJ6Gh3fI2GNKNrWnQxKL7oCPmngKBug== + dependencies: + "@typescript-eslint/types" "4.31.2" + eslint-visitor-keys "^2.0.0" + +"@ungap/promise-all-settled@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44" + integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== + +acorn-jsx@^5.2.0, acorn-jsx@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" + integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng== + +acorn-walk@^8.1.1: + version "8.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" + integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== + +acorn@^7.1.1, acorn@^7.4.0: + version "7.4.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" + integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== + +acorn@^8.4.1: + version "8.5.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.5.0.tgz#4512ccb99b3698c752591e9bb4472e38ad43cee2" + integrity sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q== + +aggregate-error@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-4.0.0.tgz#83dbdb53a0d500721281d22e19eee9bc352a89cd" + integrity sha512-8DGp7zUt1E9k0NE2q4jlXHk+V3ORErmwolEdRz9iV+LKJ40WhMHh92cxAvhqV2I+zEn/gotIoqoMs0NjF3xofg== + dependencies: + clean-stack "^4.0.0" + indent-string "^5.0.0" + +ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + 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@^8.0.1: + version "8.1.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.1.0.tgz#45d5d3d36c7cdd808930cc3e603cf6200dbeb736" + integrity sha512-B/Sk2Ix7A36fs/ZkuGLIR86EdjbgR6fsAcbx9lOP/QBSXujDNbVmIS/U4Itz5k8fPFDeVZl/zQ/gJW4Jrq6XjQ== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + +ansi-colors@4.1.1, ansi-colors@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + +ansi-colors@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-1.1.0.tgz#6374b4dd5d4718ff3ce27a671a3b1cad077132a9" + integrity sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA== + dependencies: + ansi-wrap "^0.1.0" + +ansi-escapes@^4.2.1: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + +ansi-gray@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ansi-gray/-/ansi-gray-0.1.1.tgz#2962cf54ec9792c48510a3deb524436861ef7251" + integrity sha1-KWLPVOyXksSFEKPetSRDaGHvclE= + dependencies: + ansi-wrap "0.1.0" + +ansi-regex@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" + integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== + +ansi-regex@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^3.2.0, ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-wrap@0.1.0, ansi-wrap@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf" + integrity sha1-qCJQ3bABXponyoLoLqYDu/pF768= + +anymatch@~3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" + integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +append-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/append-buffer/-/append-buffer-1.0.2.tgz#d8220cf466081525efea50614f3de6514dfa58f1" + integrity sha1-2CIM9GYIFSXv6lBhTz3mUU36WPE= + dependencies: + buffer-equal "^1.0.0" + +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +arr-diff@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" + integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= + +arr-union@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" + integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= + +array-differ@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-3.0.0.tgz#3cbb3d0f316810eafcc47624734237d6aee4ae6b" + integrity sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg== + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +arrify@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" + integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== + +assertion-error@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" + integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== + +assign-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" + integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= + +astral-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" + integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== + +astral-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" + integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^3.0.1, braces@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +browser-stdout@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== + +buffer-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-1.0.0.tgz#59616b498304d556abd466966b22eeda3eca5fbe" + integrity sha1-WWFrSYME1Var1GaWayLu2j7KX74= + +call-bind@^1.0.0, call-bind@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camelcase@^6.0.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809" + integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== + +chai@^4.3.4: + version "4.3.6" + resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.6.tgz#ffe4ba2d9fa9d6680cc0b370adae709ec9011e9c" + integrity sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q== + dependencies: + assertion-error "^1.1.0" + check-error "^1.0.2" + deep-eql "^3.0.1" + get-func-name "^2.0.0" + loupe "^2.3.1" + pathval "^1.1.1" + type-detect "^4.0.5" + +chalk@^2.0.0, chalk@^2.1.0, chalk@^2.4.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^4.0.0, chalk@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" + integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chardet@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" + integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== + +check-error@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" + integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII= + +chokidar@3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + +clean-stack@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-4.1.0.tgz#5ce5a2fd19a12aecdce8570daefddb7ac94b6b4e" + integrity sha512-dxXQYI7mfQVcaF12s6sjNFoZ6ZPDQuBBLp3QJ5156k9EvUFClUoZ11fo8HnLQO241DDVntHEug8MOuFO5PSfRg== + dependencies: + escape-string-regexp "5.0.0" + +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.0" + +cli-width@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" + integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== + +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +clone-buffer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58" + integrity sha1-4+JbIHrE5wGvch4staFnksrD3Fg= + +clone-stats@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-1.0.0.tgz#b3782dff8bb5474e18b9b6bf0fdfe782f8777680" + integrity sha1-s3gt/4u1R04Yuba/D9/ngvh3doA= + +clone@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" + integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18= + +cloneable-readable@^1.0.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/cloneable-readable/-/cloneable-readable-1.1.3.tgz#120a00cb053bfb63a222e709f9683ea2e11d8cec" + integrity sha512-2EF8zTQOxYq70Y4XKtorQupqF0m49MBz2/yf5Bj+MHjvpG3Hy7sImifnqD6UA+TKYxeSV+u6qqQPawN5UvnpKQ== + dependencies: + inherits "^2.0.1" + process-nextick-args "^2.0.0" + readable-stream "^2.3.5" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +color-support@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" + integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== + +commander@^2.19.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +commandpost@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/commandpost/-/commandpost-1.4.0.tgz#89218012089dfc9b67a337ba162f15c88e0f1048" + integrity sha512-aE2Y4MTFJ870NuB/+2z1cXBhSBBzRydVVjzhFC4gtenEhpnj15yu0qptWGJsO9YGrcPZ3ezX8AWb1VA391MKpQ== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +convert-source-map@^1.5.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" + integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA== + dependencies: + safe-buffer "~5.1.1" + +copyfiles@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/copyfiles/-/copyfiles-2.4.1.tgz#d2dcff60aaad1015f09d0b66e7f0f1c5cd3c5da5" + integrity sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg== + dependencies: + glob "^7.0.5" + minimatch "^3.0.3" + mkdirp "^1.0.4" + noms "0.0.0" + through2 "^2.0.1" + untildify "^4.0.0" + yargs "^16.1.0" + +core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + +cross-spawn@^6.0.5: + version "6.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" + integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== + dependencies: + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" + +cross-spawn@^7.0.2: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +debug@4.3.3: + version "4.3.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" + integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== + dependencies: + ms "2.1.2" + +debug@^4.0.1, debug@^4.1.1, debug@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" + integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== + dependencies: + ms "2.1.2" + +decamelize@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" + integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== + +deep-eql@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df" + integrity sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw== + dependencies: + type-detect "^4.0.0" + +deep-is@^0.1.3, deep-is@~0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" + integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= + +define-properties@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" + integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== + dependencies: + object-keys "^1.0.12" + +diff@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" + integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== + +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +duplexer@^0.1.1, duplexer@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" + integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== + +duplexify@^3.6.0: + version "3.7.1" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" + integrity sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g== + dependencies: + end-of-stream "^1.0.0" + inherits "^2.0.1" + readable-stream "^2.0.0" + stream-shift "^1.0.0" + +editorconfig@^0.15.0: + version "0.15.3" + resolved "https://registry.yarnpkg.com/editorconfig/-/editorconfig-0.15.3.tgz#bef84c4e75fb8dcb0ce5cee8efd51c15999befc5" + integrity sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g== + dependencies: + commander "^2.19.0" + lru-cache "^4.1.5" + semver "^5.6.0" + sigmund "^1.0.1" + +emoji-regex@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" + integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +end-of-stream@^1.0.0, end-of-stream@^1.1.0: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +enquirer@^2.3.5: + version "2.3.6" + resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" + integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== + dependencies: + ansi-colors "^4.1.1" + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +es-abstract@^1.18.0-next.2: + version "1.18.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0.tgz#ab80b359eecb7ede4c298000390bc5ac3ec7b5a4" + integrity sha512-LJzK7MrQa8TS0ja2w3YNLzUgJCGPdPOV1yVvezjNnS89D+VR08+Szt2mz3YB2Dck/+w5tfIq/RoUAFqJJGM2yw== + dependencies: + call-bind "^1.0.2" + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + get-intrinsic "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.2" + is-callable "^1.2.3" + is-negative-zero "^2.0.1" + is-regex "^1.1.2" + is-string "^1.0.5" + object-inspect "^1.9.0" + object-keys "^1.1.1" + object.assign "^4.1.2" + string.prototype.trimend "^1.0.4" + string.prototype.trimstart "^1.0.4" + unbox-primitive "^1.0.0" + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-string-regexp@4.0.0, escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +escape-string-regexp@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8" + integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +eslint-scope@^5.0.0, eslint-scope@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +eslint-utils@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f" + integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q== + dependencies: + eslint-visitor-keys "^1.1.0" + +eslint-utils@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" + integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== + dependencies: + eslint-visitor-keys "^1.1.0" + +eslint-utils@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672" + integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== + dependencies: + eslint-visitor-keys "^2.0.0" + +eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" + integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== + +eslint-visitor-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8" + integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ== + +eslint@^6.0.0: + version "6.8.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.8.0.tgz#62262d6729739f9275723824302fb227c8c93ffb" + integrity sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig== + dependencies: + "@babel/code-frame" "^7.0.0" + ajv "^6.10.0" + chalk "^2.1.0" + cross-spawn "^6.0.5" + debug "^4.0.1" + doctrine "^3.0.0" + eslint-scope "^5.0.0" + eslint-utils "^1.4.3" + eslint-visitor-keys "^1.1.0" + espree "^6.1.2" + esquery "^1.0.1" + 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" + inquirer "^7.0.0" + is-glob "^4.0.0" + js-yaml "^3.13.1" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.3.0" + lodash "^4.17.14" + minimatch "^3.0.4" + mkdirp "^0.5.1" + natural-compare "^1.4.0" + optionator "^0.8.3" + progress "^2.0.0" + regexpp "^2.0.1" + semver "^6.1.2" + strip-ansi "^5.2.0" + strip-json-comments "^3.0.1" + table "^5.2.3" + text-table "^0.2.0" + v8-compile-cache "^2.0.3" + +eslint@^7.32.0: + version "7.32.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d" + integrity sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA== + dependencies: + "@babel/code-frame" "7.12.11" + "@eslint/eslintrc" "^0.4.3" + "@humanwhocodes/config-array" "^0.5.0" + 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" + escape-string-regexp "^4.0.0" + eslint-scope "^5.1.1" + eslint-utils "^2.1.0" + eslint-visitor-keys "^2.0.0" + espree "^7.3.1" + esquery "^1.4.0" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + functional-red-black-tree "^1.0.1" + glob-parent "^5.1.2" + globals "^13.6.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.merge "^4.6.2" + 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 "^6.0.9" + text-table "^0.2.0" + v8-compile-cache "^2.0.3" + +espree@^6.1.2: + version "6.2.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-6.2.1.tgz#77fc72e1fd744a2052c20f38a5b575832e82734a" + integrity sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw== + dependencies: + acorn "^7.1.1" + acorn-jsx "^5.2.0" + eslint-visitor-keys "^1.1.0" + +espree@^7.3.0, espree@^7.3.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6" + integrity sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g== + dependencies: + acorn "^7.4.0" + acorn-jsx "^5.3.1" + eslint-visitor-keys "^1.3.0" + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esquery@^1.0.1, esquery@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" + integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.1.0, estraverse@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" + integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +event-stream@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-4.0.1.tgz#4092808ec995d0dd75ea4580c1df6a74db2cde65" + integrity sha512-qACXdu/9VHPBzcyhdOWR5/IahhGMf0roTeZJfzz077GwylcDd90yOHLouhmv7GJ5XzPi6ekaQWd8AvPP2nOvpA== + dependencies: + duplexer "^0.1.1" + from "^0.1.7" + map-stream "0.0.7" + pause-stream "^0.0.11" + split "^1.0.1" + stream-combiner "^0.2.2" + through "^2.3.8" + +extend-shallow@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" + integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= + dependencies: + assign-symbols "^1.0.0" + is-extendable "^1.0.1" + +extend@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +external-editor@^3.0.3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" + integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== + dependencies: + chardet "^0.7.0" + iconv-lite "^0.4.24" + tmp "^0.0.33" + +fancy-log@^1.3.2: + version "1.3.3" + resolved "https://registry.yarnpkg.com/fancy-log/-/fancy-log-1.3.3.tgz#dbc19154f558690150a23953a0adbd035be45fc7" + integrity sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw== + dependencies: + ansi-gray "^0.1.1" + color-support "^1.1.3" + parse-node-version "^1.0.0" + time-stamp "^1.0.0" + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^3.1.1: + version "3.2.5" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.5.tgz#7939af2a656de79a4f1901903ee8adcaa7cb9661" + integrity sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg== + dependencies: + "@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@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= + +fastq@^1.6.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.11.0.tgz#bb9fb955a07130a918eb63c1f5161cc32a5d0858" + integrity sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g== + dependencies: + reusify "^1.0.4" + +figures@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" + integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== + dependencies: + escape-string-regexp "^1.0.5" + +file-entry-cache@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c" + integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g== + dependencies: + flat-cache "^2.0.1" + +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +find-up@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat-cache@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0" + integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA== + dependencies: + flatted "^2.0.0" + rimraf "2.6.3" + write "1.0.3" + +flat-cache@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" + integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== + dependencies: + flatted "^3.1.0" + rimraf "^3.0.2" + +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + +flatted@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138" + integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA== + +flatted@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.1.tgz#c4b489e80096d9df1dfc97c79871aea7c617c469" + integrity sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA== + +flush-write-stream@^1.0.2: + version "1.1.1" + resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8" + integrity sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w== + dependencies: + inherits "^2.0.3" + readable-stream "^2.3.6" + +follow-redirects@^1.14.8: + version "1.14.8" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc" + integrity sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA== + +from@^0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe" + integrity sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4= + +fs-minipass@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + +fs-mkdirp-stream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz#0b7815fc3201c6a69e14db98ce098c16935259eb" + integrity sha1-C3gV/DIBxqaeFNuYzgmMFpNSWes= + dependencies: + graceful-fs "^4.1.11" + through2 "^2.0.3" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +functional-red-black-tree@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" + integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-func-name@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" + integrity sha1-6td0q+5y4gQJQzoGY2YCPdaIekE= + +get-intrinsic@^1.0.2, get-intrinsic@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" + integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + +glob-parent@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" + integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4= + dependencies: + is-glob "^3.1.0" + path-dirname "^1.0.0" + +glob-parent@^5.0.0, glob-parent@^5.1.0, glob-parent@^5.1.2, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-stream@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/glob-stream/-/glob-stream-6.1.0.tgz#7045c99413b3eb94888d83ab46d0b404cc7bdde4" + integrity sha1-cEXJlBOz65SIjYOrRtC0BMx73eQ= + dependencies: + extend "^3.0.0" + glob "^7.1.1" + glob-parent "^3.1.0" + is-negated-glob "^1.0.0" + ordered-read-streams "^1.0.0" + pumpify "^1.3.5" + readable-stream "^2.1.5" + remove-trailing-separator "^1.0.1" + to-absolute-glob "^2.0.0" + unique-stream "^2.0.2" + +glob@7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== + dependencies: + 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@^7.0.5, glob@^7.1.1, glob@^7.1.3: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + 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" + +globals@^12.1.0: + version "12.4.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-12.4.0.tgz#a18813576a41b00a24a97e7f815918c2e19925f8" + integrity sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg== + dependencies: + type-fest "^0.8.1" + +globals@^13.6.0: + version "13.8.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.8.0.tgz#3e20f504810ce87a8d72e55aecf8435b50f4c1b3" + integrity sha512-rHtdA6+PDBIjeEvA91rpqzEvk/k3/i7EeNQiryiWuJH0Hw9cpyJMAt2jtbAwUaRdhD+573X4vWw6IcjKPasi9Q== + dependencies: + type-fest "^0.20.2" + +globals@^13.9.0: + version "13.9.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.9.0.tgz#4bf2bf635b334a173fb1daf7c5e6b218ecdc06cb" + integrity sha512-74/FduwI/JaIrr1H8e71UbDE+5x7pIPs1C2rrwC52SszOo043CsWOZEMW7o2Y58xwm9b+0RBKDxY5n2sUpEFxA== + dependencies: + type-fest "^0.20.2" + +globby@^11.0.3: + version "11.0.3" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.3.tgz#9b1f0cb523e171dd1ad8c7b2a9fb4b644b9593cb" + integrity sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg== + dependencies: + 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@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6: + version "4.2.6" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" + integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== + +growl@1.10.5: + version "1.10.5" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" + integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== + +gulp-eslint@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/gulp-eslint/-/gulp-eslint-6.0.0.tgz#7d402bb45f8a67652b868277011812057370a832" + integrity sha512-dCVPSh1sA+UVhn7JSQt7KEb4An2sQNbOdB3PA8UCfxsoPlAKjJHxYHGXdXC7eb+V1FAnilSFFqslPrq037l1ig== + dependencies: + eslint "^6.0.0" + fancy-log "^1.3.2" + plugin-error "^1.0.1" + +gulp-filter@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/gulp-filter/-/gulp-filter-7.0.0.tgz#e0712f3e57b5d647f802a1880255cafb54abf158" + integrity sha512-ZGWtJo0j1mHfP77tVuhyqem4MRA5NfNRjoVe6VAkLGeQQ/QGo2VsFwp7zfPTGDsd1rwzBmoDHhxpE6f5B3Zuaw== + dependencies: + multimatch "^5.0.0" + plugin-error "^1.0.1" + streamfilter "^3.0.0" + to-absolute-glob "^2.0.2" + +has-bigints@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113" + integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-symbols@^1.0.1, has-symbols@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" + integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +he@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +hosted-git-info@^2.1.4: + version "2.8.9" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" + integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== + +iconv-lite@^0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ignore@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" + integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== + +ignore@^5.1.4: + version "5.1.8" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" + integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== + +import-fresh@^3.0.0, import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= + +indent-string@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-5.0.0.tgz#4fd2980fccaf8622d14c64d694f4cf33c81951a5" + integrity sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inquirer@^7.0.0: + version "7.3.3" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003" + integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA== + dependencies: + ansi-escapes "^4.2.1" + chalk "^4.1.0" + cli-cursor "^3.1.0" + cli-width "^3.0.0" + external-editor "^3.0.3" + figures "^3.0.0" + lodash "^4.17.19" + mute-stream "0.0.8" + run-async "^2.4.0" + rxjs "^6.6.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + through "^2.3.6" + +is-absolute@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-absolute/-/is-absolute-1.0.0.tgz#395e1ae84b11f26ad1795e73c17378e48a301576" + integrity sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA== + dependencies: + is-relative "^1.0.0" + is-windows "^1.0.1" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= + +is-bigint@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.1.tgz#6923051dfcbc764278540b9ce0e6b3213aa5ebc2" + integrity sha512-J0ELF4yHFxHy0cmSxZuheDOz2luOdVvqjwmEcj8H/L1JHeuEDSDbeRP+Dk9kFVk5RTFzbucJ2Kb9F7ixY2QaCg== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-boolean-object@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.0.tgz#e2aaad3a3a8fca34c28f6eee135b156ed2587ff0" + integrity sha512-a7Uprx8UtD+HWdyYwnD1+ExtTgqQtD2k/1yJgtXP6wnMm8byhkoTZRl+95LLThpzNZJ5aEvi46cdH+ayMFRwmA== + dependencies: + call-bind "^1.0.0" + +is-buffer@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== + +is-callable@^1.1.4, is-callable@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.3.tgz#8b1e0500b73a1d76c70487636f368e519de8db8e" + integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ== + +is-core-module@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.2.0.tgz#97037ef3d52224d85163f5597b2b63d9afed981a" + integrity sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ== + dependencies: + has "^1.0.3" + +is-date-object@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" + integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== + +is-extendable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" + integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== + dependencies: + is-plain-object "^2.0.4" + +is-extglob@^2.1.0, is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" + integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo= + dependencies: + is-extglob "^2.1.0" + +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" + integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== + dependencies: + is-extglob "^2.1.1" + +is-negated-glob@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-negated-glob/-/is-negated-glob-1.0.0.tgz#6910bca5da8c95e784b5751b976cf5a10fee36d2" + integrity sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI= + +is-negative-zero@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24" + integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w== + +is-number-object@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197" + integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw== + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-plain-obj@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + +is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-regex@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.2.tgz#81c8ebde4db142f2cf1c53fc86d6a45788266251" + integrity sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg== + dependencies: + call-bind "^1.0.2" + has-symbols "^1.0.1" + +is-relative@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-1.0.0.tgz#a1bb6935ce8c5dba1e8b9754b9b2dcc020e2260d" + integrity sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA== + dependencies: + is-unc-path "^1.0.0" + +is-string@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6" + integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ== + +is-symbol@^1.0.2, is-symbol@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" + integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ== + dependencies: + has-symbols "^1.0.1" + +is-unc-path@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-unc-path/-/is-unc-path-1.0.0.tgz#d731e8898ed090a12c352ad2eaed5095ad322c9d" + integrity sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ== + dependencies: + unc-path-regex "^0.1.2" + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + +is-utf8@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" + integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= + +is-valid-glob@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-valid-glob/-/is-valid-glob-1.0.0.tgz#29bf3eff701be2d4d315dbacc39bc39fe8f601aa" + integrity sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao= + +is-windows@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@4.1.0, js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +js-yaml@^3.13.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +json-parse-better-errors@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" + integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= + +jsonc-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.0.0.tgz#abdd785701c7e7eaca8a9ec8cf070ca51a745a22" + integrity sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA== + +lazystream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.0.tgz#f6995fe0f820392f61396be89462407bb77168e4" + integrity sha1-9plf4PggOS9hOWvolGJAe7dxaOQ= + dependencies: + readable-stream "^2.0.5" + +lead@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lead/-/lead-1.0.0.tgz#6f14f99a37be3a9dd784f5495690e5903466ee42" + integrity sha1-bxT5mje+Op3XhPVJVpDlkDRm7kI= + dependencies: + flush-write-stream "^1.0.2" + +levn@^0.3.0, levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +load-json-file@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" + integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs= + dependencies: + graceful-fs "^4.1.2" + parse-json "^4.0.0" + pify "^3.0.0" + strip-bom "^3.0.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.clonedeep@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +lodash.truncate@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" + integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM= + +lodash@^4.17.14, lodash@^4.17.19: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +log-symbols@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +looper@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/looper/-/looper-3.0.0.tgz#2efa54c3b1cbaba9b94aee2e5914b0be57fbb749" + integrity sha1-LvpUw7HLq6m5Su4uWRSwvlf7t0k= + +loupe@^2.3.1: + version "2.3.4" + resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.4.tgz#7e0b9bffc76f148f9be769cb1321d3dcf3cb25f3" + integrity sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ== + dependencies: + get-func-name "^2.0.0" + +lru-cache@^4.1.5: + version "4.1.5" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" + integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +map-stream@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.0.7.tgz#8a1f07896d82b10926bd3744a2420009f88974a8" + integrity sha1-ih8HiW2CsQkmvTdEokIACfiJdKg= + +memorystream@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2" + integrity sha1-htcJCzDORV1j+64S3aUaR93K+bI= + +merge2@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +micromatch@^4.0.2: + version "4.0.4" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" + integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== + dependencies: + braces "^3.0.1" + picomatch "^2.2.3" + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +minimatch@3.0.4, minimatch@^3.0.3, minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimist@^1.2.5: + version "1.2.6" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" + integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== + +minipass@^3.0.0: + version "3.1.3" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd" + integrity sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg== + dependencies: + yallist "^4.0.0" + +minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + +mkdirp@^0.5.1: + version "0.5.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== + dependencies: + minimist "^1.2.5" + +mkdirp@^1.0.3, mkdirp@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + +mocha@^9.2.1: + version "9.2.1" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-9.2.1.tgz#a1abb675aa9a8490798503af57e8782a78f1338e" + integrity sha512-T7uscqjJVS46Pq1XDXyo9Uvey9gd3huT/DD9cYBb4K2Xc/vbKRPUWK067bxDQRK0yIz6Jxk73IrnimvASzBNAQ== + dependencies: + "@ungap/promise-all-settled" "1.1.2" + ansi-colors "4.1.1" + browser-stdout "1.3.1" + chokidar "3.5.3" + debug "4.3.3" + diff "5.0.0" + escape-string-regexp "4.0.0" + find-up "5.0.0" + glob "7.2.0" + growl "1.10.5" + he "1.2.0" + js-yaml "4.1.0" + log-symbols "4.1.0" + minimatch "3.0.4" + ms "2.1.3" + nanoid "3.2.0" + serialize-javascript "6.0.0" + strip-json-comments "3.1.1" + supports-color "8.1.1" + which "2.0.2" + workerpool "6.2.0" + yargs "16.2.0" + yargs-parser "20.2.4" + yargs-unparser "2.0.0" + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +multimatch@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-5.0.0.tgz#932b800963cea7a31a033328fa1e0c3a1874dbe6" + integrity sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA== + dependencies: + "@types/minimatch" "^3.0.3" + array-differ "^3.0.0" + array-union "^2.1.0" + arrify "^2.0.1" + minimatch "^3.0.4" + +mute-stream@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" + integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== + +nan@^2.14.0: + version "2.14.2" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19" + integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ== + +nanoid@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c" + integrity sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= + +nice-try@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" + integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== + +node-pty@^0.10.1: + version "0.10.1" + resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.10.1.tgz#cd05d03a2710315ec40221232ec04186f6ac2c6d" + integrity sha512-JTdtUS0Im/yRsWJSx7yiW9rtpfmxqxolrtnyKwPLI+6XqTAPW/O2MjS8FYL4I5TsMbH2lVgDb2VMjp+9LoQGNg== + dependencies: + nan "^2.14.0" + +noms@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/noms/-/noms-0.0.0.tgz#da8ebd9f3af9d6760919b27d9cdc8092a7332859" + integrity sha1-2o69nzr51nYJGbJ9nNyAkqczKFk= + dependencies: + inherits "^2.0.1" + readable-stream "~1.0.31" + +normalize-package-data@^2.3.2: + version "2.5.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" + integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== + dependencies: + hosted-git-info "^2.1.4" + resolve "^1.10.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +normalize-path@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" + integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= + dependencies: + remove-trailing-separator "^1.0.1" + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +now-and-later@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/now-and-later/-/now-and-later-2.0.1.tgz#8e579c8685764a7cc02cb680380e94f43ccb1f7c" + integrity sha512-KGvQ0cB70AQfg107Xvs/Fbu+dGmZoTRJp2TaPwcwQm3/7PteUyN2BCgk8KBMPGBUXZdVwyWS8fDCGFygBm19UQ== + dependencies: + once "^1.3.2" + +npm-run-all@^4.1.5: + version "4.1.5" + resolved "https://registry.yarnpkg.com/npm-run-all/-/npm-run-all-4.1.5.tgz#04476202a15ee0e2e214080861bff12a51d98fba" + integrity sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ== + dependencies: + ansi-styles "^3.2.1" + chalk "^2.4.1" + cross-spawn "^6.0.5" + memorystream "^0.3.1" + minimatch "^3.0.4" + pidtree "^0.3.0" + read-pkg "^3.0.0" + shell-quote "^1.6.1" + string.prototype.padend "^3.0.0" + +object-inspect@^1.9.0: + version "1.10.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.10.2.tgz#b6385a3e2b7cae0b5eafcf90cddf85d128767f30" + integrity sha512-gz58rdPpadwztRrPjZE9DZLOABUpTGdcANUgOwBFO1C+HZZhePoP83M65WGDmbpwFYJSWqavbl4SgDn4k8RYTA== + +object-keys@^1.0.12, object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@^4.0.4, object.assign@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" + integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + has-symbols "^1.0.1" + object-keys "^1.1.1" + +once@^1.3.0, once@^1.3.1, once@^1.3.2, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +onetime@^5.1.0: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +optionator@^0.8.3: + version "0.8.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" + integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== + dependencies: + 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" + +optionator@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" + integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== + dependencies: + 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" + +ordered-read-streams@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz#77c0cb37c41525d64166d990ffad7ec6a0e1363e" + integrity sha1-d8DLN8QVJdZBZtmQ/61+xqDhNj4= + dependencies: + readable-stream "^2.0.1" + +os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= + +p-all@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-all/-/p-all-4.0.0.tgz#fd0d57391727646da85cfe9595b9215617982c19" + integrity sha512-QXqMc8PpYu0gmNM6VcKP0uYqeI+dtvSNeaDb8ktnNjposr+nftHHCSYbj/S/oUceF6R868jw1XOxkJKUSiHgEQ== + dependencies: + p-map "^5.0.0" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +p-map@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-5.1.0.tgz#1c31bdfc492910098bdb4e63d099efbdd9b37755" + integrity sha512-hDTnBRGPXM4hUkmV4Nbe9ZyFnqUAHFYq5S/3+P38TRf0KbmkQuRSzfGM+JngEJsvB0m6nHvhsSv5E6VsGSB2zA== + dependencies: + aggregate-error "^4.0.0" + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-json@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" + integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= + dependencies: + error-ex "^1.3.1" + json-parse-better-errors "^1.0.1" + +parse-node-version@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parse-node-version/-/parse-node-version-1.0.1.tgz#e2b5dbede00e7fa9bc363607f53327e8b073189b" + integrity sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA== + +path-dirname@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" + integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-key@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-type@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" + integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg== + dependencies: + pify "^3.0.0" + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +pathval@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" + integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== + +pause-stream@^0.0.11: + version "0.0.11" + resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445" + integrity sha1-/lo0sMvOErWqaitAPuLnO2AvFEU= + dependencies: + through "~2.3" + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.3.tgz#465547f359ccc206d3c48e46a1bcb89bf7ee619d" + integrity sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg== + +pidtree@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.3.1.tgz#ef09ac2cc0533df1f3250ccf2c4d366b0d12114a" + integrity sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA== + +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= + +plugin-error@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/plugin-error/-/plugin-error-1.0.1.tgz#77016bd8919d0ac377fdcdd0322328953ca5781c" + integrity sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA== + dependencies: + ansi-colors "^1.0.1" + arr-diff "^4.0.0" + arr-union "^3.1.0" + extend-shallow "^3.0.2" + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= + +process-nextick-args@^2.0.0, process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +progress@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + +pseudomap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= + +pull-stream@^3.2.3, pull-stream@^3.6.14: + version "3.6.14" + resolved "https://registry.yarnpkg.com/pull-stream/-/pull-stream-3.6.14.tgz#529dbd5b86131f4a5ed636fdf7f6af00781357ee" + integrity sha512-KIqdvpqHHaTUA2mCYcLG1ibEbu/LCKoJZsBWyv9lSYtPkJPBq8m3Hxa103xHi6D2thj5YXa0TqK3L3GUkwgnew== + +pump@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" + integrity sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +pumpify@^1.3.5: + version "1.5.1" + resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce" + integrity sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ== + dependencies: + duplexify "^3.6.0" + inherits "^2.0.3" + pump "^2.0.0" + +punycode@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +read-pkg@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389" + integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k= + dependencies: + load-json-file "^4.0.0" + normalize-package-data "^2.3.2" + path-type "^3.0.0" + +readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.5, readable-stream@^2.1.5, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + dependencies: + 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" + +readable-stream@^3.0.6: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readable-stream@~1.0.31: + version "1.0.34" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" + integrity sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw= + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +regexpp@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" + integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw== + +regexpp@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2" + integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q== + +remove-bom-buffer@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz#c2bf1e377520d324f623892e33c10cac2c252b53" + integrity sha512-8v2rWhaakv18qcvNeli2mZ/TMTL2nEyAKRvzo1WtnZBl15SHyEhrCu2/xKlJyUFKHiHgfXIyuY6g2dObJJycXQ== + dependencies: + is-buffer "^1.1.5" + is-utf8 "^0.2.1" + +remove-bom-stream@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/remove-bom-stream/-/remove-bom-stream-1.2.0.tgz#05f1a593f16e42e1fb90ebf59de8e569525f9523" + integrity sha1-BfGlk/FuQuH7kOv1nejlaVJflSM= + dependencies: + remove-bom-buffer "^3.0.0" + safe-buffer "^5.1.0" + through2 "^2.0.3" + +remove-trailing-separator@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" + integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= + +replace-ext@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.1.tgz#2d6d996d04a15855d967443631dd5f77825b016a" + integrity sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw== + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve-options@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/resolve-options/-/resolve-options-1.1.0.tgz#32bb9e39c06d67338dc9378c0d6d6074566ad131" + integrity sha1-MrueOcBtZzONyTeMDW1gdFZq0TE= + dependencies: + value-or-function "^3.0.0" + +resolve@^1.10.0: + version "1.20.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" + integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== + dependencies: + is-core-module "^2.2.0" + path-parse "^1.0.6" + +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rimraf@2.6.3: + version "2.6.3" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" + integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== + dependencies: + glob "^7.1.3" + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +run-async@^2.4.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" + integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +rxjs@^6.6.0: + version "6.6.7" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" + integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== + dependencies: + tslib "^1.9.0" + +safe-buffer@^5.1.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +"semver@2 || 3 || 4 || 5", semver@^5.5.0, semver@^5.6.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + +semver@^6.1.2: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +semver@^7.2.1, semver@^7.3.5: + version "7.3.5" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" + integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== + dependencies: + lru-cache "^6.0.0" + +serialize-javascript@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" + integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== + dependencies: + randombytes "^2.1.0" + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= + dependencies: + shebang-regex "^1.0.0" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +shell-quote@^1.6.1: + version "1.7.2" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2" + integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg== + +shell-quote@^1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.3.tgz#aa40edac170445b9a431e17bb62c0b881b9c4123" + integrity sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw== + +sigmund@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" + integrity sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA= + +signal-exit@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" + integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +slice-ansi@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636" + integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ== + dependencies: + ansi-styles "^3.2.0" + astral-regex "^1.0.0" + is-fullwidth-code-point "^2.0.0" + +slice-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" + integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + +spdx-correct@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" + integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w== + dependencies: + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" + +spdx-exceptions@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" + integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== + +spdx-expression-parse@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" + integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.7" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz#e9c18a410e5ed7e12442a549fbd8afa767038d65" + integrity sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ== + +split@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9" + integrity sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg== + dependencies: + through "2" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + +stream-combiner@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.2.2.tgz#aec8cbac177b56b6f4fa479ced8c1912cee52858" + integrity sha1-rsjLrBd7Vrb0+kec7YwZEs7lKFg= + dependencies: + duplexer "~0.1.1" + through "~2.3.4" + +stream-shift@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" + integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== + +stream-to-pull-stream@^1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/stream-to-pull-stream/-/stream-to-pull-stream-1.7.3.tgz#4161aa2d2eb9964de60bfa1af7feaf917e874ece" + integrity sha512-6sNyqJpr5dIOQdgNy/xcDWwDuzAsAwVzhzrWlAPAQ7Lkjx/rv0wgvxEyKwTq6FmNd5rjTrELt/CLmaSw7crMGg== + dependencies: + looper "^3.0.0" + pull-stream "^3.2.3" + +streamfilter@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/streamfilter/-/streamfilter-3.0.0.tgz#8c61b08179a6c336c6efccc5df30861b7a9675e7" + integrity sha512-kvKNfXCmUyC8lAXSSHCIXBUlo/lhsLcCU/OmzACZYpRUdtKIH68xYhm/+HI15jFJYtNJGYtCgn2wmIiExY1VwA== + dependencies: + readable-stream "^3.0.6" + +string-width@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" + integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== + dependencies: + emoji-regex "^7.0.1" + is-fullwidth-code-point "^2.0.0" + strip-ansi "^5.1.0" + +string-width@^4.1.0, string-width@^4.2.0: + version "4.2.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" + integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.0" + +string.prototype.padend@^3.0.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/string.prototype.padend/-/string.prototype.padend-3.1.2.tgz#6858ca4f35c5268ebd5e8615e1327d55f59ee311" + integrity sha512-/AQFLdYvePENU3W5rgurfWSMU6n+Ww8n/3cUt7E+vPBB/D7YDG8x+qjoFs4M/alR2bW7Qg6xMjVwWUOvuQ0XpQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.18.0-next.2" + +string.prototype.trimend@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80" + integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + +string.prototype.trimstart@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed" + integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^5.1.0, strip-ansi@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== + dependencies: + ansi-regex "^4.1.0" + +strip-ansi@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" + integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== + dependencies: + ansi-regex "^5.0.0" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= + +strip-json-comments@3.1.1, strip-json-comments@^3.0.1, strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +supports-color@8.1.1: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +table@^5.2.3: + version "5.4.6" + resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" + integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug== + dependencies: + ajv "^6.10.2" + lodash "^4.17.14" + slice-ansi "^2.1.0" + string-width "^3.0.0" + +table@^6.0.9: + version "6.7.1" + resolved "https://registry.yarnpkg.com/table/-/table-6.7.1.tgz#ee05592b7143831a8c94f3cee6aae4c1ccef33e2" + integrity sha512-ZGum47Yi6KOOFDE8m223td53ath2enHcYLgOCjGr5ngu8bdIARQk6mN/wRMv4yMRcHnCSnHbCEha4sobQx5yWg== + dependencies: + ajv "^8.0.1" + lodash.clonedeep "^4.5.0" + lodash.truncate "^4.4.2" + slice-ansi "^4.0.0" + string-width "^4.2.0" + strip-ansi "^6.0.0" + +tar@^6.1.11: + version "6.1.11" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" + integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA== + dependencies: + 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" + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= + +through2-filter@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/through2-filter/-/through2-filter-3.0.0.tgz#700e786df2367c2c88cd8aa5be4cf9c1e7831254" + integrity sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA== + dependencies: + through2 "~2.0.0" + xtend "~4.0.0" + +through2@^2.0.0, through2@^2.0.1, through2@^2.0.3, through2@~2.0.0: + version "2.0.5" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" + integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== + dependencies: + readable-stream "~2.3.6" + xtend "~4.0.1" + +through@2, through@^2.3.6, through@^2.3.8, through@~2.3, through@~2.3.4: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= + +time-stamp@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-1.1.0.tgz#764a5a11af50561921b133f3b44e618687e0f5c3" + integrity sha1-dkpaEa9QVhkhsTPztE5hhofg9cM= + +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + dependencies: + os-tmpdir "~1.0.2" + +to-absolute-glob@^2.0.0, to-absolute-glob@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz#1865f43d9e74b0822db9f145b78cff7d0f7c849b" + integrity sha1-GGX0PZ50sIItufFFt4z/fQ98hJs= + dependencies: + is-absolute "^1.0.0" + is-negated-glob "^1.0.0" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +to-through@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-through/-/to-through-2.0.0.tgz#fc92adaba072647bc0b67d6b03664aa195093af6" + integrity sha1-/JKtq6ByZHvAtn1rA2ZKoZUJOvY= + dependencies: + through2 "^2.0.3" + +ts-node@^10.4.0: + version "10.4.0" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.4.0.tgz#680f88945885f4e6cf450e7f0d6223dd404895f7" + integrity sha512-g0FlPvvCXSIO1JDF6S232P5jPYqBkRL9qly81ZgAOSU7rwI0stphCgd2kLiCrU9DjQCrJMWEqcNSjQL02s6d8A== + dependencies: + "@cspotcode/source-map-support" "0.7.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + yn "3.1.1" + +tslib@^1.8.1, tslib@^1.9.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tsutils@^3.21.0: + version "3.21.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" + integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== + dependencies: + tslib "^1.8.1" + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= + dependencies: + prelude-ls "~1.1.2" + +type-detect@^4.0.0, type-detect@^4.0.5: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + +type-fest@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" + integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== + +typescript-formatter@^7.2.2: + version "7.2.2" + resolved "https://registry.yarnpkg.com/typescript-formatter/-/typescript-formatter-7.2.2.tgz#a147181839b7bb09c2377b072f20f6336547c00a" + integrity sha512-V7vfI9XArVhriOTYHPzMU2WUnm5IMdu9X/CPxs8mIMGxmTBFpDABlbkBka64PZJ9/xgQeRpK8KzzAG4MPzxBDQ== + dependencies: + commandpost "^1.0.0" + editorconfig "^0.15.0" + +typescript@^4.5.5: + version "4.5.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3" + integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA== + +unbox-primitive@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471" + integrity sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw== + dependencies: + function-bind "^1.1.1" + has-bigints "^1.0.1" + has-symbols "^1.0.2" + which-boxed-primitive "^1.0.2" + +unc-path-regex@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" + integrity sha1-5z3T17DXxe2G+6xrCufYxqadUPo= + +unique-stream@^2.0.2: + version "2.3.1" + resolved "https://registry.yarnpkg.com/unique-stream/-/unique-stream-2.3.1.tgz#c65d110e9a4adf9a6c5948b28053d9a8d04cbeac" + integrity sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A== + dependencies: + json-stable-stringify-without-jsonify "^1.0.1" + through2-filter "^3.0.0" + +untildify@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" + integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +v8-compile-cache@^2.0.3: + version "2.3.0" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" + integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== + +validate-npm-package-license@^3.0.1: + version "3.0.4" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" + integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== + dependencies: + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" + +value-or-function@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/value-or-function/-/value-or-function-3.0.0.tgz#1c243a50b595c1be54a754bfece8563b9ff8d813" + integrity sha1-HCQ6ULWVwb5Up1S/7OhWO5/42BM= + +vinyl-fs@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/vinyl-fs/-/vinyl-fs-3.0.3.tgz#c85849405f67428feabbbd5c5dbdd64f47d31bc7" + integrity sha512-vIu34EkyNyJxmP0jscNzWBSygh7VWhqun6RmqVfXePrOwi9lhvRs//dOaGOTRUQr4tx7/zd26Tk5WeSVZitgng== + dependencies: + fs-mkdirp-stream "^1.0.0" + glob-stream "^6.1.0" + graceful-fs "^4.0.0" + is-valid-glob "^1.0.0" + lazystream "^1.0.0" + lead "^1.0.0" + object.assign "^4.0.4" + pumpify "^1.3.5" + readable-stream "^2.3.3" + remove-bom-buffer "^3.0.0" + remove-bom-stream "^1.2.0" + resolve-options "^1.1.0" + through2 "^2.0.0" + to-through "^2.0.0" + value-or-function "^3.0.0" + vinyl "^2.0.0" + vinyl-sourcemap "^1.1.0" + +vinyl-sourcemap@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/vinyl-sourcemap/-/vinyl-sourcemap-1.1.0.tgz#92a800593a38703a8cdb11d8b300ad4be63b3e16" + integrity sha1-kqgAWTo4cDqM2xHYswCtS+Y7PhY= + dependencies: + append-buffer "^1.0.2" + convert-source-map "^1.5.0" + graceful-fs "^4.1.6" + normalize-path "^2.1.1" + now-and-later "^2.0.0" + remove-bom-buffer "^3.0.0" + vinyl "^2.0.0" + +vinyl@^2.0.0, vinyl@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.2.1.tgz#23cfb8bbab5ece3803aa2c0a1eb28af7cbba1974" + integrity sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw== + dependencies: + clone "^2.1.1" + clone-buffer "^1.0.0" + clone-stats "^1.0.0" + cloneable-readable "^1.0.0" + remove-trailing-separator "^1.0.1" + replace-ext "^1.0.0" + +"vscode-dev-containers@https://github.com/microsoft/vscode-dev-containers/releases/download/v0.233.0/vscode-dev-containers-0.233.0.tgz": + version "0.233.0" + resolved "https://github.com/microsoft/vscode-dev-containers/releases/download/v0.233.0/vscode-dev-containers-0.233.0.tgz#18eef49ed9a786976eb9f53ea63ff13cab4e9939" + +vscode-uri@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.3.tgz#a95c1ce2e6f41b7549f86279d19f47951e4f4d84" + integrity sha512-EcswR2S8bpR7fD0YPeS7r2xXExrScVMxg4MedACaWHEtx9ftCF/qHG1xGkolzTPcEmjTavCQgbVzHUIdTMzFGA== + +which-boxed-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== + dependencies: + is-bigint "^1.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + is-symbol "^1.0.3" + +which@2.0.2, which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +which@^1.2.9: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +word-wrap@^1.2.3, word-wrap@~1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + +workerpool@6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.0.tgz#827d93c9ba23ee2019c3ffaff5c27fccea289e8b" + integrity sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A== + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +write@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3" + integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig== + dependencies: + mkdirp "^0.5.1" + +xtend@~4.0.0, xtend@~4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yallist@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yargs-parser@20.2.4: + version "20.2.4" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" + integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== + +yargs-parser@^20.2.2: + version "20.2.7" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.7.tgz#61df85c113edfb5a7a4e36eb8aa60ef423cbc90a" + integrity sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw== + +yargs-unparser@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" + integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== + dependencies: + camelcase "^6.0.0" + decamelize "^4.0.0" + flat "^5.0.2" + is-plain-obj "^2.1.0" + +yargs@16.2.0, yargs@^16.1.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + +yargs@~17.0.1: + version "17.0.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.0.1.tgz#6a1ced4ed5ee0b388010ba9fd67af83b9362e0bb" + integrity sha512-xBBulfCc8Y6gLFcrPvtqKz9hz8SO0l1Ni8GgDekvBX2ro0HRQImDGnikfc33cgzcYUSncapnNcZDjVFIH3f6KQ== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==