├── .ackrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── codeql.yml │ ├── dependency-review.yml │ ├── devenv-e2e.yml │ ├── git-hash-security-check.yml │ ├── npm-prepare-release.yml │ ├── npm-publish-prerelease.yml │ ├── npm-publish.yml │ ├── publish-docs.yml │ ├── stale.yml │ └── windows-tests.yml ├── .gitignore ├── .npmignore ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .sonarcloud.properties ├── LICENSE ├── README.md ├── __fixtures__ ├── certs │ ├── empty.ca │ ├── garbage.ca │ ├── split0.ca │ ├── split1.ca │ └── test-chain.bundle ├── client-file-uploader │ ├── db-dump-ipsum-67mb.sql │ ├── emptyfile.txt │ ├── numerical-test-file-5.24mb.txt │ └── tinyfile.txt ├── custom-deploy │ ├── invalid-file-chars.zip │ ├── no-root-folder.zip │ ├── no-themes-folder.zip │ ├── valid-zip-posix.zip │ └── valid-zip-win32.zip ├── dev-env-e2e │ ├── .lando_mariadb.yml │ ├── empty.sql │ ├── fail-autoIncrement-validation.sql │ ├── fail-autoIncrement-validation.sql.gz │ ├── fail-validation.sql │ ├── fail-validation.sql.gz │ ├── instance_data_mariadb.json │ ├── mydumper-detection.expected.sql │ ├── mydumper-detection.sql │ ├── mydumper-detection.sql.gz │ ├── mysqldump-detection.sql │ └── mysqldump-detection.sql.gz ├── search-replace-binaries │ ├── go-search-replace-test-darwin-arm64 │ ├── go-search-replace-test-darwin-x64 │ ├── go-search-replace-test-linux-x64 │ └── go-search-replace-test-win32-x64.exe └── validations │ ├── bad-sql-dev-env.sql │ ├── bad-sql-dump.sql │ ├── bad-sql-dump.sql.gz │ ├── bad-sql-duplicate-tables.sql │ ├── empty.zip │ └── multiline-statements.sql ├── __tests__ ├── bin │ ├── vip-app-deploy-validate.e2e.js │ ├── vip-app-deploy-validate.js │ ├── vip-app-deploy.js │ ├── vip-cache-purge-url.js │ ├── vip-config-envvar-delete.js │ ├── vip-config-envvar-get-all.js │ ├── vip-config-envvar-get.js │ ├── vip-config-envvar-list.js │ ├── vip-config-envvar-set.js │ ├── vip-import-sql.js │ ├── vip-import-validate-files.js │ ├── vip-logs.js │ ├── vip-slowlogs.js │ └── vip-whoami.js ├── commands │ ├── backup-db.ts │ ├── dev-env-sync-sql.ts │ ├── export-sql.ts │ ├── phpmyadmin.ts │ └── wp-ssh.ts ├── devenv-e2e │ ├── 001-create.spec.js │ ├── 002-destroy.spec.js │ ├── 003-start.spec.js │ ├── 004-stop.spec.js │ ├── 005-update.spec.js │ ├── 006-list.spec.js │ ├── 007-info.spec.js │ ├── 008-exec.spec.js │ ├── 009-import-media.spec.js │ ├── 010-import-sql.spec.js │ ├── 011-logs.spec.js │ ├── 012-shell.spec.js │ ├── 013-configuration-file.spec.js │ ├── helpers │ │ ├── cli-test.js │ │ ├── commands.js │ │ ├── docker-utils.js │ │ └── utils.js │ └── jest │ │ ├── jest.config.js │ │ └── sequencer.js ├── e2e_test.bat └── lib │ ├── analytics │ ├── clients │ │ └── tracks.js │ └── index.js │ ├── app-logs │ └── app-logs.js │ ├── backup-storage-availability │ └── backup-storage-availability.ts │ ├── cli │ ├── apiConfig.js │ ├── command.js │ ├── config.js │ ├── envAlias.js │ ├── exit.js │ └── format.js │ ├── client-file-uploader.js │ ├── dev-environment │ ├── dev-environment-cli.js │ ├── dev-environment-configuration-file.js │ ├── dev-environment-core.js │ └── docker-utils.spec.js │ ├── envvar │ └── api.js │ ├── http │ └── proxy-agent.js │ ├── keychain.js │ ├── search-and-replace.js │ ├── site-import.js │ ├── token.js │ ├── validations │ ├── is-multi-site-sql-dump.js │ ├── is-multisite-domain-mapped.js │ ├── sql.js │ └── utils.js │ └── vip-import-validate-files.js ├── assets ├── dev-env.lando.template.yml.ejs └── dev-env.nginx.template.conf.ejs ├── babel.config.js ├── codegen.ts ├── config ├── config.local.json └── config.publish.json ├── docs ├── ARCHITECTURE.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── DEBUGGING.md ├── RELEASING.md ├── SECURITY.md ├── SETUP.md └── TESTING.md ├── helpers ├── check-version.js ├── generate-docs.js ├── postinstall.js └── prepublishOnly.js ├── index.js ├── jest.config.js ├── jest.setup.js ├── jest.setupMocks.js ├── npm-shrinkwrap.json ├── package.json ├── src ├── bin │ ├── vip-app-deploy-validate.ts │ ├── vip-app-deploy.generated.d.ts │ ├── vip-app-deploy.ts │ ├── vip-app-list.generated.d.ts │ ├── vip-app-list.js │ ├── vip-app.js │ ├── vip-backup-db.ts │ ├── vip-backup.ts │ ├── vip-cache-purge-url.js │ ├── vip-cache.js │ ├── vip-config-envvar-delete.js │ ├── vip-config-envvar-get-all.js │ ├── vip-config-envvar-get.js │ ├── vip-config-envvar-list.js │ ├── vip-config-envvar-set.js │ ├── vip-config-envvar.js │ ├── vip-config-software-get.js │ ├── vip-config-software-update.js │ ├── vip-config-software.js │ ├── vip-config.js │ ├── vip-db-phpmyadmin.ts │ ├── vip-db.ts │ ├── vip-dev-env-create.js │ ├── vip-dev-env-destroy.js │ ├── vip-dev-env-exec.js │ ├── vip-dev-env-import-media.js │ ├── vip-dev-env-import-sql.js │ ├── vip-dev-env-import.js │ ├── vip-dev-env-info.js │ ├── vip-dev-env-list.js │ ├── vip-dev-env-logs.js │ ├── vip-dev-env-purge.js │ ├── vip-dev-env-shell.js │ ├── vip-dev-env-start.js │ ├── vip-dev-env-stop.js │ ├── vip-dev-env-sync-sql.js │ ├── vip-dev-env-sync.js │ ├── vip-dev-env-update.js │ ├── vip-dev-env.js │ ├── vip-export-sql.js │ ├── vip-export.js │ ├── vip-import-media-abort.generated.d.ts │ ├── vip-import-media-abort.js │ ├── vip-import-media-status.js │ ├── vip-import-media.generated.d.ts │ ├── vip-import-media.js │ ├── vip-import-sql-status.js │ ├── vip-import-sql.generated.d.ts │ ├── vip-import-sql.js │ ├── vip-import-validate-files.js │ ├── vip-import-validate-sql.js │ ├── vip-import.js │ ├── vip-logout.ts │ ├── vip-logs.js │ ├── vip-search-replace.js │ ├── vip-slowlogs.ts │ ├── vip-sync.generated.d.ts │ ├── vip-sync.js │ ├── vip-validate-preflight.generated.d.ts │ ├── vip-validate-preflight.js │ ├── vip-validate.js │ ├── vip-whoami.ts │ ├── vip-wp.generated.d.ts │ ├── vip-wp.js │ └── vip.js ├── commands │ ├── backup-db.generated.d.ts │ ├── backup-db.ts │ ├── dev-env-import-sql.ts │ ├── dev-env-sync-sql.ts │ ├── export-sql.generated.d.ts │ ├── export-sql.ts │ ├── phpmyadmin.generated.d.ts │ ├── phpmyadmin.ts │ └── wp-ssh.ts ├── graphqlTypes.d.ts └── lib │ ├── analytics │ ├── clients │ │ ├── client.ts │ │ ├── pendo.ts │ │ └── tracks.ts │ └── index.ts │ ├── api.ts │ ├── api │ ├── app.ts │ ├── cache-purge.generated.d.ts │ ├── cache-purge.ts │ ├── feature-flags.generated.d.ts │ ├── feature-flags.ts │ ├── http.ts │ ├── user.generated.d.ts │ └── user.ts │ ├── app-logs │ ├── app-logs.generated.d.ts │ └── app-logs.ts │ ├── app-slowlogs │ ├── app-slowlogs.generated.d.ts │ ├── app-slowlogs.ts │ └── types.ts │ ├── app.ts │ ├── backup-storage-availability │ ├── backup-storage-availability.ts │ └── docker-machine-not-found-error.ts │ ├── cli │ ├── apiConfig.ts │ ├── command.d.ts │ ├── command.js │ ├── config.ts │ ├── envAlias.ts │ ├── exit.ts │ ├── format.ts │ ├── progress.ts │ └── prompt.ts │ ├── client-file-uploader.ts │ ├── config │ ├── software.generated.d.ts │ └── software.ts │ ├── constants │ ├── dev-environment.ts │ ├── file-size.ts │ └── vipgo.ts │ ├── custom-deploy │ ├── custom-deploy.generated.d.ts │ └── custom-deploy.ts │ ├── database.ts │ ├── dev-environment │ ├── dev-environment-cli.ts │ ├── dev-environment-configuration-file.ts │ ├── dev-environment-core.ts │ ├── dev-environment-database.ts │ ├── dev-environment-lando.ts │ ├── docker-utils.ts │ └── types.ts │ ├── env.ts │ ├── envvar │ ├── api-delete.generated.d.ts │ ├── api-delete.ts │ ├── api-get-all.generated.d.ts │ ├── api-get-all.ts │ ├── api-get.ts │ ├── api-list.generated.d.ts │ ├── api-list.ts │ ├── api-set.generated.d.ts │ ├── api-set.ts │ ├── api.ts │ ├── input.ts │ ├── logging.ts │ └── read-file.ts │ ├── http │ └── proxy-agent.ts │ ├── keychain.ts │ ├── keychain │ ├── insecure.ts │ ├── keychain.ts │ └── secure.ts │ ├── logout.ts │ ├── media-import │ ├── config.generated.d.ts │ ├── config.ts │ ├── media-file-import.ts │ ├── progress.ts │ ├── status.generated.d.ts │ └── status.ts │ ├── promise.ts │ ├── read-file.ts │ ├── retry.ts │ ├── search-and-replace.ts │ ├── site-import │ ├── db-file-import.ts │ ├── status.generated.d.ts │ └── status.ts │ ├── token.ts │ ├── tracker.ts │ ├── types.ts │ ├── types │ └── graphql │ │ └── rate-limit-exceeded-error.ts │ ├── user-error.ts │ ├── utils.ts │ ├── validations │ ├── custom-deploy.ts │ ├── is-multi-site-sql-dump.ts │ ├── is-multi-site.generated.d.ts │ ├── is-multi-site.ts │ ├── is-multisite-domain-mapped.generated.d.ts │ ├── is-multisite-domain-mapped.ts │ ├── line-by-line.ts │ ├── site-type.ts │ ├── sql.ts │ └── utils.ts │ └── vip-import-validate-files.ts ├── tsconfig.json └── types ├── copy-dir └── index.d.ts ├── enquirer └── index.d.ts └── lando ├── index.d.ts ├── lib ├── app.d.ts ├── art.d.ts ├── bootstrap.d.ts ├── cache.d.ts ├── cli.d.ts ├── compose.d.ts ├── config.d.ts ├── daemon.d.ts ├── docker.d.ts ├── engine.d.ts ├── env.d.ts ├── error.d.ts ├── events.d.ts ├── factory.d.ts ├── formatters.d.ts ├── lando.d.ts ├── logger.d.ts ├── metrics.d.ts ├── plugins.d.ts ├── promise.d.ts ├── router.d.ts ├── scan.d.ts ├── shell.d.ts ├── table.d.ts ├── user.d.ts ├── utils.d.ts └── yaml.d.ts └── plugins ├── lando-core └── lib │ └── utils.d.ts └── lando-tooling └── lib └── build.d.ts /.ackrc: -------------------------------------------------------------------------------- 1 | --ignore-directory=is:dist 2 | --ignore-directory=is:coverage 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = tab 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [*.json,*.yaml,*.yml] 11 | indent_style = space 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | *.generated.d.ts 2 | /dist/ 3 | /flow-typed/ 4 | /src/graphqlTypes.d.ts 5 | codegen.ts 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | require( '@automattic/eslint-plugin-wpvip/init' ); 2 | 3 | module.exports = { 4 | extends: [ 5 | 'plugin:@automattic/wpvip/recommended', 6 | 'plugin:@automattic/wpvip/cli', 7 | 'plugin:@automattic/wpvip/weak-testing', 8 | 'plugin:@automattic/wpvip/typescript', 9 | ], 10 | rules: { 11 | camelcase: 'warn', 12 | 'jest/no-mocks-import': 'warn', 13 | 'no-await-in-loop': 'warn', 14 | 'no-console': 0, 15 | 'security/detect-object-injection': 0, 16 | 'security/detect-non-literal-fs-filename': 0, 17 | 'promise/no-multiple-resolved': 0, 18 | 'no-unused-vars': 'off', 19 | '@typescript-eslint/no-unused-vars': 'warn', 20 | }, 21 | root: true, 22 | }; 23 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners 2 | * @Automattic/vip-platform-patisserie 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## For Automatticians! 2 | 3 | :wave: Just a quick reminder that this is a public repo. Please don't include any internal links or sensitive data (like PII, private code, client names, site URLs, etc. If you're not sure if something is safe to share, please just ask! 4 | 5 | If you're not an Automattician, welcome! We look forward to your contribution! :heart: 6 | 7 | Please remove this section before submitting the issue. 8 | 9 | --- 10 | 11 | ## Expected/Desired Behavior 12 | 13 | Fill with either one: 14 | 15 | 1. Expected behavior for bugs 16 | 1. Desired behavior for improvements/enhancements 17 | 18 | ## Actual Behavior 19 | 20 | How does the CLI currently work. 21 | 22 | ## Steps to Reproduce the Problem 23 | 24 | 1. Step 1 25 | 1. Step 2 26 | 1. Step 3 27 | 28 | ## (Optional) Additional notes 29 | 30 | Any other relevant information to help with debugging/implemention. 31 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## For Automatticians! 2 | 3 | :wave: Just a quick reminder that this is a public repo. Please don't include any internal links or sensitive data (like PII, private code, client names, site URLs, etc. If you're not sure if something is safe to share, please just ask! 4 | 5 | If you're not an Automattician, welcome! We look forward to your contribution! :heart: 6 | 7 | Please remove this section before submitting the PR. 8 | 9 | --- 10 | 11 | ## Description 12 | 13 | A few sentences describing the overall goals of the Pull Request. 14 | 15 | Should include any special considerations, decisions, and links to relevant GitHub issues. 16 | 17 | Please don't include internal or private links :) 18 | 19 | ## Pull request checklist 20 | 21 | - [ ] Update [SETUP.md](https://github.com/Automattic/vip-cli/blob/trunk/docs/SETUP.md#list-of-environmental-variables) with any new environmental variables. 22 | - [ ] Update [the documentation](https://github.com/Automattic/vip-cli/blob/trunk/docs). 23 | - [ ] [Manually test](https://github.com/Automattic/vip-cli/blob/trunk/docs/TESTING.md#manual-testing) the relevant changes. 24 | - [ ] Follow the [pull request checklist](https://github.com/Automattic/vip-cli/blob/trunk/docs/RELEASING.md#new-pull-requests) 25 | - [ ] Add/update [automated tests](https://github.com/Automattic/vip-cli/blob/trunk/docs/TESTING.md#automated-testing) as needed. 26 | 27 | ## New release checklist 28 | 29 | - [ ] [Automated tests](https://github.com/Automattic/vip-cli/blob/trunk/docs/TESTING.md#automated-testing) pass. 30 | - [ ] The [Preparing for release checklist](https://github.com/Automattic/vip-cli/blob/trunk/docs/RELEASING.md#preparing-for-release) is completed. 31 | 32 | ## Steps to Test 33 | 34 | Outline the steps to test and verify the PR here. 35 | 36 | Example: 37 | 38 | 1. Check out PR. 39 | 1. Run `npm run build` 40 | 1. Run `./dist/bin/vip-cookies.js nom` 41 | 1. Verify cookies are delicious. 42 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | # Enable version updates for npm 9 | - package-ecosystem: 'npm' # See documentation for possible values 10 | directory: '/' # Location of package manifests 11 | schedule: 12 | interval: 'daily' 13 | labels: 14 | - '[Status] Needs Review' 15 | - 'dependencies' 16 | # Allow up to 15 open pull requests at the same time 17 | open-pull-requests-limit: 15 18 | groups: 19 | testing: 20 | patterns: 21 | - '@jest/*' 22 | - 'jest' 23 | - '@types/jest' 24 | babel: 25 | patterns: 26 | - '@babel/*' 27 | typings: 28 | update-types: 29 | - patch 30 | - minor 31 | patterns: 32 | - 'types/*' 33 | 34 | # Enable version updates for GitHub Actions 35 | - package-ecosystem: 'github-actions' # See documentation for possible values 36 | directory: '/' # Location of package manifests 37 | schedule: 38 | interval: 'daily' 39 | labels: 40 | - '[Status] Needs Review' 41 | - 'dependencies' 42 | # Allow up to 15 open pull requests at the same time 43 | open-pull-requests-limit: 15 44 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: CodeQL 2 | 3 | on: 4 | push: 5 | branches: 6 | - trunk 7 | pull_request: 8 | branches: 9 | - trunk 10 | schedule: 11 | - cron: '36 18 * * 5' 12 | 13 | jobs: 14 | analyze: 15 | name: Analyze 16 | runs-on: ubuntu-latest 17 | timeout-minutes: 360 18 | permissions: 19 | security-events: write 20 | actions: read 21 | contents: read 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | language: 26 | - javascript-typescript 27 | - actions 28 | steps: 29 | - name: Checkout repository 30 | uses: actions/checkout@v4 31 | 32 | - name: Initialize CodeQL 33 | uses: github/codeql-action/init@v3 34 | with: 35 | languages: ${{ matrix.language }} 36 | queries: security-and-quality 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v3 40 | with: 41 | category: '/language:${{matrix.language}}' 42 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | name: Dependency Review 2 | 3 | on: 4 | pull_request: 5 | 6 | permissions: 7 | contents: read 8 | pull-requests: write 9 | 10 | jobs: 11 | dependency-review: 12 | runs-on: ubuntu-latest 13 | name: Review Dependencies 14 | steps: 15 | - name: Harden Runner 16 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 17 | with: 18 | egress-policy: block 19 | allowed-endpoints: > 20 | api.deps.dev:443 21 | api.github.com:443 22 | api.securityscorecards.dev:443 23 | github.com:443 24 | 25 | - name: Check out the source code 26 | uses: actions/checkout@v4.1.2 27 | 28 | - name: Review dependencies 29 | uses: actions/dependency-review-action@v4.7.1 30 | with: 31 | comment-summary-in-pr: true 32 | show-openssf-scorecard: true 33 | vulnerability-check: true 34 | -------------------------------------------------------------------------------- /.github/workflows/devenv-e2e.yml: -------------------------------------------------------------------------------- 1 | name: E2E Tests for Dev Env 2 | 3 | on: 4 | push: 5 | workflow_dispatch: 6 | 7 | permissions: 8 | contents: read 9 | 10 | env: 11 | DO_NOT_TRACK: '1' 12 | 13 | jobs: 14 | test: 15 | name: Run E2E Tests, shard ${{ matrix.shard }} 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | shard: 20 | - 1 21 | - 2 22 | - 3 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v4 27 | 28 | - name: Set up Node.js environment 29 | uses: actions/setup-node@v4 30 | with: 31 | node-version: 'lts/*' 32 | cache: npm 33 | cache-dependency-path: npm-shrinkwrap.json 34 | 35 | - name: Install dependencies 36 | run: npm ci 37 | 38 | - name: Pack and install 39 | run: npm pack && npm i -g *.tgz 40 | 41 | - name: Install docker-compose 42 | run: | 43 | REPO="docker/compose" 44 | LATEST_RELEASE=$(curl -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" --silent "https://api.github.com/repos/${REPO}/releases/latest" | jq -r .tag_name) 45 | sudo curl -L "https://github.com/${REPO}/releases/download/${LATEST_RELEASE}/docker-compose-linux-x86_64" -o /usr/local/bin/docker-compose && sudo chmod 0755 /usr/local/bin/docker-compose 46 | 47 | - name: Preload Docker images 48 | run: | 49 | vip dev-env create --app-code image --php 8.2 --mu-plugins image -e false -p true --mailpit true --photon true && \ 50 | vip dev-env start -w && \ 51 | vip dev-env destroy 52 | 53 | - name: Run tests 54 | run: npm run test:e2e:dev-env -- --shard=${{ matrix.shard }}/${{ strategy.job-total }} 55 | -------------------------------------------------------------------------------- /.github/workflows/git-hash-security-check.yml: -------------------------------------------------------------------------------- 1 | name: Git Hash Security Check 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - develop 8 | - trunk 9 | workflow_dispatch: 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | check-git-hash-security: 16 | name: Verify Full Git Commit Hashes 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Check out the source code 20 | uses: actions/checkout@v4 21 | 22 | - name: Check for short git hashes in package.json 23 | uses: Automattic/vip-actions/git-hash-security-check@7b98dcb98d652bf02037977b1b85abb971c345d0 24 | -------------------------------------------------------------------------------- /.github/workflows/npm-prepare-release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Prepare a new npm release 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | # This is copied from npm-publish/action.yml 7 | npm-version-type: 8 | description: 'The npm version type we are publishing.' 9 | required: true 10 | type: choice 11 | default: 'patch' 12 | options: 13 | - patch 14 | - minor 15 | - major 16 | 17 | jobs: 18 | publish: 19 | name: Prepare a new npm release 20 | runs-on: ubuntu-latest 21 | permissions: 22 | contents: write 23 | pull-requests: write 24 | steps: 25 | - name: Check out the source code 26 | uses: actions/checkout@v4 27 | 28 | - name: Run npm-prepare-release 29 | uses: Automattic/vip-actions/npm-prepare-release@1137b91acf0f5ea4e0db044bcf14ceabed9b068f # trunk 30 | with: 31 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | npm-version-type: ${{ inputs.npm-version-type }} 33 | conventional-commits: 'true' 34 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish-prerelease.yml: -------------------------------------------------------------------------------- 1 | name: Publish prerelease to npm 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | npm_tag: 7 | description: 'NPM tag for prerelease' 8 | default: 'next' 9 | 10 | jobs: 11 | publish: 12 | name: Publish prerelease 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: write 16 | id-token: write 17 | pull-requests: write 18 | steps: 19 | - uses: Automattic/vip-actions/npm-publish-prerelease@1137b91acf0f5ea4e0db044bcf14ceabed9b068f # trunk 20 | with: 21 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 23 | PROVENANCE: 'true' 24 | NPM_TAG: ${{ inputs.npm_tag }} 25 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Publish to npm (if applicable) 3 | on: 4 | pull_request: 5 | types: [closed] 6 | 7 | jobs: 8 | publish: 9 | name: Publish to npm 10 | runs-on: ubuntu-latest 11 | if: contains( github.event.pull_request.labels.*.name, '[ Type ] NPM version update' ) && startsWith( github.head_ref, 'release/') && github.event.pull_request.merged == true 12 | permissions: 13 | contents: write 14 | id-token: write 15 | pull-requests: write 16 | steps: 17 | - uses: Automattic/vip-actions/npm-publish@1137b91acf0f5ea4e0db044bcf14ceabed9b068f # trunk 18 | with: 19 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 21 | PROVENANCE: 'true' 22 | CONVENTIONAL_COMMITS: 'true' 23 | -------------------------------------------------------------------------------- /.github/workflows/publish-docs.yml: -------------------------------------------------------------------------------- 1 | name: Publish docs 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*.*.*' 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | publish-docs: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: read 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | - run: npm i -g @automattic/vip 21 | - run: node ./helpers/generate-docs.js > /tmp/docs.json 22 | - name: Print docs.json 23 | run: cat /tmp/docs.json 24 | - name: Send docs to docs site 25 | env: 26 | DOCS_SECRET_TOKEN: ${{ secrets.DOCS_SECRET_TOKEN }} 27 | run: | 28 | curl -X PUT -v \ 29 | -H "Authorization: Bearer $DOCS_SECRET_TOKEN" \ 30 | -H "Content-Type: application/json" \ 31 | --data "@/tmp/docs.json" \ 32 | https://docs.wpvip.com/wp-json/cli-command-reference/v1/ingest/vip-cli 33 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Stale monitor 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | 7 | jobs: 8 | stale: 9 | name: Stale 10 | runs-on: ubuntu-latest 11 | permissions: 12 | issues: write 13 | pull-requests: write 14 | steps: 15 | - uses: Automattic/vip-actions/stale@1137b91acf0f5ea4e0db044bcf14ceabed9b068f # trunk 16 | -------------------------------------------------------------------------------- /.github/workflows/windows-tests.yml: -------------------------------------------------------------------------------- 1 | name: Run VIP-cli tests 2 | 3 | on: pull_request 4 | 5 | env: 6 | NODE_OPTIONS: --unhandled-rejections=warn 7 | DO_NOT_TRACK: '1' 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | Run_windows_tests: 14 | name: Run Windows Tests 15 | runs-on: windows-latest 16 | permissions: 17 | contents: read 18 | steps: 19 | # To prevent issues with fixtures on Windows tests 20 | - name: Set git to use LF 21 | run: | 22 | git config --global core.autocrlf false 23 | git config --global core.eol lf 24 | 25 | - name: Check out repository code 26 | uses: actions/checkout@v4 27 | with: 28 | submodules: true 29 | path: vip 30 | 31 | - name: Setup Node 32 | uses: actions/setup-node@v4 33 | with: 34 | node-version: 'lts/*' 35 | 36 | - name: Install dependencies 37 | run: npm ci 38 | working-directory: vip 39 | 40 | - name: Unit Tests 41 | working-directory: vip 42 | run: npm run jest 43 | 44 | - name: Test Command line 45 | working-directory: vip 46 | run: ./__tests__/e2e_test.bat 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built files 2 | dist 3 | 4 | # Logs 5 | /logs 6 | *.log 7 | npm-debug.log* 8 | 9 | # Dependency directories 10 | node_modules/ 11 | 12 | # Optional eslint cache 13 | .eslintcache 14 | 15 | # jest coverage 16 | coverage 17 | 18 | .DS_Store 19 | .vscode 20 | .idea 21 | *.iml 22 | 23 | schema.gql 24 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Source files 2 | src 3 | 4 | # Tests 5 | __fixtures__ 6 | __tests__ 7 | jest.config.js 8 | jest.setup.js 9 | jest.setupMocks.js 10 | .travis.yml 11 | appveyor.yml 12 | coverage 13 | 14 | # Flow 15 | flow-typed 16 | .flowconfig 17 | 18 | # We don't publish the local config file 19 | config/config.local.json 20 | 21 | # Logs 22 | /logs 23 | *.log 24 | npm-debug.log* 25 | 26 | # Dependency directories 27 | node_modules/ 28 | 29 | # Optional eslint cache 30 | .eslintcache 31 | 32 | # Other helper files 33 | .nvmrc 34 | .ackrc 35 | .publishrc 36 | .prettierrc 37 | 38 | # Liniting rules 39 | .eslintrc.json 40 | .eslintignore 41 | 42 | # Configs 43 | babel.config.js 44 | renovate.json 45 | .github 46 | .gitattributes 47 | 48 | .vscode 49 | .idea 50 | *.iml 51 | 52 | parker.gql 53 | codegen.ts 54 | *.d.ts 55 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /docs/CHANGELOG.md 2 | /__fixtures__/ 3 | /dist/ 4 | /npm-shrinkwrap.json 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | "@automattic/eslint-plugin-wpvip/prettierrc" 2 | -------------------------------------------------------------------------------- /.sonarcloud.properties: -------------------------------------------------------------------------------- 1 | sonar.sources=src, helpers 2 | sonar.tests=__tests__ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Automattic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VIP-CLI 2 | 3 | VIP-CLI is a tool for interacting with and managing your [WordPress VIP applications](https://wpvip.com/). 4 | 5 | ## Further documentation 6 | 7 | - [SETUP.md](https://github.com/Automattic/vip-cli/blob/trunk/docs/SETUP.md) for installation and setup instructions, basic usage and environmental variables. 8 | - [ARCHITECTURE.md](https://github.com/Automattic/vip-cli/blob/trunk/docs/ARCHITECTURE.md) for information on architecture, code structure, data storage and security. 9 | - [CONTRIBUTING.md](https://github.com/Automattic/vip-cli/blob/trunk/docs/CONTRIBUTING.md) for information on how to contribute patches and features, also issue and pull request labels. 10 | - [DEBUGGING.md](https://github.com/Automattic/vip-cli/blob/trunk/docs/DEBUGGING.md) for information on how to debug the software. 11 | - [TESTING.md](https://github.com/Automattic/vip-cli/blob/trunk/docs/TESTING.md) for details on testing the software and individual tasks. 12 | - [RELEASING.md](https://github.com/Automattic/vip-cli/blob/trunk/docs/RELEASING.md) for details on deploying a new release. 13 | - [SECURITY.md](https://github.com/Automattic/vip-cli/blob/trunk/docs/SECURITY.md) for information if you **found a security issue**. 14 | 15 | ## Further information 16 | 17 | - In the [WordPress VIP Lobby](https://lobby.vip.wordpress.com/) find announcements related to [VIP-CLI](https://lobby.vip.wordpress.com/?s=vip-cli) and [API](https://lobby.vip.wordpress.com/?s=vip%20go%20api). 18 | - Find instructions for using [VIP-CLI](https://docs.wpvip.com/vip-cli/) in [WordPress VIP's Documentation](https://docs.wpvip.com/). 19 | - [Changelog](https://github.com/Automattic/vip-cli/blob/trunk/docs/CHANGELOG.md) file for VIP-CLI is available. 20 | - [VIP Cloud Changelog](https://wpvipchangelog.wordpress.com/) logs changes to the [VIP-CLI](https://wpvipchangelog.wordpress.com/?s=cli) and other aspects of the platform. 21 | -------------------------------------------------------------------------------- /__fixtures__/certs/empty.ca: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/vip-cli/88e0dd37c01c3ba403c47841c608c19a98deebb9/__fixtures__/certs/empty.ca -------------------------------------------------------------------------------- /__fixtures__/certs/garbage.ca: -------------------------------------------------------------------------------- 1 | MIIGzDCCBLSgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBpjELMAkGA1UEBhMCVVMx 2 | DjAMBgNVBAgTBVRleGFzMRQwEgYDVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMR 3 | R2xvYmFsU0NBUEUsIEluYy4xFDASBgNVBAsTC0VuZ2luZWVyaW5nMRUwEwYDVQQD 4 | EwxtaWtlLXJvb3QtY2ExKDAmBgkqhkiG9w0BCQEWGW1oYW1iaWRnZUBnbG9iYWxz 5 | Y2FwZS5jb20wHhcNMTAxMTE4MjEyMzA4WhcNMTUxMTE3MjEyMzA4WjCBmDELMAkG 6 | A1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRowGAYDVQQKExFHbG9iYWxTQ0FQRSwg 7 | SW5jLjEUMBIGA1UECxMLRW5naW5lZXJpbmcxHTAbBgNVBAMTFG1pa2UtaW50ZXJt 8 | ZWRpYXRlLWNhMSgwJgYJKoZIhvcNAQkBFhltaGFtYmlkZ2VAZ2xvYmFsc2NhcGUu 9 | Y29tMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAsF+vlQfZnssDsqFx 10 | IXCGHST1jiTHJGGHiiwc9Qb1NPDbyvcdNXvcfkdyYjd8VlYyo3/jnj6xx3PxzJhG 11 | NmnBGJ0I7h/RFJaG7nmGfeWUHCLsVjGfQeEjC++d6zzE3unPOiLVIhv9abD6kISa 12 | hLdltOBcT19mqg1yG4Q4XExjeYmSYGFiDIdv+WwwUssTyPdppaaWcsjNaFcmuopU 13 | RfmcfULPFwvFN6LsgvSTYwYe9l8421fA5c+WiR1JomjGuJT/0sITpzQRCenWi0S0 14 | WZuftT61+fU0/OxINhgO4yK6C1eOoaxmoEG2oVm2o4Bjy9ceYN2UqdRGt8t/23/h 15 | Wog3vEwdoHqghrjeiGWWs98qfzINKokiMd7APcxdkZ1SzyvOEWht4V3/XedleiMx 16 | 8WGjbVtRg/k4Hgf2TGwxcw== 17 | -------------------------------------------------------------------------------- /__fixtures__/certs/split0.ca: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIGzDCCBLSgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBpjELMAkGA1UEBhMCVVMx 3 | DjAMBgNVBAgTBVRleGFzMRQwEgYDVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMR 4 | R2xvYmFsU0NBUEUsIEluYy4xFDASBgNVBAsTC0VuZ2luZWVyaW5nMRUwEwYDVQQD 5 | EwxtaWtlLXJvb3QtY2ExKDAmBgkqhkiG9w0BCQEWGW1oYW1iaWRnZUBnbG9iYWxz 6 | Y2FwZS5jb20wHhcNMTAxMTE4MjEyMzA4WhcNMTUxMTE3MjEyMzA4WjCBmDELMAkG 7 | A1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRowGAYDVQQKExFHbG9iYWxTQ0FQRSwg 8 | SW5jLjEUMBIGA1UECxMLRW5naW5lZXJpbmcxHTAbBgNVBAMTFG1pa2UtaW50ZXJt 9 | ZWRpYXRlLWNhMSgwJgYJKoZIhvcNAQkBFhltaGFtYmlkZ2VAZ2xvYmFsc2NhcGUu 10 | Y29tMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAsF+vlQfZnssDsqFx 11 | IXCGHST1jiTHJGGHiiwc9Qb1NPDbyvcdNXvcfkdyYjd8VlYyo3/jnj6xx3PxzJhG 12 | NmnBGJ0I7h/RFJaG7nmGfeWUHCLsVjGfQeEjC++d6zzE3unPOiLVIhv9abD6kISa 13 | hLdltOBcT19mqg1yG4Q4XExjeYmSYGFiDIdv+WwwUssTyPdppaaWcsjNaFcmuopU 14 | RfmcfULPFwvFN6LsgvSTYwYe9l8421fA5c+WiR1JomjGuJT/0sITpzQRCenWi0S0 15 | WZuftT61+fU0/OxINhgO4yK6C1eOoaxmoEG2oVm2o4Bjy9ceYN2UqdRGt8t/23/h 16 | Wog3vEwdoHqghrjeiGWWs98qfzINKokiMd7APcxdkZ1SzyvOEWht4V3/XedleiMx 17 | 8WGjbVtRg/k4Hgf2TGwxcw== 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /__fixtures__/certs/split1.ca: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIG4jCCBMqgAwIBAgIJAJjguYVnU08GMA0GCSqGSIb3DQEBBQUAMIGmMQswCQYD 3 | VQQGEwJVUzEOMAwGA1UECBMFVGV4YXMxFDASBgNVBAcTC1NhbiBBbnRvbmlvMRow 4 | GAYDVQQKExFHbG9iYWxTQ0FQRSwgSW5jLjEUMBIGA1UECxMLRW5naW5lZXJpbmcx 5 | FTATBgNVBAMTDG1pa2Utcm9vdC1jYTEoMCYGCSqGSIb3DQEJARYZbWhhbWJpZGdl 6 | QGdsb2JhbHNjYXBlLmNvbTAeFw0xMDExMTgyMTE5NDdaFw0xNTExMTcyMTE5NDda 7 | MIGmMQswCQYDVQQGEwJVUzEOMAwGA1UECBMFVGV4YXMxFDASBgNVBAcTC1NhbiBB 8 | bnRvbmlvMRowGAYDVQQKExFHbG9iYWxTQ0FQRSwgSW5jLjEUMBIGA1UECxMLRW5n 9 | aW5lZXJpbmcxFTATBgNVBAMTDG1pa2Utcm9vdC1jYTEoMCYGCSqGSIb3DQEJARYZ 10 | bWhhbWJpZGdlQGdsb2JhbHNjYXBlLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIP 11 | xYK3mO1034kBdDxmVoBeEwfjWWPyC/uyFGwCNZCzoAQGMxNAnj33NBiCLHJRo1Z5 12 | BxirSSMxOT4LEkmkOhuTyKB0TJZf+8wP8pK5BsO3xjO+uM1K3LY= 13 | -----END CERTIFICATE----- 14 | -------------------------------------------------------------------------------- /__fixtures__/certs/test-chain.bundle: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIGzDCCBLSgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBpjELMAkGA1UEBhMCVVMx 3 | DjAMBgNVBAgTBVRleGFzMRQwEgYDVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMR 4 | R2xvYmFsU0NBUEUsIEluYy4xFDASBgNVBAsTC0VuZ2luZWVyaW5nMRUwEwYDVQQD 5 | EwxtaWtlLXJvb3QtY2ExKDAmBgkqhkiG9w0BCQEWGW1oYW1iaWRnZUBnbG9iYWxz 6 | Y2FwZS5jb20wHhcNMTAxMTE4MjEyMzA4WhcNMTUxMTE3MjEyMzA4WjCBmDELMAkG 7 | A1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRowGAYDVQQKExFHbG9iYWxTQ0FQRSwg 8 | SW5jLjEUMBIGA1UECxMLRW5naW5lZXJpbmcxHTAbBgNVBAMTFG1pa2UtaW50ZXJt 9 | ZWRpYXRlLWNhMSgwJgYJKoZIhvcNAQkBFhltaGFtYmlkZ2VAZ2xvYmFsc2NhcGUu 10 | Y29tMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAsF+vlQfZnssDsqFx 11 | IXCGHST1jiTHJGGHiiwc9Qb1NPDbyvcdNXvcfkdyYjd8VlYyo3/jnj6xx3PxzJhG 12 | NmnBGJ0I7h/RFJaG7nmGfeWUHCLsVjGfQeEjC++d6zzE3unPOiLVIhv9abD6kISa 13 | hLdltOBcT19mqg1yG4Q4XExjeYmSYGFiDIdv+WwwUssTyPdppaaWcsjNaFcmuopU 14 | RfmcfULPFwvFN6LsgvSTYwYe9l8421fA5c+WiR1JomjGuJT/0sITpzQRCenWi0S0 15 | WZuftT61+fU0/OxINhgO4yK6C1eOoaxmoEG2oVm2o4Bjy9ceYN2UqdRGt8t/23/h 16 | Wog3vEwdoHqghrjeiGWWs98qfzINKokiMd7APcxdkZ1SzyvOEWht4V3/XedleiMx 17 | 8WGjbVtRg/k4Hgf2TGwxcw== 18 | -----END CERTIFICATE----- 19 | -----BEGIN CERTIFICATE----- 20 | MIIG4jCCBMqgAwIBAgIJAJjguYVnU08GMA0GCSqGSIb3DQEBBQUAMIGmMQswCQYD 21 | VQQGEwJVUzEOMAwGA1UECBMFVGV4YXMxFDASBgNVBAcTC1NhbiBBbnRvbmlvMRow 22 | GAYDVQQKExFHbG9iYWxTQ0FQRSwgSW5jLjEUMBIGA1UECxMLRW5naW5lZXJpbmcx 23 | FTATBgNVBAMTDG1pa2Utcm9vdC1jYTEoMCYGCSqGSIb3DQEJARYZbWhhbWJpZGdl 24 | QGdsb2JhbHNjYXBlLmNvbTAeFw0xMDExMTgyMTE5NDdaFw0xNTExMTcyMTE5NDda 25 | MIGmMQswCQYDVQQGEwJVUzEOMAwGA1UECBMFVGV4YXMxFDASBgNVBAcTC1NhbiBB 26 | bnRvbmlvMRowGAYDVQQKExFHbG9iYWxTQ0FQRSwgSW5jLjEUMBIGA1UECxMLRW5n 27 | aW5lZXJpbmcxFTATBgNVBAMTDG1pa2Utcm9vdC1jYTEoMCYGCSqGSIb3DQEJARYZ 28 | bWhhbWJpZGdlQGdsb2JhbHNjYXBlLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIP 29 | xYK3mO1034kBdDxmVoBeEwfjWWPyC/uyFGwCNZCzoAQGMxNAnj33NBiCLHJRo1Z5 30 | BxirSSMxOT4LEkmkOhuTyKB0TJZf+8wP8pK5BsO3xjO+uM1K3LY= 31 | -----END CERTIFICATE----- 32 | -------------------------------------------------------------------------------- /__fixtures__/client-file-uploader/emptyfile.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/vip-cli/88e0dd37c01c3ba403c47841c608c19a98deebb9/__fixtures__/client-file-uploader/emptyfile.txt -------------------------------------------------------------------------------- /__fixtures__/client-file-uploader/tinyfile.txt: -------------------------------------------------------------------------------- 1 | ohai you! 2 | 3 | I'm purty smol 4 | -------------------------------------------------------------------------------- /__fixtures__/custom-deploy/invalid-file-chars.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/vip-cli/88e0dd37c01c3ba403c47841c608c19a98deebb9/__fixtures__/custom-deploy/invalid-file-chars.zip -------------------------------------------------------------------------------- /__fixtures__/custom-deploy/no-root-folder.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/vip-cli/88e0dd37c01c3ba403c47841c608c19a98deebb9/__fixtures__/custom-deploy/no-root-folder.zip -------------------------------------------------------------------------------- /__fixtures__/custom-deploy/no-themes-folder.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/vip-cli/88e0dd37c01c3ba403c47841c608c19a98deebb9/__fixtures__/custom-deploy/no-themes-folder.zip -------------------------------------------------------------------------------- /__fixtures__/custom-deploy/valid-zip-posix.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/vip-cli/88e0dd37c01c3ba403c47841c608c19a98deebb9/__fixtures__/custom-deploy/valid-zip-posix.zip -------------------------------------------------------------------------------- /__fixtures__/custom-deploy/valid-zip-win32.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/vip-cli/88e0dd37c01c3ba403c47841c608c19a98deebb9/__fixtures__/custom-deploy/valid-zip-win32.zip -------------------------------------------------------------------------------- /__fixtures__/dev-env-e2e/empty.sql: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/vip-cli/88e0dd37c01c3ba403c47841c608c19a98deebb9/__fixtures__/dev-env-e2e/empty.sql -------------------------------------------------------------------------------- /__fixtures__/dev-env-e2e/fail-autoIncrement-validation.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS `wp_a8c_cron_control_jobs`; 2 | 3 | CREATE TABLE `wp_a8c_cron_control_jobs` ( 4 | `ID` bigint unsigned , 5 | `timestamp` bigint unsigned NOT NULL, 6 | `action` varchar(255) NOT NULL, 7 | `action_hashed` varchar(32) NOT NULL, 8 | `instance` varchar(32) NOT NULL, 9 | `args` longtext NOT NULL, 10 | `schedule` varchar(255) DEFAULT NULL, 11 | `interval` int unsigned DEFAULT '0', 12 | `status` varchar(32) NOT NULL DEFAULT 'pending', 13 | `created` datetime NOT NULL, 14 | `last_modified` datetime NOT NULL, 15 | PRIMARY KEY (`ID`), 16 | UNIQUE KEY `ts_action_instance_status` (`timestamp`,`action`(191),`instance`,`status`), 17 | KEY `status` (`status`) 18 | ) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; 19 | -------------------------------------------------------------------------------- /__fixtures__/dev-env-e2e/fail-autoIncrement-validation.sql.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/vip-cli/88e0dd37c01c3ba403c47841c608c19a98deebb9/__fixtures__/dev-env-e2e/fail-autoIncrement-validation.sql.gz -------------------------------------------------------------------------------- /__fixtures__/dev-env-e2e/fail-validation.sql: -------------------------------------------------------------------------------- 1 | REPLACE INTO wp_options (option_name, option_value, autoload) VALUES ('e2etest', '200', 'no'); 2 | -------------------------------------------------------------------------------- /__fixtures__/dev-env-e2e/fail-validation.sql.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/vip-cli/88e0dd37c01c3ba403c47841c608c19a98deebb9/__fixtures__/dev-env-e2e/fail-validation.sql.gz -------------------------------------------------------------------------------- /__fixtures__/dev-env-e2e/instance_data_mariadb.json: -------------------------------------------------------------------------------- 1 | {"wpTitle":"VIP Dev","multisite":false,"elasticsearch":false,"php":"ghcr.io/automattic/vip-container-images/php-fpm:8.2","mariadb":"10.3","mediaRedirectDomain":"","wordpress":{"mode":"image","tag":"6.6"},"muPlugins":{"mode":"image"},"appCode":{"mode":"image"},"phpmyadmin":false,"xdebug":false,"xdebugConfig":"","siteSlug":"vip-local"} -------------------------------------------------------------------------------- /__fixtures__/dev-env-e2e/mydumper-detection.expected.sql: -------------------------------------------------------------------------------- 1 | 2 | -- metadata.header -1 3 | # Started dump at: 2024-07-26 03:00:36 4 | [config] 5 | quote_character = BACKTICK 6 | 7 | [myloader_session_variables] 8 | SQL_MODE='NO_AUTO_VALUE_ON_ZERO,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION' /*!40101 9 | 10 | 11 | -- some_db-schema-create.sql -1 12 | /*!40101 SET NAMES utf8mb4*/; 13 | /*!40014 SET FOREIGN_KEY_CHECKS=0*/; 14 | /*!40101 SET SQL_MODE='NO_AUTO_VALUE_ON_ZERO,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'*/; 15 | /*!40103 SET TIME_ZONE='+00:00' */; 16 | CREATE DATABASE /*!32312 IF NOT EXISTS*/ `some_db` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci */ /*!80016 DEFAULT ENCRYPTION='N' */; 17 | -------------------------------------------------------------------------------- /__fixtures__/dev-env-e2e/mydumper-detection.sql: -------------------------------------------------------------------------------- 1 | 2 | -- metadata.header 198 3 | # Started dump at: 2024-07-26 03:00:36 4 | [config] 5 | quote_character = BACKTICK 6 | 7 | [myloader_session_variables] 8 | SQL_MODE='NO_AUTO_VALUE_ON_ZERO,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION' /*!40101 9 | 10 | 11 | -- some_db-schema-create.sql 370 12 | /*!40101 SET NAMES utf8mb4*/; 13 | /*!40014 SET FOREIGN_KEY_CHECKS=0*/; 14 | /*!40101 SET SQL_MODE='NO_AUTO_VALUE_ON_ZERO,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'*/; 15 | /*!40103 SET TIME_ZONE='+00:00' */; 16 | CREATE DATABASE /*!32312 IF NOT EXISTS*/ `some_db` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci */ /*!80016 DEFAULT ENCRYPTION='N' */; 17 | -------------------------------------------------------------------------------- /__fixtures__/dev-env-e2e/mydumper-detection.sql.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/vip-cli/88e0dd37c01c3ba403c47841c608c19a98deebb9/__fixtures__/dev-env-e2e/mydumper-detection.sql.gz -------------------------------------------------------------------------------- /__fixtures__/dev-env-e2e/mysqldump-detection.sql: -------------------------------------------------------------------------------- 1 | -- MySQL dump 10.13 Distrib 8.0.28, for Linux (x86_64) 2 | -- 3 | -- Host: localhost Database: some_db 4 | -- ------------------------------------------------------ 5 | -- Server version 8.0.28 6 | 7 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; 8 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; 9 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; 10 | /*!50503 SET NAMES utf8mb4 */; 11 | /*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; 12 | /*!40103 SET TIME_ZONE='+00:00' */; 13 | /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; 14 | /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; 15 | /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; 16 | /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; 17 | -------------------------------------------------------------------------------- /__fixtures__/dev-env-e2e/mysqldump-detection.sql.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/vip-cli/88e0dd37c01c3ba403c47841c608c19a98deebb9/__fixtures__/dev-env-e2e/mysqldump-detection.sql.gz -------------------------------------------------------------------------------- /__fixtures__/search-replace-binaries/go-search-replace-test-darwin-arm64: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/vip-cli/88e0dd37c01c3ba403c47841c608c19a98deebb9/__fixtures__/search-replace-binaries/go-search-replace-test-darwin-arm64 -------------------------------------------------------------------------------- /__fixtures__/search-replace-binaries/go-search-replace-test-darwin-x64: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/vip-cli/88e0dd37c01c3ba403c47841c608c19a98deebb9/__fixtures__/search-replace-binaries/go-search-replace-test-darwin-x64 -------------------------------------------------------------------------------- /__fixtures__/search-replace-binaries/go-search-replace-test-linux-x64: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/vip-cli/88e0dd37c01c3ba403c47841c608c19a98deebb9/__fixtures__/search-replace-binaries/go-search-replace-test-linux-x64 -------------------------------------------------------------------------------- /__fixtures__/search-replace-binaries/go-search-replace-test-win32-x64.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/vip-cli/88e0dd37c01c3ba403c47841c608c19a98deebb9/__fixtures__/search-replace-binaries/go-search-replace-test-win32-x64.exe -------------------------------------------------------------------------------- /__fixtures__/validations/bad-sql-dev-env.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE automatticians; 2 | 3 | -- for dev-env you should not switch database 4 | USE automatticians; 5 | 6 | INSERT INTO wp_options (option_name, option_value, autoload) 7 | VALUES 8 | ('siteurl', 'https://super-employees-go.vip.net', 'yes'), 9 | ('home', 'https://super-empoyees.com', 'yes'), 10 | ('home', 'home', 'yes'); -------------------------------------------------------------------------------- /__fixtures__/validations/bad-sql-dump.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE automatticians; 2 | 3 | USE automatticians; 4 | 5 | -- DROP TABLE IF EXISTS `employees`; 6 | 7 | -- CREATE TABLE `employees` ( 8 | -- id INT(10) UNSIGNED NOT NULL AUTO_INCREMENTS, 9 | -- employeeNumber VARCHAR(255) NOT NULL, 10 | -- firstName VARCHAR(255) NOT NULL, 11 | -- lastName VARCHAR(255) NOT NULL, 12 | -- PRIMARY KEY (id), 13 | -- UNIQUE KEY (employeeNumber) 14 | -- ) ENGINE=MyISAM DEFAULT CHARSET=utf8; 15 | 16 | CREATE TRIGGER before_employee_update 17 | BEFORE UPDATE ON employees 18 | FOR EACH ROW 19 | INSERT INTO employee_audit 20 | SET action = 'update', 21 | employeeNumber = OLD.employeeNumber, 22 | lastname = OLD.lastname, 23 | changedat = NOW(); 24 | 25 | ALTER USER USER() IDENTIFIED BY 'auth_string'; 26 | 27 | DROP DATABASE countrycodes; 28 | 29 | INSERT INTO wp_options (option_name, option_value, autoload) 30 | VALUES 31 | ('siteurl', 'https://super-employees-go.vip.net', 'yes'), 32 | ('home', 'https://super-empoyees.com', 'yes'); 33 | 34 | SET @@SESSION.SQL_LOG_BIN= 0; 35 | 36 | ALTER TABLE wp_options 37 | ADD PRIMARY KEY (`option_id`), 38 | ADD UNIQUE KEY `option_name` (`option_name`), 39 | ADD KEY `autoload` (`autoload`); 40 | 41 | SET UNIQUE_CHECKS = 0; 42 | -------------------------------------------------------------------------------- /__fixtures__/validations/bad-sql-dump.sql.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/vip-cli/88e0dd37c01c3ba403c47841c608c19a98deebb9/__fixtures__/validations/bad-sql-dump.sql.gz -------------------------------------------------------------------------------- /__fixtures__/validations/bad-sql-duplicate-tables.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `wp_users` ( 2 | id INT(10) UNSIGNED NOT NULL AUTO_INCREMENTS, 3 | PRIMARY KEY(id); 4 | ) ENGINER=InnoDB DEFAULT CHARSET=utf8; 5 | 6 | CREATE TABLE `wp_users` ( 7 | id INT(10) UNSIGNED NOT NULL AUTO_INCREMENTS, 8 | PRIMARY KEY(id); 9 | ) ENGINER=InnoDB DEFAULT CHARSET=utf8; 10 | -------------------------------------------------------------------------------- /__fixtures__/validations/empty.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/vip-cli/88e0dd37c01c3ba403c47841c608c19a98deebb9/__fixtures__/validations/empty.zip -------------------------------------------------------------------------------- /__fixtures__/validations/multiline-statements.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO `wp_site` (`id`, `domain`, `path`) 2 | VALUES 3 | (1,'www.example.com','/'); 4 | 5 | INSERT INTO wp_blogs VALUES (1,1,'www.example.com','/','2022-07-25 00:00:00','2022-07-25 00:00:00',1,0,0,0,0,0); 6 | 7 | INSERT INTO wp_blogs (blog_id, site_id, domain, path, registered, last_updated, public, archived, mature, spam, deleted, lang_id) 8 | VALUES 9 | (1,1,'www.example.com','/','2022-07-25 00:00:00','2022-07-25 00:00:00',1,0,0,0,0,0); 10 | 11 | INSERT INTO wp_blogs (blog_id, site_id, domain, path, registered, last_updated, public, archived, mature, spam, deleted, lang_id) 12 | VALUES 13 | (1,1,'www.example.com','/','2022-07-25 00:00:00','2022-07-25 00:00:00',1,0,0,0,0,0), 14 | (2,1,'www.example.com','/2/','2022-07-25 00:00:00','2022-07-25 00:00:00',1,0,0,0,0,0); 15 | 16 | INSERT INTO wp_blogs (blog_id, site_id, domain, path, registered, last_updated, public, archived, mature, spam, deleted, lang_id) 17 | VALUES 18 | (1,1,'www.example.com','/','2022-07-25 00:00:00','2022-07-25 00:00:00',1,0,0,0,0,0), 19 | (2,1,'www.example.com','/2/','2022-07-25 00:00:00','2022-07-25 00:00:00',1,0,0,0,0,0), 20 | (3,1,'www.example.com','/3/','2022-07-25 00:00:00','2022-07-25 00:00:00',1,0,0,0,0,0); 21 | 22 | 23 | -------------------------------------------------------------------------------- /__tests__/bin/vip-app-deploy-validate.e2e.js: -------------------------------------------------------------------------------- 1 | import * as exit from '../../src/lib/cli/exit'; 2 | import { validateZipFile } from '../../src/lib/validations/custom-deploy'; 3 | 4 | const exitSpy = jest.spyOn( exit, 'withError' ); 5 | jest.spyOn( process, 'exit' ).mockImplementation( () => {} ); 6 | console.error = jest.fn(); 7 | 8 | describe( 'vip-app-deploy-validate e2e', () => { 9 | beforeEach( async () => { 10 | jest.clearAllMocks(); 11 | } ); 12 | 13 | describe( 'validateZipFile', () => { 14 | it.each( [ 15 | // Archive: __fixtures__/custom-deploy/valid-zip-posix.zip 16 | // __MACOSX/ 17 | // mysite/ 18 | // mysite/.DS_Store 19 | // mysite/__MACOSX 20 | // mysite/themes 21 | // mysite/themes/.DS_Store 22 | // mysite/themes/__MACOSX 23 | // mysite/themes/mytheme.php 24 | '__fixtures__/custom-deploy/valid-zip-posix.zip', 25 | 26 | // Archive: __fixtures__/custom-deploy/valid-zip-win32.zip 27 | // mysite/ 28 | // mysite/themes 29 | // mysite/themes/mytheme.php 30 | '__fixtures__/custom-deploy/valid-zip-win32.zip', 31 | ] )( 'should not throw error for valid zip file: %s', async file => { 32 | await validateZipFile( file ); 33 | 34 | expect( exitSpy ).not.toHaveBeenCalled(); 35 | } ); 36 | 37 | it.each( [ 38 | { 39 | // Archive: __fixtures__/custom-deploy/invalid-file-chars.zip 40 | // mysite/ 41 | // mysite/themes 42 | // mysite/themes/invalid-file-name?.txt 43 | file: '__fixtures__/custom-deploy/invalid-file-chars.zip', 44 | error: `Filename invalid-file-name?.txt contains disallowed characters: [!/:*?"<>|'/^..]+`, 45 | }, 46 | { 47 | // Archive: __fixtures__/custom-deploy/no-root-folder.zip 48 | // no-root-folder.txt 49 | file: '__fixtures__/custom-deploy/no-root-folder.zip', 50 | error: `The compressed file must contain a single root directory.`, 51 | }, 52 | { 53 | // Archive: __fixtures__/custom-deploy/no-themes-folder.zip 54 | // mysite/ 55 | // mysite/file 56 | file: '__fixtures__/custom-deploy/no-themes-folder.zip', 57 | error: `Missing \`themes\` directory from root folder.`, 58 | }, 59 | ] )( 'should throw an error for invalid zip file - $file', async ( { file, error } ) => { 60 | await validateZipFile( file ); 61 | 62 | expect( exitSpy ).toHaveBeenCalledWith( error ); 63 | } ); 64 | } ); 65 | } ); 66 | -------------------------------------------------------------------------------- /__tests__/commands/phpmyadmin.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-return */ 2 | /** 3 | * External dependencies 4 | */ 5 | import { beforeEach, describe, expect, it, jest } from '@jest/globals'; 6 | 7 | /** 8 | * Internal dependencies 9 | */ 10 | import { PhpMyAdminCommand } from '../../src/commands/phpmyadmin'; 11 | import API from '../../src/lib/api'; 12 | import { CommandTracker } from '../../src/lib/tracker'; 13 | 14 | const generatePMAAccessMutationMock = jest.fn( async () => { 15 | return Promise.resolve( { 16 | data: { 17 | generatePHPMyAdminAccess: { 18 | url: 'http://test-url.com', 19 | }, 20 | }, 21 | } ); 22 | } ); 23 | 24 | const enablePMAMutationMock = jest.fn( async () => { 25 | return Promise.resolve( { 26 | data: { 27 | enablePHPMyAdmin: { 28 | success: true, 29 | }, 30 | }, 31 | } ); 32 | } ); 33 | 34 | const pmaEnabledQueryMockTrue = jest.fn( async () => { 35 | return Promise.resolve( { 36 | data: { 37 | app: { 38 | environments: [ 39 | { 40 | phpMyAdminStatus: { 41 | status: 'enabled', 42 | }, 43 | }, 44 | ], 45 | }, 46 | }, 47 | } ); 48 | } ); 49 | 50 | jest.mock( '../../src/lib/api' ); 51 | jest.mocked( API ).mockImplementation( 52 | () => 53 | ( { 54 | mutate: generatePMAAccessMutationMock, 55 | query: pmaEnabledQueryMockTrue, 56 | } as any ) 57 | ); 58 | 59 | describe( 'commands/PhpMyAdminCommand', () => { 60 | beforeEach( () => {} ); 61 | 62 | describe( '.run', () => { 63 | const app = { id: 123 }; 64 | const env = { id: 456, jobs: [] }; 65 | const tracker = jest.fn() as CommandTracker; 66 | const cmd = new PhpMyAdminCommand( app, env, tracker ); 67 | const openUrl = jest.spyOn( cmd, 'openUrl' ); 68 | 69 | beforeEach( () => { 70 | openUrl.mockReset(); 71 | } ); 72 | 73 | it( 'should open the generated URL in browser', async () => { 74 | await cmd.run(); 75 | expect( pmaEnabledQueryMockTrue ).toHaveBeenCalledWith( { 76 | query: expect.anything(), 77 | variables: { 78 | appId: 123, 79 | envId: 456, 80 | }, 81 | fetchPolicy: 'network-only', 82 | } ); 83 | expect( enablePMAMutationMock ).not.toHaveBeenCalled(); 84 | expect( generatePMAAccessMutationMock ).toHaveBeenCalledWith( { 85 | mutation: expect.anything(), 86 | variables: { 87 | input: { 88 | environmentId: 456, 89 | }, 90 | }, 91 | } ); 92 | expect( openUrl ).toHaveBeenCalledWith( 'http://test-url.com' ); 93 | } ); 94 | } ); 95 | } ); 96 | -------------------------------------------------------------------------------- /__tests__/devenv-e2e/009-import-media.spec.js: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, jest } from '@jest/globals'; 2 | import { mkdtemp, rm, stat } from 'node:fs/promises'; 3 | import os from 'node:os'; 4 | import path from 'node:path'; 5 | import xdgBaseDir from 'xdg-basedir'; 6 | 7 | import { CliTest } from './helpers/cli-test'; 8 | import { vipDevEnvCreate, vipDevEnvImportMedia } from './helpers/commands'; 9 | import { checkEnvExists, getProjectSlug, prepareEnvironment } from './helpers/utils'; 10 | import { getEnvironmentPath } from '../../src/lib/dev-environment/dev-environment-core'; 11 | 12 | jest.setTimeout( 30 * 1000 ).retryTimes( 1, { logErrorsBeforeRetry: true } ); 13 | 14 | describe( 'vip dev-env import media', () => { 15 | /** @type {CliTest} */ 16 | let cliTest; 17 | /** @type {NodeJS.ProcessEnv} */ 18 | let env; 19 | /** @type {string} */ 20 | let tmpPath; 21 | 22 | beforeAll( async () => { 23 | cliTest = new CliTest(); 24 | 25 | tmpPath = await mkdtemp( path.join( os.tmpdir(), 'vip-dev-env-' ) ); 26 | xdgBaseDir.data = tmpPath; 27 | 28 | env = prepareEnvironment( tmpPath ); 29 | } ); 30 | 31 | afterAll( () => rm( tmpPath, { recursive: true, force: true } ) ); 32 | 33 | it( 'should fail if environment does not exist', async () => { 34 | const slug = getProjectSlug(); 35 | expect( await checkEnvExists( slug ) ).toBe( false ); 36 | 37 | const result = await cliTest.spawn( 38 | [ process.argv[ 0 ], vipDevEnvImportMedia, '--slug', slug, __dirname ], 39 | { env } 40 | ); 41 | expect( result.rc ).toBeGreaterThan( 0 ); 42 | expect( result.stderr ).toContain( "Error: Environment doesn't exist." ); 43 | } ); 44 | 45 | it( 'should copy files if environment exists', async () => { 46 | const slug = getProjectSlug(); 47 | expect( await checkEnvExists( slug ) ).toBe( false ); 48 | 49 | let result = await cliTest.spawn( 50 | [ process.argv[ 0 ], vipDevEnvCreate, '--slug', slug ], 51 | { env }, 52 | true 53 | ); 54 | expect( result.rc ).toBe( 0 ); 55 | expect( await checkEnvExists( slug ) ).toBe( true ); 56 | 57 | result = await cliTest.spawn( 58 | [ process.argv[ 0 ], vipDevEnvImportMedia, '--slug', slug, __dirname ], 59 | { env }, 60 | true 61 | ); 62 | expect( result.rc ).toBe( 0 ); 63 | 64 | const uploadsPath = path.join( getEnvironmentPath( slug ), 'uploads' ); 65 | const file = path.join( uploadsPath, path.basename( __filename ) ); 66 | 67 | return expect( stat( file ) ).resolves.toBeTruthy(); 68 | } ); 69 | } ); 70 | -------------------------------------------------------------------------------- /__tests__/devenv-e2e/helpers/cli-test.js: -------------------------------------------------------------------------------- 1 | import { spawn } from 'child_process'; 2 | 3 | /** 4 | * @typedef {Object} CliResult 5 | * @property {string} stdout Standard output 6 | * @property {string} stderr Standard error 7 | * @property {number} rc Return code 8 | */ 9 | export class CliTest { 10 | /** 11 | * @param {string[]} args Command and its arguments 12 | * @param {Object} options Spawn options 13 | * @param {boolean} printStderrOnError Whether to print stderr on error 14 | * @return {Promise} Return value of the command 15 | */ 16 | spawn( args, options, printStderrOnError = false ) { 17 | const [ command, ...commandArgs ] = args; 18 | 19 | let stdout = ''; 20 | let stderr = ''; 21 | let finished = false; 22 | 23 | return new Promise( ( resolve, reject ) => { 24 | const child = spawn( command, commandArgs, options ); 25 | child.stdout.setEncoding( 'utf8' ); 26 | child.stderr.setEncoding( 'utf8' ); 27 | 28 | child.stdout.on( 'data', data => { 29 | stdout += String( data ); 30 | } ); 31 | 32 | child.stderr.on( 'data', data => { 33 | stderr += String( data ); 34 | } ); 35 | 36 | child.on( 'exit', code => { 37 | if ( ! finished ) { 38 | finished = true; 39 | const rc = code === null ? -1 : code; 40 | 41 | if ( rc && printStderrOnError ) { 42 | console.error( stderr ); 43 | } 44 | 45 | resolve( { stdout, stderr, rc } ); 46 | } 47 | } ); 48 | 49 | child.on( 'error', err => { 50 | if ( ! finished ) { 51 | finished = true; 52 | reject( err ); 53 | } 54 | } ); 55 | } ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /__tests__/devenv-e2e/helpers/commands.js: -------------------------------------------------------------------------------- 1 | import { join, resolve } from 'node:path'; 2 | 3 | const vipPath = resolve( __dirname, '../../../dist/bin' ); 4 | 5 | export const vipDevEnvCreate = join( vipPath, 'vip-dev-env-create.js' ); 6 | export const vipDevEnvDestroy = join( vipPath, 'vip-dev-env-destroy.js' ); 7 | export const vipDevEnvExec = join( vipPath, 'vip-dev-env-exec.js' ); 8 | export const vipDevEnvImportMedia = join( vipPath, 'vip-dev-env-import-media.js' ); 9 | export const vipDevEnvImportSQL = join( vipPath, 'vip-dev-env-import-sql.js' ); 10 | export const vipDevEnvInfo = join( vipPath, 'vip-dev-env-info.js' ); 11 | export const vipDevEnvList = join( vipPath, 'vip-dev-env-list.js' ); 12 | export const vipDevEnvShell = join( vipPath, 'vip-dev-env-shell.js' ); 13 | export const vipDevEnvStart = join( vipPath, 'vip-dev-env-start.js' ); 14 | export const vipDevEnvStop = join( vipPath, 'vip-dev-env-stop.js' ); 15 | export const vipDevEnvUpdate = join( vipPath, 'vip-dev-env-update.js' ); 16 | export const vipDevEnvLogs = join( vipPath, 'vip-dev-env-logs.js' ); 17 | -------------------------------------------------------------------------------- /__tests__/devenv-e2e/helpers/docker-utils.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable id-length */ 2 | 3 | import { dockerComposify } from 'lando/lib/utils'; 4 | 5 | /** 6 | * @typedef {import('dockerode')} Docker 7 | * @typedef {import('dockerode').ContainerInfo} ContainerInfo 8 | */ 9 | 10 | /** 11 | * @param {Docker} docker Docker instance 12 | * @param {string} project Project slug 13 | * @return {Promise} List of containers 14 | */ 15 | export function getContainersForProject( docker, project ) { 16 | const prefix = dockerComposify( project ); 17 | return docker.listContainers( { 18 | filters: { 19 | label: [ `com.docker.compose.project=${ prefix }` ], 20 | }, 21 | } ); 22 | } 23 | 24 | /** 25 | * @param {Docker} docker Docker instance 26 | * @param {string[]} ids List of container IDs to kill 27 | */ 28 | export async function killContainers( docker, ids ) { 29 | const containers = ids.map( id => docker.getContainer( id ) ); 30 | await Promise.all( containers.map( container => container.remove( { force: true, v: true } ) ) ); 31 | } 32 | 33 | /** 34 | * @param {Docker} docker Docker instance 35 | * @param {string|undefined} project Project slug 36 | */ 37 | export async function killProjectContainers( docker, project ) { 38 | if ( project ) { 39 | const containers = await getContainersForProject( docker, project ); 40 | const ids = containers.map( containerInfo => containerInfo.Id ); 41 | await killContainers( docker, ids ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /__tests__/devenv-e2e/jest/jest.config.js: -------------------------------------------------------------------------------- 1 | const path = require( 'node:path' ); 2 | 3 | /** @type {import('jest').Config} */ 4 | const config = { 5 | verbose: true, 6 | slowTestThreshold: 30, 7 | testRegex: [ '\\.(test|spec)\\.js$' ], 8 | rootDir: path.join( __dirname, '..', '..', '..' ), 9 | roots: [ '/__tests__/devenv-e2e' ], 10 | reporters: [ 'default', 'github-actions' ], 11 | testTimeout: 120000, 12 | maxWorkers: process.env.CI ? 1 : 2, 13 | testSequencer: path.join( __dirname, 'sequencer.js' ), 14 | forceExit: true, 15 | }; 16 | 17 | module.exports = config; 18 | -------------------------------------------------------------------------------- /__tests__/devenv-e2e/jest/sequencer.js: -------------------------------------------------------------------------------- 1 | const Sequencer = require( '@jest/test-sequencer' ).default; 2 | 3 | /** 4 | * @typedef {import('@jest/test-sequencer').ShardOptions} ShardOptions 5 | * @typedef {import('@jest/test-result').Test} Test 6 | */ 7 | 8 | class TestSequencer extends Sequencer { 9 | /** 10 | * @param {Test[]} tests All tests 11 | * @param {ShardOptions} options Sharding options 12 | * @return {Test[]} Chunk 13 | */ 14 | shard( tests, options ) { 15 | const { shardIndex, shardCount } = options; 16 | const shardSize = Math.ceil( tests.length / shardCount ); 17 | const shardStart = shardSize * ( shardIndex - 1 ); 18 | const shardEnd = shardSize * shardIndex; 19 | 20 | return [ ...tests ] 21 | .sort( ( lhs, rhs ) => ( lhs.path > rhs.path ? 1 : -1 ) ) 22 | .slice( shardStart, shardEnd ); 23 | } 24 | 25 | /** 26 | * @param {Test[]} tests Tests 27 | * @return {Test[]} Sorted tests 28 | */ 29 | sort( tests ) { 30 | return tests.sort( ( testA, testB ) => ( testA.path > testB.path ? 1 : -1 ) ); 31 | } 32 | } 33 | 34 | module.exports = TestSequencer; 35 | -------------------------------------------------------------------------------- /__tests__/e2e_test.bat: -------------------------------------------------------------------------------- 1 | call npm pack 2 | 3 | echo "== Installing vip" 4 | 5 | FOR /R "." %%f IN ( *.tgz) DO ( 6 | call npm i -g %%f 7 | ) 8 | 9 | echo "== Running e2e tests" 10 | 11 | call vip --help 12 | 13 | rem dev-env tests 14 | 15 | echo "== Creating dev-env" 16 | 17 | call vip dev-env create --app-code image --title Test --multisite false --php 8.2 --wordpress trunk --mu-plugins image -e false -p false -x false --mailpit false --photon false 18 | 19 | if NOT %errorlevel% == 0 ( 20 | echo "== Failed to create dev-env" 21 | exit 1 22 | ) 23 | 24 | call ls -al C:\Users\runneradmin\.local\share\vip\dev-environment\vip-local 25 | if NOT %errorlevel% == 0 ( 26 | echo "== local environment folder not found" 27 | exit 1 28 | ) 29 | 30 | call cat C:\Users\runneradmin\.local\share\vip\dev-environment\vip-local\.lando.yml 31 | if NOT %errorlevel% == 0 ( 32 | echo "== local environment lando config not found" 33 | exit 1 34 | ) 35 | -------------------------------------------------------------------------------- /__tests__/lib/analytics/index.js: -------------------------------------------------------------------------------- 1 | import { afterEach, describe, expect, it, jest } from '@jest/globals'; 2 | 3 | import Analytics from '../../../src/lib/analytics'; 4 | 5 | class AnalyticsClientStub { 6 | trackEvent() { 7 | return Promise.resolve( false ); 8 | } 9 | } 10 | 11 | describe( 'lib/analytics', () => { 12 | describe( '.trackEvent()', () => { 13 | const OLD_ENV = process.env; 14 | 15 | afterEach( () => { 16 | process.env = OLD_ENV; 17 | } ); 18 | 19 | it( 'should track events for all clients', async () => { 20 | delete process.env.DO_NOT_TRACK; 21 | const stubClient1 = new AnalyticsClientStub(); 22 | const stubClient1Spy = jest.spyOn( stubClient1, 'trackEvent' ); 23 | const stubClient2 = new AnalyticsClientStub(); 24 | const stubClient2Spy = jest.spyOn( stubClient2, 'trackEvent' ); 25 | const analytics = new Analytics( [ stubClient1, stubClient2 ] ); 26 | 27 | const result = analytics.trackEvent( 'test_event', {} ); 28 | 29 | await expect( result ).resolves.toStrictEqual( [ false, false ] ); 30 | expect( stubClient1Spy ).toHaveBeenCalledTimes( 1 ); 31 | expect( stubClient2Spy ).toHaveBeenCalledTimes( 1 ); 32 | } ); 33 | 34 | it( 'should not track events when DO_NOT_TRACK is set', async () => { 35 | process.env.DO_NOT_TRACK = 1; 36 | 37 | const stubClient = new AnalyticsClientStub(); 38 | const stubClientSpy = jest.spyOn( stubClient, 'trackEvent' ); 39 | const analytics = new Analytics( [ stubClient ] ); 40 | 41 | const result = analytics.trackEvent( 'test_event', {} ); 42 | 43 | await expect( result ).resolves.toStrictEqual( [] ); 44 | expect( stubClientSpy ).toHaveBeenCalledTimes( 0 ); 45 | } ); 46 | } ); 47 | } ); 48 | -------------------------------------------------------------------------------- /__tests__/lib/app-logs/app-logs.js: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | import API from '../../../src/lib/api'; 4 | import { getRecentLogs } from '../../../src/lib/app-logs/app-logs'; 5 | 6 | jest.mock( '../../../src/lib/api', () => jest.fn() ); 7 | 8 | const EXPECTED_QUERY = gql` 9 | query GetAppLogs( 10 | $appId: Int 11 | $envId: Int 12 | $type: AppEnvironmentLogType 13 | $limit: Int 14 | $after: String 15 | ) { 16 | app(id: $appId) { 17 | environments(id: $envId) { 18 | id 19 | logs(type: $type, limit: $limit, after: $after) { 20 | nodes { 21 | timestamp 22 | message 23 | } 24 | nextCursor 25 | pollingDelaySeconds 26 | } 27 | } 28 | } 29 | } 30 | `; 31 | 32 | describe( 'getRecentLogs()', () => { 33 | beforeEach( jest.clearAllMocks ); 34 | 35 | it( 'should query the API with the correct values', async () => { 36 | const queryMock = jest.fn(); 37 | 38 | API.mockImplementation( () => ( { 39 | query: queryMock, 40 | } ) ); 41 | 42 | queryMock.mockImplementation( () => 43 | logsResponse( [ 44 | { timestamp: '2021-11-05T20:18:36.234041811Z', message: 'My container message 1' }, 45 | { timestamp: '2021-11-09T20:47:07.301221112Z', message: 'My container message 2' }, 46 | ] ) 47 | ); 48 | 49 | await getRecentLogs( 1, 3, 'batch', 1200 ); 50 | 51 | expect( queryMock ).toHaveBeenCalledTimes( 1 ); 52 | expect( queryMock ).toHaveBeenCalledWith( { 53 | query: EXPECTED_QUERY, 54 | variables: { 55 | appId: 1, 56 | envId: 3, 57 | type: 'batch', 58 | limit: 1200, 59 | after: undefined, 60 | }, 61 | } ); 62 | } ); 63 | 64 | it( 'should throw when logs field is not returned', () => { 65 | const queryMock = jest.fn(); 66 | 67 | API.mockImplementation( () => ( { 68 | query: queryMock, 69 | } ) ); 70 | 71 | queryMock.mockImplementation( () => ( { data: {} } ) ); 72 | 73 | const result = getRecentLogs( 1, 3, 'batch', 1200 ); 74 | 75 | return expect( result ).rejects.toThrow( 'Unable to query logs' ); 76 | } ); 77 | } ); 78 | 79 | function logsResponse( logs, nextCursor = null ) { 80 | return { 81 | data: { 82 | app: { 83 | environments: [ 84 | { 85 | logs: { 86 | nextCursor, 87 | nodes: logs, 88 | }, 89 | }, 90 | ], 91 | }, 92 | }, 93 | }; 94 | } 95 | -------------------------------------------------------------------------------- /__tests__/lib/cli/command.js: -------------------------------------------------------------------------------- 1 | import { containsAppEnvArgument } from '../../../src/lib/cli/command'; 2 | 3 | describe( 'utils/cli/command', () => { 4 | describe( 'containsAppEnvArgument', () => { 5 | it.each( [ 6 | [ [ 'test', 'one' ], false ], 7 | [ [ 'test', '@123', 'dev-env' ], true ], 8 | [ [ 'test', '@123.develop', 'dev-env' ], true ], 9 | [ [ 'test', '--app', '123', 'dev-env' ], true ], 10 | ] )( 'should identify app/env arguments - %p', ( argv, expected ) => { 11 | const result = containsAppEnvArgument( argv ); 12 | expect( result ).toBe( expected ); 13 | } ); 14 | } ); 15 | } ); 16 | -------------------------------------------------------------------------------- /__tests__/lib/cli/config.js: -------------------------------------------------------------------------------- 1 | describe( 'utils/cli/config', () => { 2 | beforeEach( () => { 3 | jest.resetModules(); 4 | jest.clearAllMocks(); 5 | } ); 6 | it.each( [ 7 | { 8 | description: 'should return development if config.local.json is present', 9 | files: { local: true, publish: true }, 10 | expected: { environment: 'development' }, 11 | }, 12 | { 13 | description: 'should return production if config.local.json is missing', 14 | files: { local: false, publish: true }, 15 | expected: { environment: 'production' }, 16 | }, 17 | { 18 | description: 'should throw error if config.local.json and config.publish.json are missing', 19 | files: { local: false, publish: false }, 20 | expected: Error, 21 | }, 22 | ] )( '$description', ( { files, expected } ) => { 23 | // An array of files would've been nicer but it doesn't play well with jest.doMock 24 | if ( ! files.local ) { 25 | jest.doMock( '../../../config/config.local.json', () => { 26 | throw new Error(); 27 | } ); 28 | } 29 | if ( ! files.publish ) { 30 | jest.doMock( '../../../config/config.publish.json', () => { 31 | throw new Error(); 32 | } ); 33 | } 34 | 35 | if ( ! files.local && ! files.publish ) { 36 | // eslint-disable-next-line jest/no-conditional-expect 37 | expect( () => require( '../../../src/lib/cli/config' ) ).toThrow( expected ); 38 | } else { 39 | const config = require( '../../../src/lib/cli/config' ); 40 | // eslint-disable-next-line jest/no-conditional-expect 41 | expect( config.default ).toMatchObject( expected ); 42 | } 43 | } ); 44 | } ); 45 | -------------------------------------------------------------------------------- /__tests__/lib/cli/exit.js: -------------------------------------------------------------------------------- 1 | import { withError } from '../../../src/lib/cli/exit'; 2 | import env from '../../../src/lib/env'; 3 | 4 | // Mock console.log() 5 | let output; 6 | global.console = { 7 | log: message => ( output += message + '\n' ), 8 | error: message => ( output += message + '\n' ), 9 | }; 10 | jest.spyOn( global.console, 'log' ); 11 | 12 | const mockExit = jest.spyOn( process, 'exit' ).mockImplementation( () => {} ); 13 | const ERROR_CODE = 1; 14 | 15 | describe( '../../src/lib/cli/exit', () => { 16 | beforeAll( async () => { 17 | output = ''; 18 | mockExit.mockClear(); 19 | } ); 20 | 21 | it( 'calls process.exit with code 1', () => { 22 | withError( 'Unexpected error' ); 23 | expect( mockExit ).toHaveBeenCalledWith( ERROR_CODE ); 24 | } ); 25 | 26 | it( 'outputs the passed error message', () => { 27 | withError( 'My error message' ); 28 | expect( output ).toContain( 'My error message' ); 29 | } ); 30 | 31 | it( 'outputs debug information', () => { 32 | withError( 'Oh no' ); 33 | expect( output ).toContain( 'Debug' ); 34 | expect( output ).toContain( `VIP-CLI v${ env.app.version }` ); 35 | expect( output ).toContain( `Node ${ env.node.version }` ); 36 | } ); 37 | } ); 38 | -------------------------------------------------------------------------------- /__tests__/lib/envvar/api.js: -------------------------------------------------------------------------------- 1 | import { validateName } from '../../../src/lib/envvar/api'; 2 | 3 | describe( 'validateName', () => { 4 | it( 'validates allowed names', () => { 5 | const allowedNames = [ 6 | 'M', 7 | 'MY', 8 | 'MY_', 9 | 'MY_V', 10 | 'MY_VAR', 11 | 'MY__VAR', 12 | 'M2', 13 | 'MY_VAR2', 14 | 'MY_2VAR', 15 | 'MY_2', 16 | 'MY2VAR', 17 | ]; 18 | 19 | allowedNames.forEach( name => { 20 | expect( validateName( name ) ).toBe( true ); 21 | } ); 22 | } ); 23 | 24 | it( 'rejects disallowed names', () => { 25 | const disallowedNames = [ 26 | '1', 27 | '1M', 28 | '111', 29 | '_MY', 30 | '__MY_VAR', 31 | '2MY_VAR', 32 | 'my_var', 33 | 'myvar', 34 | 'MY_VAr', 35 | 'myVar', 36 | ' MY_VAR ', 37 | '\nMY_VAR\n', 38 | ]; 39 | 40 | disallowedNames.forEach( name => { 41 | expect( validateName( name ) ).toBe( false ); 42 | } ); 43 | } ); 44 | } ); 45 | -------------------------------------------------------------------------------- /__tests__/lib/keychain.js: -------------------------------------------------------------------------------- 1 | import { randomBytes } from 'crypto'; 2 | 3 | import Insecure from '../../src/lib/keychain/insecure'; 4 | 5 | const account = 'vip-cli-test'; 6 | const password = randomBytes( 256 ).toString(); 7 | 8 | describe( 'token tests (insecure)', () => { 9 | const keychain = new Insecure( account ); 10 | 11 | it( 'should correctly set token', async () => { 12 | await keychain.setPassword( account, password ); 13 | const passwd = keychain.getPassword( account ); 14 | return expect( passwd ).resolves.toBe( password ); 15 | } ); 16 | 17 | it( 'should correctly set multiple tokens', async () => { 18 | const expected = [ 'password1', 'password2' ]; 19 | await Promise.all( [ 20 | keychain.setPassword( 'first', expected[ 0 ] ), 21 | keychain.setPassword( 'second', expected[ 1 ] ), 22 | ] ); 23 | 24 | const promise = Promise.all( [ 25 | keychain.getPassword( 'first' ), 26 | keychain.getPassword( 'second' ), 27 | ] ); 28 | 29 | return expect( promise ).resolves.toEqual( expected ); 30 | } ); 31 | 32 | it( 'should correctly delete token', async () => { 33 | await keychain.setPassword( account, password ); 34 | await keychain.deletePassword( account ); 35 | 36 | const passwd = keychain.getPassword( account ); 37 | return expect( passwd ).resolves.toBeNull(); 38 | } ); 39 | 40 | it( 'should correctly delete a single token', async () => { 41 | const expected = [ null, 'password2' ]; 42 | 43 | await Promise.all( [ 44 | keychain.setPassword( 'first', 'password1' ), 45 | keychain.setPassword( 'second', expected[ 1 ] ), 46 | ] ); 47 | 48 | await keychain.deletePassword( 'first' ); 49 | 50 | const promise = Promise.all( [ 51 | keychain.getPassword( 'first' ), 52 | keychain.getPassword( 'second' ), 53 | ] ); 54 | 55 | return expect( promise ).resolves.toEqual( expected ); 56 | } ); 57 | } ); 58 | -------------------------------------------------------------------------------- /__tests__/lib/site-import.js: -------------------------------------------------------------------------------- 1 | import { isImportingBlockedBySync, isSupportedApp } from '../../src/lib/site-import/db-file-import'; 2 | 3 | describe( 'site import tests', () => { 4 | describe( 'db-file-import', () => { 5 | describe( 'isImportingBlockedBySync', () => { 6 | it( 'should return false for not_syncing status', () => { 7 | expect( isImportingBlockedBySync( { syncProgress: { status: 'not_syncing' } } ) ).toBe( 8 | false 9 | ); 10 | } ); 11 | 12 | it( 'should return true for some other status', () => { 13 | expect( isImportingBlockedBySync( { syncProgress: { status: 'gibberish' } } ) ).toBe( 14 | true 15 | ); 16 | } ); 17 | 18 | it( 'should return true for missing status', () => { 19 | expect( isImportingBlockedBySync( { syncProgress: {} } ) ).toBe( true ); 20 | } ); 21 | } ); 22 | 23 | describe( 'isSupportedApp', () => { 24 | it( 'should return true for site types with a database', () => { 25 | expect( isSupportedApp( { typeId: 2 } ) ).toBe( true ); 26 | expect( isSupportedApp( { typeId: 5 } ) ).toBe( true ); 27 | expect( isSupportedApp( { typeId: 6 } ) ).toBe( true ); 28 | expect( isSupportedApp( { typeId: 8 } ) ).toBe( true ); 29 | } ); 30 | 31 | it( 'should return false for site types without a database', () => { 32 | expect( isSupportedApp( { typeId: 3 } ) ).toBe( false ); 33 | expect( isSupportedApp( { typeId: 4 } ) ).toBe( false ); 34 | expect( isSupportedApp( { typeId: 7 } ) ).toBe( false ); 35 | expect( isSupportedApp( { typeId: 3 } ) ).toBe( false ); 36 | } ); 37 | 38 | it( 'should return false for no type', () => { 39 | expect( isSupportedApp( {} ) ).toBe( false ); 40 | } ); 41 | } ); 42 | } ); 43 | } ); 44 | -------------------------------------------------------------------------------- /__tests__/lib/validations/is-multi-site-sql-dump.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | 5 | import { sqlDumpLineIsMultiSite } from '../../../src/lib/validations/is-multi-site-sql-dump'; 6 | 7 | describe( 'is-multi-site-sql-dump', () => { 8 | describe( 'sqlDumpLineIsMultiSite', () => { 9 | it( 'return true when a multisite table line is detected', () => { 10 | expect( sqlDumpLineIsMultiSite( 'CREATE TABLE wp_2_posts' ) ).toBe( true ); 11 | expect( sqlDumpLineIsMultiSite( 'CREATE TABLE wp_23_posts' ) ).toBe( true ); 12 | expect( sqlDumpLineIsMultiSite( 'CREATE TABLE wp_2345235_posts' ) ).toBe( true ); 13 | expect( sqlDumpLineIsMultiSite( 'CREATE TABLE wp_blogs' ) ).toBe( true ); 14 | expect( sqlDumpLineIsMultiSite( 'CREATE TABLE IF NOT EXISTS wp_2_posts' ) ).toBe( true ); 15 | } ); 16 | it( 'returns false for non-multi site table creations', () => { 17 | expect( sqlDumpLineIsMultiSite( 'CREATE TABLE wp_posts' ) ).toBe( false ); 18 | expect( sqlDumpLineIsMultiSite( 'CREATE TABLE IF NOT EXISTS wp_posts' ) ).toBe( false ); 19 | } ); 20 | it( 'return true if a wp_users table has a spam or deleted column', () => { 21 | expect( sqlDumpLineIsMultiSite( '`spam` tinyint(2) NOT NULL DEFAULT 0,' ) ).toBe( true ); 22 | expect( sqlDumpLineIsMultiSite( '`deleted` tinyint(2) NOT NULL DEFAULT 0,' ) ).toBe( true ); 23 | } ); 24 | } ); 25 | } ); 26 | -------------------------------------------------------------------------------- /__tests__/lib/validations/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | 5 | import { once } from 'node:events'; 6 | import path from 'path'; 7 | 8 | import { getReadInterface } from '../../../src/lib/validations/line-by-line'; 9 | import { getMultilineStatement } from '../../../src/lib/validations/utils'; 10 | 11 | describe( 'utils', () => { 12 | describe( 'getMultilineStatement', () => { 13 | it( 'should return each occurance of a statement as an array of its lines', async () => { 14 | const sqlDumpPath = path.join( 15 | process.cwd(), 16 | '__fixtures__', 17 | 'validations', 18 | 'multiline-statements.sql' 19 | ); 20 | const readInterface = await getReadInterface( sqlDumpPath ); 21 | 22 | const getStatementsByLine = getMultilineStatement( /INSERT INTO wp_blogs/s ); 23 | 24 | let statements; 25 | readInterface.on( 'line', line => { 26 | statements = getStatementsByLine( line ); 27 | } ); 28 | 29 | await once( readInterface, 'close' ); 30 | 31 | // expecting the correct number of matching statements 32 | expect( statements ).toHaveLength( 4 ); 33 | // expecting the statement to have the right number of lines 34 | expect( statements[ 0 ] ).toHaveLength( 1 ); 35 | expect( statements[ 1 ] ).toHaveLength( 3 ); 36 | expect( statements[ 2 ] ).toHaveLength( 4 ); 37 | expect( statements[ 3 ] ).toHaveLength( 5 ); 38 | } ); 39 | 40 | it( 'should accurately capture the statement', async () => { 41 | const sqlDumpPath = path.join( 42 | process.cwd(), 43 | '__fixtures__', 44 | 'validations', 45 | 'multiline-statements.sql' 46 | ); 47 | const readInterface = await getReadInterface( sqlDumpPath ); 48 | 49 | const getStatementsByLine = getMultilineStatement( /INSERT INTO `wp_site`/s ); 50 | 51 | let statements; 52 | readInterface.on( 'line', line => { 53 | statements = getStatementsByLine( line ); 54 | } ); 55 | 56 | await once( readInterface, 'close' ); 57 | 58 | expect( statements[ 0 ].join( '' ).replace( /\s/g, '' ) ).toBe( 59 | "INSERTINTO`wp_site`(`id`,`domain`,`path`)VALUES(1,'www.example.com','/');" 60 | ); 61 | } ); 62 | } ); 63 | } ); 64 | -------------------------------------------------------------------------------- /assets/dev-env.nginx.template.conf.ejs: -------------------------------------------------------------------------------- 1 | <% if ( photon ) { %> 2 | 3 | location ^~ /wp-content/uploads/ { 4 | expires max; 5 | log_not_found off; 6 | <% if ( mediaRedirectDomain ) { %> 7 | if (!-f $request_filename) { 8 | rewrite ^/(.*)$ <%= mediaRedirectDomain %>/$1 redirect; 9 | } 10 | <% } %> 11 | 12 | include fastcgi_params; 13 | fastcgi_param DOCUMENT_ROOT /usr/share/webapps/photon; 14 | fastcgi_param SCRIPT_FILENAME /usr/share/webapps/photon/index.php; 15 | fastcgi_param SCRIPT_NAME /index.php; 16 | 17 | if ($request_uri ~* \.(gif|jpe?g|png)\?) { 18 | fastcgi_pass photon:9000; 19 | } 20 | } 21 | 22 | <% } else if ( mediaRedirectDomain ) { %> 23 | 24 | location ^~ /wp-content/uploads { 25 | expires max; 26 | log_not_found off; 27 | try_files $uri @prod_site; 28 | } 29 | 30 | location @prod_site { 31 | rewrite ^/(.*)$ <%= mediaRedirectDomain %>/$1 redirect; 32 | } 33 | 34 | <% } %> 35 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-typescript', 5 | { 6 | allowDeclareFields: true, 7 | }, 8 | ], 9 | [ 10 | '@babel/preset-env', 11 | { 12 | loose: true, 13 | exclude: [ '@babel/plugin-proposal-dynamic-import' ], 14 | targets: { 15 | node: '18', // Keep this in sync with package.json engines.node 16 | }, 17 | }, 18 | ], 19 | ], 20 | ignore: [ '**/*.d.ts' ], 21 | }; 22 | -------------------------------------------------------------------------------- /codegen.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | import { CodegenConfig } from '@graphql-codegen/cli'; 3 | 4 | const config: CodegenConfig = { 5 | schema: './schema.gql', 6 | ignoreNoDocuments: true, // for better experience with the watcher 7 | hooks: { afterAllFileWrite: [ 'prettier --write' ] }, 8 | generates: { 9 | './src/graphqlTypes.d.ts': { 10 | plugins: [ 'typescript' ], 11 | config: { 12 | enumsAsTypes: true, 13 | }, 14 | }, 15 | './src/': { 16 | documents: [ 'src/**/*.js', 'src/**/*.ts' ], 17 | preset: 'near-operation-file', 18 | presetConfig: { 19 | extension: '.generated.d.ts', 20 | baseTypesPath: 'graphqlTypes.ts', 21 | }, 22 | plugins: [ 'typescript-operations' ], 23 | }, 24 | }, 25 | }; 26 | 27 | export default config; 28 | -------------------------------------------------------------------------------- /config/config.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "tracksUserType": "vip:user_id", 3 | "tracksAnonUserType": "anon", 4 | "tracksEventPrefix": "vip_cli_dev_", 5 | "environment": "development" 6 | } 7 | -------------------------------------------------------------------------------- /config/config.publish.json: -------------------------------------------------------------------------------- 1 | { 2 | "tracksUserType": "vip:user_id", 3 | "tracksAnonUserType": "anon", 4 | "tracksEventPrefix": "vip_cli_", 5 | "environment": "production" 6 | } 7 | -------------------------------------------------------------------------------- /docs/DEBUGGING.md: -------------------------------------------------------------------------------- 1 | # Debugging 2 | 3 | ## Using debugger 4 | 5 | A debugger can easily be used to help pinpointing issues with the code. Follow these steps. 6 | 7 | 1. First, make sure to run the `npm run build:watch`, this will generate source maps 8 | 2. Run the command you want via `node --inspect`, like so: `node --inspect ./dist/bin/vip-dev-env-import-sql.js` 9 | 3. Note the port the debugger is listening on: 10 | 11 | ``` 12 | Debugger listening on ws://127.0.0.1:9229/db6c03e9-2585-4a08-a1c6-1fee0295c9ff 13 | For help, see: https://nodejs.org/en/docs/inspector 14 | ``` 15 | 16 | 4. In your editor of choice attach to the debugger. For VSCode: Hit 'Run and Debug' panel, hit the "gear" icon (open launch.json), make your `Attach` configuration entry to look like so: 17 | Make sure the `port` matches the port from step 3, and the `runtimeExecutable` matches the exact `node` executable you ran. If you use a version manager like `nvm`, its especially important to check this. 18 | 19 | ```json 20 | { 21 | "name": "Attach", 22 | "port": 9229, 23 | "request": "attach", 24 | "skipFiles": [ "/**" ], 25 | "type": "node", 26 | "runtimeExecutable": "/Users/user/.nvm/versions/node/v14.18.2/bin/node" 27 | } 28 | ``` 29 | 30 | 5. Set your breakpoints, add debug code, and hit the play button. 31 | 6. Confirm that you attached the debugger to continue command execution. 32 | 7. Resolve the problem. 33 | 8. [Optional but recommended] Pay it forward and implement a similar approach to other internal/external tooling. 34 | -------------------------------------------------------------------------------- /docs/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | > Although we strive to create the most secure products possible, we are not perfect. If you happen to find a security vulnerability in one of our services, we would appreciate letting us know and allowing us to respond before disclosing the issue publicly. We take security seriously, and we will try to review and reply to every legitimate security report personally within 24 hours. 4 | 5 | ([Source](https://automattic.com/security/)) 6 | 7 | ## Reporting a Vulnerability 8 | 9 | For responsible disclosure of security issues and to be eligible for our bug bounty program, please submit security issuess via the HackerOne portal: https://hackerone.com/automattic 10 | -------------------------------------------------------------------------------- /helpers/check-version.js: -------------------------------------------------------------------------------- 1 | const semver = require( 'semver' ); 2 | 3 | const { name, engines } = require( '../package.json' ); 4 | 5 | const version = engines.node; 6 | 7 | if ( ! semver.satisfies( process.version, version ) ) { 8 | console.log( 9 | [ 10 | `The current version of Node (${ process.version }) does not meet the minimum requirements;`, 11 | `${ name } requires Node version ${ version }.`, 12 | 'Please follow the installation instructions at https://nodejs.org/en/download/ to upgrade before continuing.`', 13 | ].join( ' ' ) 14 | ); 15 | process.exit( 1 ); 16 | } 17 | -------------------------------------------------------------------------------- /helpers/postinstall.js: -------------------------------------------------------------------------------- 1 | const { trackEvent } = require( '../dist/lib/tracker' ); 2 | trackEvent( 'install' ); 3 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const API = require( './dist/lib/api' ); 2 | const Token = require( './dist/lib/token' ); 3 | 4 | module.exports = { 5 | API, 6 | Token, 7 | }; 8 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | setupFiles: [ './jest.setup.js', './jest.setupMocks.js' ], 3 | maxWorkers: 4, 4 | }; 5 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | import nock from 'nock'; 2 | 3 | process.env.API_HOST = 'http://localhost:4000'; 4 | 5 | // Don't let tests talk to the outside world 6 | nock.disableNetConnect(); 7 | -------------------------------------------------------------------------------- /jest.setupMocks.js: -------------------------------------------------------------------------------- 1 | import * as apiConfig from './src/lib/cli/apiConfig'; 2 | 3 | // This function is mocked globally because it is called by trackEvent. 4 | jest.spyOn( apiConfig, 'checkIfUserIsVip' ).mockResolvedValue( true ); 5 | -------------------------------------------------------------------------------- /src/bin/vip-app-deploy-validate.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * External dependencies 5 | */ 6 | import chalk from 'chalk'; 7 | import debugLib from 'debug'; 8 | import { extname } from 'path'; 9 | 10 | /** 11 | * Internal dependencies 12 | */ 13 | import command from '../lib/cli/command'; 14 | import { getFileMeta } from '../lib/client-file-uploader'; 15 | import { validateFile } from '../lib/custom-deploy/custom-deploy'; 16 | import { trackEventWithEnv } from '../lib/tracker'; 17 | import { validateZipFile, validateTarFile } from '../lib/validations/custom-deploy'; 18 | 19 | const debug = debugLib( '@automattic/vip:bin:vip-app-deploy-validate' ); 20 | const baseUsage = 'vip app deploy validate'; 21 | 22 | export async function appDeployValidateCmd( 23 | arg: string[] = [], 24 | opts: Record< string, unknown > = {} 25 | ) { 26 | const app = opts.app as string | number; 27 | const env = opts.env as string | number; 28 | 29 | const [ fileName ] = arg; 30 | const fileMeta = await getFileMeta( fileName ); 31 | 32 | debug( 'Options: ', opts ); 33 | debug( 'Args: ', arg ); 34 | 35 | const track = trackEventWithEnv.bind( null, app, env ); 36 | 37 | debug( 'Validating file...' ); 38 | await validateFile( app as number, env as number, fileMeta ); 39 | 40 | await track( 'deploy_validate_app_command_execute' ); 41 | 42 | const ext = extname( fileName ); 43 | if ( ext === '.zip' ) { 44 | await validateZipFile( fileName ); 45 | } else { 46 | await validateTarFile( fileName ); 47 | } 48 | 49 | console.log( chalk.green( '✓ Compressed file has been successfully validated with no errors.' ) ); 50 | } 51 | 52 | // Command examples for the `vip app deploy validate` help prompt 53 | const examples = [ 54 | { 55 | usage: 'vip app deploy validate file.tar.gz', 56 | description: 'Validate the directory structure of the local archived file named "file.tar.gz".', 57 | }, 58 | ]; 59 | 60 | void command( { 61 | requiredArgs: 1, 62 | usage: baseUsage, 63 | } ) 64 | .examples( examples ) 65 | .argv( process.argv, appDeployValidateCmd ); 66 | -------------------------------------------------------------------------------- /src/bin/vip-app-deploy.generated.d.ts: -------------------------------------------------------------------------------- 1 | import * as Types from '../graphqlTypes'; 2 | 3 | export type StartCustomDeployMutationVariables = Types.Exact< { 4 | input?: Types.InputMaybe< Types.AppEnvironmentCustomDeployInput >; 5 | } >; 6 | 7 | export type StartCustomDeployMutation = { 8 | __typename?: 'Mutation'; 9 | startCustomDeploy?: { 10 | __typename?: 'AppEnvironmentCustomDeployPayload'; 11 | success?: boolean | null; 12 | message?: string | null; 13 | } | null; 14 | }; 15 | -------------------------------------------------------------------------------- /src/bin/vip-app-list.generated.d.ts: -------------------------------------------------------------------------------- 1 | import * as Types from '../graphqlTypes'; 2 | 3 | export type AppsQueryVariables = Types.Exact< { 4 | first?: Types.InputMaybe< Types.Scalars[ 'Int' ][ 'input' ] >; 5 | after?: Types.InputMaybe< Types.Scalars[ 'String' ][ 'input' ] >; 6 | } >; 7 | 8 | export type AppsQuery = { 9 | __typename?: 'Query'; 10 | apps?: { 11 | __typename?: 'AppList'; 12 | total?: number | null; 13 | nextCursor?: string | null; 14 | edges?: Array< { 15 | __typename?: 'App'; 16 | id?: number | null; 17 | name?: string | null; 18 | repo?: string | null; 19 | } | null > | null; 20 | } | null; 21 | }; 22 | -------------------------------------------------------------------------------- /src/bin/vip-app-list.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import gql from 'graphql-tag'; 4 | 5 | import API from '../lib/api'; 6 | import command from '../lib/cli/command'; 7 | import { trackEvent } from '../lib/tracker'; 8 | 9 | const baseUsage = 'vip app list'; 10 | 11 | command( { format: true, usage: baseUsage } ) 12 | .examples( [ 13 | { 14 | usage: 15 | baseUsage + 16 | '\n' + 17 | ' - ┌──────┬─────────────────┬─────────────────────────────────┐\n' + 18 | ' - │ id │ name │ repo │\n' + 19 | ' - ┌──────┬─────────────────┬─────────────────────────────────┐\n' + 20 | ' - │ 8886 │ example-app │ wpcomvip/my-org-example-app │\n' + 21 | ' - ┌──────┬─────────────────┬─────────────────────────────────┐\n' + 22 | ' - │ 4325 │ mytestmultisite │ wpcomvip/my-org-mytestmultisite │\n' + 23 | ' - └──────┴─────────────────┴─────────────────────────────────┘\n', 24 | description: 25 | 'Retrieve a list of applications that can be accessed by the current authenticated VIP-CLI user.', 26 | }, 27 | ] ) 28 | .argv( process.argv, async () => { 29 | const api = API(); 30 | 31 | await trackEvent( 'app_list_command_execute' ); 32 | 33 | let response; 34 | try { 35 | response = await api.query( { 36 | query: gql` 37 | query Apps($first: Int, $after: String) { 38 | apps(first: $first, after: $after) { 39 | total 40 | nextCursor 41 | edges { 42 | id 43 | name 44 | repo 45 | } 46 | } 47 | } 48 | `, 49 | variables: { 50 | first: 100, 51 | after: null, // TODO make dynamic 52 | }, 53 | } ); 54 | } catch ( err ) { 55 | const message = err.toString(); 56 | 57 | await trackEvent( 'app_list_command_fetch_error', { 58 | error: message, 59 | } ); 60 | 61 | console.log( 'Failed to fetch apps: %s', message ); 62 | return; 63 | } 64 | 65 | if ( 66 | ! response || 67 | ! response.data || 68 | ! response.data.apps || 69 | ! response.data.apps.edges || 70 | ! response.data.apps.edges.length 71 | ) { 72 | const message = 'No apps found'; 73 | 74 | await trackEvent( 'app_list_command_fetch_error', { 75 | error: message, 76 | } ); 77 | 78 | console.log( message ); 79 | return; 80 | } 81 | 82 | await trackEvent( 'app_list_command_success' ); 83 | 84 | return response.data.apps.edges; 85 | } ); 86 | -------------------------------------------------------------------------------- /src/bin/vip-backup-db.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { BackupDBCommand } from '../commands/backup-db'; 4 | import { App, AppEnvironment } from '../graphqlTypes'; 5 | import command from '../lib/cli/command'; 6 | import { makeCommandTracker } from '../lib/tracker'; 7 | 8 | const examples = [ 9 | { 10 | usage: 11 | 'vip @example-app.develop backup db\n' + 12 | ' Generating a new database backup...\n' + 13 | ' ✓ Preparing for backup generation\n' + 14 | ' ✓ Generating backup\n' + 15 | ' New database backup created', 16 | description: 'Generate a new database backup of an environment.', 17 | }, 18 | ]; 19 | 20 | const appQuery = ` 21 | id, 22 | name, 23 | type, 24 | organization { id, name }, 25 | environments{ 26 | id 27 | appId 28 | type 29 | name 30 | primaryDomain { name } 31 | uniqueLabel 32 | } 33 | `; 34 | 35 | void command( { 36 | appContext: true, 37 | appQuery, 38 | envContext: true, 39 | module: 'backup-db', 40 | requiredArgs: 0, 41 | usage: 'vip backup db', 42 | } ) 43 | .examples( examples ) 44 | .argv( process.argv, async ( arg: string[], { app, env }: { app: App; env: AppEnvironment } ) => { 45 | const trackerFn = makeCommandTracker( 'backup_db', { 46 | app: app.id, 47 | env: env.uniqueLabel, 48 | } ); 49 | await trackerFn( 'execute' ); 50 | 51 | const cmd = new BackupDBCommand( app, env, trackerFn ); 52 | await cmd.run(); 53 | 54 | await trackerFn( 'success' ); 55 | } ); 56 | -------------------------------------------------------------------------------- /src/bin/vip-backup.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import command from '../lib/cli/command'; 4 | import { trackEvent } from '../lib/tracker'; 5 | 6 | void command( { usage: 'vip backup' } ) 7 | .command( 'db', 'Generate a new database backup of an environment.' ) 8 | .example( 9 | 'vip @example-app.develop backup db\n' + 10 | ' Generating a new database backup...\n' + 11 | ' ✓ Preparing for backup generation\n' + 12 | ' ✓ Generating backup\n' + 13 | ' New database backup created', 14 | 'Generate a new database backup of an environment.' 15 | ) 16 | .argv( process.argv, async () => { 17 | await trackEvent( 'vip_backup_command_execute' ); 18 | } ); 19 | -------------------------------------------------------------------------------- /src/bin/vip-cache-purge-url.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { appQuery, purgeCache } from '../lib/api/cache-purge'; 4 | import command from '../lib/cli/command'; 5 | import * as exit from '../lib/cli/exit'; 6 | import { readFromFile } from '../lib/read-file'; 7 | import { trackEvent } from '../lib/tracker'; 8 | 9 | const usage = 'vip cache purge-url'; 10 | const exampleUsage = 'vip @example-app.develop cache purge-url'; 11 | 12 | const examples = [ 13 | { 14 | usage: 15 | `${ exampleUsage } https://example-app-develop.go-vip.co/sample-page/` + 16 | '\n - Purged URL: https://example-app.develop.go-vip.co/sample-page/', 17 | description: 'Purge the page cache for a single URL.', 18 | }, 19 | { 20 | usage: `${ exampleUsage } --from-file=./urls.txt`, 21 | description: 22 | 'Purge the page cache for multiple URLs, each listed on a single line in a local file.', 23 | }, 24 | ]; 25 | 26 | export async function cachePurgeCommand( urls = [], opt = {} ) { 27 | const trackingParams = { 28 | app_id: opt.app.id, 29 | command: 'vip cache purge-url', 30 | env_id: opt.env.id, 31 | from_file: Boolean( opt.fromFile ), 32 | }; 33 | 34 | await trackEvent( 'cache_purge_url_command_execute', trackingParams ); 35 | 36 | if ( opt.fromFile ) { 37 | const value = await readFromFile( opt.fromFile ); 38 | if ( value ) { 39 | urls = value.split( '\n' ).map( url => url.trim() ); 40 | } 41 | } 42 | 43 | if ( ! urls.length ) { 44 | await trackEvent( 'cache_purge_url_command_error', { 45 | ...trackingParams, 46 | error: 'No URL provided', 47 | } ); 48 | 49 | exit.withError( 'Please supply at least one URL.' ); 50 | } 51 | 52 | let purgeCacheObject = {}; 53 | try { 54 | purgeCacheObject = await purgeCache( opt.app.id, opt.env.id, urls ); 55 | } catch ( err ) { 56 | await trackEvent( 'cache_purge_url_command_error', { ...trackingParams, error: err.message } ); 57 | 58 | exit.withError( `Failed to purge URL(s) from page cache: ${ err.message }` ); 59 | } 60 | 61 | await trackEvent( 'cache_purge_url_command_success', trackingParams ); 62 | 63 | purgeCacheObject.urls.forEach( url => { 64 | console.log( `- Purged URL: ${ url }` ); 65 | } ); 66 | } 67 | 68 | command( { 69 | appContext: true, 70 | appQuery, 71 | envContext: true, 72 | wildcardCommand: true, 73 | usage, 74 | } ) 75 | .option( 'from-file', 'Read one or more URLs from a file, each listed on a single line.' ) 76 | .examples( examples ) 77 | .argv( process.argv, cachePurgeCommand ); 78 | -------------------------------------------------------------------------------- /src/bin/vip-cache.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import command from '../lib/cli/command'; 4 | 5 | const usage = 'vip cache'; 6 | const exampleUsage = 'vip @example-app.develop cache'; 7 | 8 | const examples = [ 9 | { 10 | usage: 11 | `${ exampleUsage } purge-url https://example-app-develop.go-vip.co/sample-page/` + 12 | '\n - Purged URL: https://example-app.develop.go-vip.co/sample-page/', 13 | description: 'Purge the page cache for a single URL.', 14 | }, 15 | { 16 | usage: `${ exampleUsage } purge-url --from-file=./urls.txt`, 17 | description: 18 | 'Purge the page cache for multiple URLs, each listed on a single line in a local file.', 19 | }, 20 | ]; 21 | 22 | command( { 23 | requiredArgs: 1, 24 | usage, 25 | } ) 26 | .command( 'purge-url', 'Purge page cache for one or more URLs.' ) 27 | .examples( examples ) 28 | .argv( process.argv ); 29 | -------------------------------------------------------------------------------- /src/bin/vip-config-envvar-get.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import chalk from 'chalk'; 4 | 5 | import command from '../lib/cli/command'; 6 | import { appQuery, getEnvVar } from '../lib/envvar/api'; 7 | import { debug, getEnvContext } from '../lib/envvar/logging'; 8 | import { trackEvent } from '../lib/tracker'; 9 | 10 | const baseUsage = 'vip config envvar get'; 11 | const exampleUsage = 'vip @example-app.develop config envvar get'; 12 | 13 | // Command examples 14 | const examples = [ 15 | { 16 | usage: `${ exampleUsage } MY_VARIABLE`, 17 | description: 'Retrieve the value of the environment variable "MY_VARIABLE".', 18 | }, 19 | ]; 20 | 21 | /** 22 | * @param {string[]} arg 23 | * @param {object} opt 24 | * @return {Promise} 25 | */ 26 | export async function getEnvVarCommand( arg, opt ) { 27 | // Help the user by uppercasing input. 28 | const name = arg[ 0 ].trim().toUpperCase(); 29 | 30 | const trackingParams = { 31 | app_id: opt.app.id, 32 | command: `${ baseUsage } ${ name }`, 33 | env_id: opt.env.id, 34 | org_id: opt.app.organization.id, 35 | variable_name: name, 36 | }; 37 | 38 | debug( 39 | `Request: Get environment variable ${ JSON.stringify( name ) } for ${ getEnvContext( 40 | opt.app, 41 | opt.env 42 | ) }` 43 | ); 44 | await trackEvent( 'envvar_get_command_execute', trackingParams ); 45 | 46 | const envvar = await getEnvVar( opt.app.id, opt.env.id, name ).catch( async err => { 47 | await trackEvent( 'envvar_get_query_error', { ...trackingParams, error: err.message } ); 48 | 49 | throw err; 50 | } ); 51 | 52 | await trackEvent( 'envvar_get_command_success', trackingParams ); 53 | 54 | if ( ! envvar ) { 55 | const message = `The environment variable ${ JSON.stringify( name ) } does not exist`; 56 | console.log( chalk.yellow( message ) ); 57 | process.exit(); 58 | } 59 | 60 | console.log( envvar.value ); 61 | } 62 | 63 | command( { 64 | appContext: true, 65 | appQuery, 66 | envContext: true, 67 | requiredArgs: 1, 68 | usage: `${ baseUsage } `, 69 | } ) 70 | .examples( examples ) 71 | .argv( process.argv, getEnvVarCommand ); 72 | -------------------------------------------------------------------------------- /src/bin/vip-config-envvar-list.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import chalk from 'chalk'; 4 | 5 | import command from '../lib/cli/command'; 6 | import { formatData } from '../lib/cli/format'; 7 | import { appQuery, listEnvVars } from '../lib/envvar/api'; 8 | import { debug, getEnvContext } from '../lib/envvar/logging'; 9 | import { trackEvent } from '../lib/tracker'; 10 | 11 | const usage = 'vip config envvar list'; 12 | const exampleUsage = 'vip @example-app.develop config envvar list'; 13 | 14 | // Command examples 15 | const examples = [ 16 | { 17 | usage: exampleUsage, 18 | description: 'List the names of all environment variables on an environment.', 19 | }, 20 | ]; 21 | 22 | /** 23 | * @param {string[]} arg 24 | * @param {object} opt 25 | * @return {Promise} 26 | */ 27 | export async function listEnvVarsCommand( arg, opt ) { 28 | const trackingParams = { 29 | app_id: opt.app.id, 30 | command: usage, 31 | env_id: opt.env.id, 32 | format: opt.format, 33 | org_id: opt.app.organization.id, 34 | }; 35 | 36 | debug( `Request: list environment variables for ${ getEnvContext( opt.app, opt.env ) }` ); 37 | await trackEvent( 'envvar_list_command_execute', trackingParams ); 38 | 39 | const envvars = await listEnvVars( opt.app.id, opt.env.id ).catch( async err => { 40 | await trackEvent( 'envvar_list_query_error', { ...trackingParams, error: err.message } ); 41 | 42 | throw err; 43 | } ); 44 | 45 | await trackEvent( 'envvar_list_command_success', trackingParams ); 46 | 47 | if ( 0 === envvars.length ) { 48 | console.log( chalk.yellow( 'There are no environment variables' ) ); 49 | process.exit(); 50 | } 51 | 52 | // Vary data by expected format. 53 | let key = 'name'; 54 | if ( 'keyValue' === opt.format ) { 55 | key = 'key'; 56 | } else if ( 'ids' === opt.format ) { 57 | key = 'id'; 58 | } 59 | 60 | // Format as an object for formatData. 61 | const envvarsObject = envvars.map( name => ( { [ key ]: name } ) ); 62 | 63 | console.log( formatData( envvarsObject, opt.format ) ); 64 | } 65 | 66 | command( { 67 | appContext: true, 68 | appQuery, 69 | envContext: true, 70 | format: true, 71 | usage, 72 | } ) 73 | .examples( examples ) 74 | .argv( process.argv, listEnvVarsCommand ); 75 | -------------------------------------------------------------------------------- /src/bin/vip-config-envvar.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import command from '../lib/cli/command'; 4 | 5 | const usage = 'vip config envvar'; 6 | const exampleUsage = 'vip @example-app.develop config envvar'; 7 | 8 | // Command examples 9 | const examples = [ 10 | { 11 | usage: `${ exampleUsage } set MY_VARIABLE`, 12 | description: 13 | 'Add or update the environment variable "MY_VARIABLE" and assign its value at the prompt.', 14 | }, 15 | { 16 | usage: `${ exampleUsage } get MY_VARIABLE`, 17 | description: 'Retrieve the value of the environment variable "MY_VARIABLE".', 18 | }, 19 | { 20 | usage: `${ exampleUsage } get-all`, 21 | description: 'Retrieve a list of all environment variables in the default table format.', 22 | }, 23 | { 24 | usage: `${ exampleUsage } list`, 25 | description: 'List the names of all environment variables.', 26 | }, 27 | { 28 | usage: `${ exampleUsage } delete MY_VARIABLE`, 29 | description: 'Delete the environment variable "MY_VARIABLE" from the environment.', 30 | }, 31 | ]; 32 | 33 | command( { 34 | requiredArgs: 0, 35 | usage, 36 | } ) 37 | .command( 'delete', 'Delete an environment variable.' ) 38 | .command( 'get', 'Retrieve the value of an environment variable.' ) 39 | .command( 'get-all', 'Retrieve the names and values of all environment variables.' ) 40 | .command( 'list', 'List the names of all environment variables.' ) 41 | .command( 'set', 'Add or update an environment variable.' ) 42 | .examples( examples ) 43 | .argv( process.argv ); 44 | -------------------------------------------------------------------------------- /src/bin/vip-config-software.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import command from '../lib/cli/command'; 4 | 5 | const usage = 'vip config software'; 6 | const exampleUsage = 'vip @example-app.develop config software'; 7 | const exampleUsageNode = 'vip @example-node-app.develop config software'; 8 | 9 | // Command examples 10 | const examples = [ 11 | { 12 | usage: `${ exampleUsage } get`, 13 | description: 14 | 'Retrieve a list of the current versions of all environment software in the default table format.', 15 | }, 16 | { 17 | usage: `${ exampleUsage } get wordpress --include=available_versions`, 18 | description: 19 | 'Retrieve the current version of WordPress for a WordPress environment and a list of available versions in the default table format.', 20 | }, 21 | { 22 | usage: `${ exampleUsage } update wordpress 6.4`, 23 | description: 'Update the version of WordPress on a WordPress environment to 6.4.x.', 24 | }, 25 | { 26 | usage: `${ exampleUsageNode } update nodejs 18`, 27 | description: 'Update the version of Node.js on a Node.js environment to 18.x.', 28 | }, 29 | ]; 30 | 31 | command( { 32 | requiredArgs: 1, 33 | usage, 34 | } ) 35 | .command( 'get', 'Retrieve the current versions of environment software.' ) 36 | .command( 'update', 'Update the version of software running on an environment.' ) 37 | .examples( examples ) 38 | .argv( process.argv ); 39 | -------------------------------------------------------------------------------- /src/bin/vip-config.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import command from '../lib/cli/command'; 4 | 5 | const usage = 'vip config'; 6 | const exampleUsage = 'vip @example-app.develop config'; 7 | 8 | // Command examples 9 | const examples = [ 10 | { 11 | usage: `${ exampleUsage } envvar list`, 12 | description: 'List the names of all environment variables on the environment.', 13 | }, 14 | { 15 | usage: `${ exampleUsage } software get`, 16 | description: 'Retrieve the current versions of all environment software.', 17 | }, 18 | ]; 19 | 20 | command( { 21 | requiredArgs: 2, 22 | usage, 23 | } ) 24 | .command( 'envvar', 'Manage environment variables for an environment.' ) 25 | .command( 'software', 'Manage versions of software for an environment.' ) 26 | .examples( examples ) 27 | .argv( process.argv, async () => { 28 | process.exit( 0 ); 29 | } ); 30 | -------------------------------------------------------------------------------- /src/bin/vip-db-phpmyadmin.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * External dependencies 5 | */ 6 | 7 | /** 8 | * Internal dependencies 9 | */ 10 | import { PhpMyAdminCommand } from '../commands/phpmyadmin'; 11 | import { App, AppEnvironment } from '../graphqlTypes'; 12 | import command from '../lib/cli/command'; 13 | import { makeCommandTracker } from '../lib/tracker'; 14 | 15 | const examples = [ 16 | { 17 | usage: 'vip @example-app.develop db phpmyadmin', 18 | description: 19 | "Generate access to a read-only phpMyAdmin web interface for the environment's database.", 20 | }, 21 | ]; 22 | 23 | const appQuery = ` 24 | id, 25 | name, 26 | environments{ 27 | id 28 | appId 29 | name 30 | type 31 | uniqueLabel 32 | } 33 | `; 34 | 35 | void command( { 36 | appContext: true, 37 | appQuery, 38 | envContext: true, 39 | module: 'phpmyadmin', 40 | requiredArgs: 0, 41 | usage: 'vip db phpmyadmin', 42 | } ) 43 | .examples( examples ) 44 | .argv( process.argv, async ( arg: string[], { app, env }: { app: App; env: AppEnvironment } ) => { 45 | const trackerFn = makeCommandTracker( 'phpmyadmin', { 46 | app: app.id, 47 | env: env.uniqueLabel, 48 | } ); 49 | await trackerFn( 'execute' ); 50 | 51 | const cmd = new PhpMyAdminCommand( app, env, trackerFn ); 52 | await cmd.run(); 53 | 54 | await trackerFn( 'success' ); 55 | } ); 56 | -------------------------------------------------------------------------------- /src/bin/vip-db.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * External dependencies 5 | */ 6 | 7 | /** 8 | * Internal dependencies 9 | */ 10 | import command from '../lib/cli/command'; 11 | import { trackEvent } from '../lib/tracker'; 12 | 13 | void command( { usage: 'vip db' } ) 14 | .command( 15 | 'phpmyadmin', 16 | 'Generate access to a read-only phpMyAdmin web interface for an environment database.' 17 | ) 18 | .example( 19 | 'vip @example-app.develop db phpmyadmin', 20 | "Generate access to a read-only phpMyAdmin web interface for the environment's database." 21 | ) 22 | .argv( process.argv, async () => { 23 | await trackEvent( 'vip_db_command_execute' ); 24 | } ); 25 | -------------------------------------------------------------------------------- /src/bin/vip-dev-env-import-media.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import command from '../lib/cli/command'; 4 | import { 5 | getEnvironmentName, 6 | getEnvTrackingInfo, 7 | handleCLIException, 8 | processSlug, 9 | } from '../lib/dev-environment/dev-environment-cli'; 10 | import { importMediaPath } from '../lib/dev-environment/dev-environment-core'; 11 | import { trackEvent } from '../lib/tracker'; 12 | 13 | const exampleUsage = 'vip dev-env import media'; 14 | const usage = 'vip dev-env import media'; 15 | 16 | const examples = [ 17 | { 18 | usage: `${ exampleUsage } /Users/example/Desktop/uploads --slug="example-site"`, 19 | description: 20 | 'Import the contents of the "uploads" directory from a path on the user\'s local machine to the "/wp-content/uploads" directory of the local environment named "example-site".', 21 | }, 22 | ]; 23 | 24 | command( { 25 | requiredArgs: 1, 26 | usage, 27 | } ) 28 | .examples( examples ) 29 | .option( 30 | 'slug', 31 | 'A unique name for a local environment. Default is "vip-local".', 32 | undefined, 33 | processSlug 34 | ) 35 | .argv( process.argv, async ( unmatchedArgs, opt ) => { 36 | const [ filePath ] = unmatchedArgs; 37 | const slug = await getEnvironmentName( opt ); 38 | 39 | const trackingInfo = getEnvTrackingInfo( slug ); 40 | await trackEvent( 'dev_env_import_media_command_execute', trackingInfo ); 41 | 42 | try { 43 | await importMediaPath( slug, filePath ); 44 | await trackEvent( 'dev_env_import_media_command_success', trackingInfo ); 45 | } catch ( error ) { 46 | await handleCLIException( error, 'dev_env_import_media_command_error', trackingInfo ); 47 | process.exitCode = 1; 48 | } 49 | } ); 50 | -------------------------------------------------------------------------------- /src/bin/vip-dev-env-import.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import command from '../lib/cli/command'; 4 | 5 | const exampleUsage = 'vip dev-env import'; 6 | const usage = 'vip dev-env import'; 7 | 8 | const examples = [ 9 | { 10 | usage: `${ exampleUsage } sql /Users/example/Downloads/file.sql`, 11 | description: 12 | 'Import the SQL file named "file.sql" from a path on the user\'s local machine to a running local environment.', 13 | }, 14 | { 15 | usage: `${ exampleUsage } media /Users/example/Desktop/uploads`, 16 | description: 17 | 'Import the contents of the "uploads" directory from a path on the user\'s local machine to the "/wp-content/uploads" directory of a running local environment.', 18 | }, 19 | ]; 20 | 21 | command( { 22 | requiredArgs: 1, 23 | usage, 24 | } ) 25 | .examples( examples ) 26 | .command( 'sql', 'Import a SQL file to a running local environment.' ) 27 | .command( 'media', 'Import media files to a running local environment.' ) 28 | .argv( process.argv ); 29 | -------------------------------------------------------------------------------- /src/bin/vip-dev-env-list.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import command from '../lib/cli/command'; 4 | import { 5 | handleCLIException, 6 | validateDependencies, 7 | } from '../lib/dev-environment/dev-environment-cli'; 8 | import { printAllEnvironmentsInfo } from '../lib/dev-environment/dev-environment-core'; 9 | import { bootstrapLando } from '../lib/dev-environment/dev-environment-lando'; 10 | import { trackEvent } from '../lib/tracker'; 11 | 12 | const exampleUsage = 'vip dev-env list'; 13 | const usage = 'vip dev-env list'; 14 | 15 | const examples = [ 16 | { 17 | usage: `${ exampleUsage }`, 18 | description: 'Retrieve basic information about all local environments.', 19 | }, 20 | ]; 21 | 22 | command( { 23 | usage, 24 | } ) 25 | .examples( examples ) 26 | .argv( process.argv, async () => { 27 | const lando = await bootstrapLando(); 28 | lando.events.constructor.prototype.setMaxListeners( 1024 ); 29 | validateDependencies( lando ); 30 | 31 | const trackingInfo = { all: true }; 32 | await trackEvent( 'dev_env_list_command_execute', trackingInfo ); 33 | 34 | try { 35 | await printAllEnvironmentsInfo( lando, {} ); 36 | await trackEvent( 'dev_env_list_command_success', trackingInfo ); 37 | } catch ( error ) { 38 | await handleCLIException( error, 'dev_env_list_command_error', trackingInfo ); 39 | process.exitCode = 1; 40 | } 41 | } ); 42 | -------------------------------------------------------------------------------- /src/bin/vip-dev-env-sync.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import command from '../lib/cli/command'; 4 | const usage = 'vip dev-env sync'; 5 | 6 | const examples = [ 7 | { 8 | usage: `vip @example-app.develop dev-env sync sql --slug=example-site`, 9 | description: 10 | 'Sync the database of the develop environment in the "example-app" application to a local environment named "example-site".', 11 | }, 12 | ]; 13 | 14 | command( { 15 | requiredArgs: 1, 16 | usage, 17 | } ) 18 | .examples( examples ) 19 | .command( 'sql', 'Sync the database of a VIP Platform environment to a local environment.' ) 20 | .argv( process.argv ); 21 | -------------------------------------------------------------------------------- /src/bin/vip-dev-env.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import command from '../lib/cli/command'; 4 | 5 | if ( process.getuid?.() === 0 ) { 6 | console.error( 'This script should not be run as root. Exiting.' ); 7 | process.exit( 1 ); 8 | } 9 | 10 | command( { 11 | requiredArgs: 0, 12 | } ) 13 | .command( 'create', 'Create a new local environment.' ) 14 | .command( 'update', 'Update the settings of a local environment.' ) 15 | .command( 'start', 'Start a local environment.' ) 16 | .command( 'stop', 'Stop a local environment.' ) 17 | .command( 'destroy', 'Remove a local environment.' ) 18 | .command( 'info', 'Retrieve information about a local environment.' ) 19 | .command( 'list', 'Retrieve information about all local environments.' ) 20 | .command( 'exec', 'Run a WP-CLI command against a local environment.' ) 21 | .command( 'import', 'Import media or database files to a local environment.' ) 22 | .command( 'shell', 'Create a shell and run commands against a local environment.' ) 23 | .command( 'logs', 'Retrieve logs for a local environment.' ) 24 | .command( 'sync', 'Sync the database of a VIP Platform environment to a local environment.' ) 25 | .command( 'purge', 'Remove all local environments.' ) 26 | .argv( process.argv ); 27 | -------------------------------------------------------------------------------- /src/bin/vip-export-sql.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { ExportSQLCommand } from '../commands/export-sql'; 4 | import command from '../lib/cli/command'; 5 | import { makeCommandTracker } from '../lib/tracker'; 6 | 7 | const examples = [ 8 | { 9 | usage: 'vip @example-app.develop export sql', 10 | description: 11 | 'Download an archived copy of the most recent database backup for an environment to the current local directory.', 12 | }, 13 | { 14 | usage: 'vip @example-app.develop export sql --output=~/Desktop/export.sql.gz', 15 | description: 16 | 'Download an archived copy of the most recent database backup for an environment to a specific file path.', 17 | }, 18 | { 19 | usage: 'vip @example-app.develop export sql --generate-backup', 20 | description: 21 | 'Generate a fresh database backup for an environment and download a copy of that backup.', 22 | }, 23 | ]; 24 | 25 | const appQuery = ` 26 | id, 27 | name, 28 | type, 29 | organization { id, name }, 30 | environments{ 31 | id 32 | appId 33 | type 34 | name 35 | primaryDomain { name } 36 | uniqueLabel 37 | } 38 | `; 39 | 40 | command( { 41 | appContext: true, 42 | appQuery, 43 | envContext: true, 44 | module: 'export-sql', 45 | requiredArgs: 0, 46 | usage: 'vip export sql', 47 | } ) 48 | .option( 49 | 'output', 50 | 'Download the file to a specific local directory path with a custom file name.' 51 | ) 52 | .option( 'generate-backup', 'Generate a fresh database backup and export a copy of that backup.' ) 53 | .examples( examples ) 54 | .argv( process.argv, async ( arg, { app, env, output, generateBackup } ) => { 55 | const trackerFn = makeCommandTracker( 'export_sql', { 56 | app: app.id, 57 | env: env.uniqueLabel, 58 | generate_backup: generateBackup, 59 | } ); 60 | await trackerFn( 'execute' ); 61 | 62 | const exportCommand = new ExportSQLCommand( 63 | app, 64 | env, 65 | { outputFile: output, generateBackup }, 66 | trackerFn 67 | ); 68 | await exportCommand.run(); 69 | await trackerFn( 'success' ); 70 | } ); 71 | -------------------------------------------------------------------------------- /src/bin/vip-export.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import command from '../lib/cli/command'; 4 | import { trackEvent } from '../lib/tracker'; 5 | 6 | command() 7 | .command( 8 | 'sql', 9 | 'Generate a copy of a database backup for an environment and download it as an archived SQL file.' 10 | ) 11 | .example( 12 | 'vip @example-app.develop export sql', 13 | 'Download a copy of the most recent database backup for an environment as an archived SQL file to the current local directory.' 14 | ) 15 | .argv( process.argv, async () => { 16 | await trackEvent( 'vip_export_command_execute' ); 17 | } ); 18 | -------------------------------------------------------------------------------- /src/bin/vip-import-media-abort.generated.d.ts: -------------------------------------------------------------------------------- 1 | import * as Types from '../graphqlTypes'; 2 | 3 | export type AbortMediaImportMutationVariables = Types.Exact< { 4 | input?: Types.InputMaybe< Types.AppEnvironmentAbortMediaImportInput >; 5 | } >; 6 | 7 | export type AbortMediaImportMutation = { 8 | __typename?: 'Mutation'; 9 | abortMediaImport: { 10 | __typename?: 'AppEnvironmentAbortMediaImportPayload'; 11 | applicationId?: number | null; 12 | environmentId?: number | null; 13 | mediaImportStatusChange?: { 14 | __typename?: 'AppEnvironmentMediaImportStatusChange'; 15 | importId?: number | null; 16 | siteId?: number | null; 17 | statusFrom?: string | null; 18 | statusTo?: string | null; 19 | } | null; 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /src/bin/vip-import-media.generated.d.ts: -------------------------------------------------------------------------------- 1 | import * as Types from '../graphqlTypes'; 2 | 3 | export type StartMediaImportMutationVariables = Types.Exact< { 4 | input?: Types.InputMaybe< Types.AppEnvironmentStartMediaImportInput >; 5 | } >; 6 | 7 | export type StartMediaImportMutation = { 8 | __typename?: 'Mutation'; 9 | startMediaImport?: { 10 | __typename?: 'AppEnvironmentMediaImportPayload'; 11 | applicationId?: number | null; 12 | environmentId?: number | null; 13 | mediaImportStatus: { 14 | __typename?: 'AppEnvironmentMediaImportStatus'; 15 | importId?: number | null; 16 | siteId?: number | null; 17 | status?: string | null; 18 | }; 19 | } | null; 20 | }; 21 | -------------------------------------------------------------------------------- /src/bin/vip-import-sql-status.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import command from '../lib/cli/command'; 4 | import * as exit from '../lib/cli/exit'; 5 | import { ProgressTracker } from '../lib/cli/progress'; 6 | import { isSupportedApp } from '../lib/site-import/db-file-import'; 7 | import { importSqlCheckStatus } from '../lib/site-import/status'; 8 | import { trackEventWithEnv } from '../lib/tracker'; 9 | 10 | const appQuery = ` 11 | id, 12 | name, 13 | type, 14 | typeId, 15 | environments{ 16 | id 17 | appId 18 | type 19 | name 20 | isK8sResident 21 | primaryDomain { 22 | id 23 | name 24 | } 25 | } 26 | `; 27 | 28 | const usage = 'vip import sql status'; 29 | 30 | // Command examples 31 | const examples = [ 32 | { 33 | usage: 'vip @example-app.develop import sql status', 34 | description: 35 | 'Check the status of the most recent SQL database import to the develop environment of the "example-app" application.\n' + 36 | ' * If the import is still in progress, the command will poll until the import is complete.', 37 | }, 38 | ]; 39 | 40 | command( { 41 | appContext: true, 42 | appQuery, 43 | envContext: true, 44 | requiredArgs: 0, 45 | usage, 46 | } ) 47 | .examples( examples ) 48 | .argv( process.argv, async ( arg, { app, env } ) => { 49 | const { id: envId, appId } = env; 50 | const track = trackEventWithEnv.bind( null, appId, envId ); 51 | 52 | if ( ! isSupportedApp( app ) ) { 53 | await track( 'import_sql_command_error', { errorType: 'unsupported-app' } ); 54 | exit.withError( 55 | 'The type of application you specified does not currently support SQL imports.' 56 | ); 57 | } 58 | 59 | await track( 'import_sql_check_status_command_execute' ); 60 | 61 | const progressTracker = new ProgressTracker( [] ); 62 | progressTracker.prefix = ` 63 | ============================================================= 64 | Checking the SQL import status for your environment... 65 | `; 66 | 67 | await importSqlCheckStatus( { 68 | app, 69 | env, 70 | progressTracker, 71 | shouldReturnMissingJobImmediately: true, 72 | } ); 73 | } ); 74 | -------------------------------------------------------------------------------- /src/bin/vip-import-sql.generated.d.ts: -------------------------------------------------------------------------------- 1 | import * as Types from '../graphqlTypes'; 2 | 3 | export type StartImportMutationVariables = Types.Exact< { 4 | input?: Types.InputMaybe< Types.AppEnvironmentImportInput >; 5 | } >; 6 | 7 | export type StartImportMutation = { 8 | __typename?: 'Mutation'; 9 | startImport: { 10 | __typename?: 'AppEnvironmentImportPayload'; 11 | message?: string | null; 12 | success?: boolean | null; 13 | app?: { __typename?: 'App'; id?: number | null; name?: string | null } | null; 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /src/bin/vip-import-validate-sql.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import command from '../lib/cli/command'; 4 | import * as exit from '../lib/cli/exit'; 5 | import { validate } from '../lib/validations/sql'; 6 | 7 | const usage = 'vip import validate-sql'; 8 | 9 | command( { 10 | requiredArgs: 1, 11 | usage, 12 | } ) 13 | .example( 14 | 'vip import validate-sql file.sql', 15 | 'Scan the local file named "file.sql" for SQL validation errors and potential incompatibilities with platform databases.' 16 | ) 17 | .argv( process.argv, async arg => { 18 | const filename = arg[ 0 ]; 19 | if ( ! filename ) { 20 | exit.withError( 'You must pass in a filename.' ); 21 | } 22 | 23 | await validate( filename ); 24 | } ); 25 | -------------------------------------------------------------------------------- /src/bin/vip-import.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import command from '../lib/cli/command'; 4 | import { trackEvent } from '../lib/tracker'; 5 | 6 | command() 7 | .command( 'sql', 'Import a SQL database file to an environment.' ) 8 | .command( 'validate-sql', 'Validate a local SQL database file prior to import.' ) 9 | .command( 10 | 'validate-files', 11 | 'Validate the directory structure and contents of a local media file directory prior to archiving and uploading it to a publicly accessible URL.' 12 | ) 13 | .command( 14 | 'media', 15 | 'Import media files to an environment from an archived file located at a publicly accessible URL.' 16 | ) 17 | .example( 18 | 'vip @example-app.develop import sql example-file.sql', 19 | 'Import the local SQL database file "example-file.sql" to the develop environment.' 20 | ) 21 | .example( 22 | 'vip @example-app.production import media https://www.example.com/uploads.tar.gz', 23 | 'Import an archived file from a publicly accessible URL to the production environment.' 24 | ) 25 | .argv( process.argv, async () => { 26 | await trackEvent( 'vip_import_command_execute' ); 27 | } ); 28 | -------------------------------------------------------------------------------- /src/bin/vip-logout.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import command from '../lib/cli/command'; 4 | import logout from '../lib/logout'; 5 | 6 | void command( { usage: 'vip logout' } ) 7 | .examples( [ 8 | { 9 | usage: 'vip logout', 10 | description: 'Log out the current authenticated VIP-CLI user.', 11 | }, 12 | ] ) 13 | .argv( process.argv, async () => { 14 | await logout(); 15 | 16 | console.log( 'You are successfully logged out.' ); 17 | } ); 18 | -------------------------------------------------------------------------------- /src/bin/vip-sync.generated.d.ts: -------------------------------------------------------------------------------- 1 | import * as Types from '../graphqlTypes'; 2 | 3 | export type SyncEnvironmentMutationMutationVariables = Types.Exact< { 4 | input?: Types.InputMaybe< Types.AppEnvironmentSyncInput >; 5 | } >; 6 | 7 | export type SyncEnvironmentMutationMutation = { 8 | __typename?: 'Mutation'; 9 | syncEnvironment: { 10 | __typename?: 'AppEnvironmentSyncPayload'; 11 | environment?: { __typename?: 'AppEnvironment'; id?: number | null } | null; 12 | }; 13 | }; 14 | 15 | export type AppQueryVariables = Types.Exact< { 16 | id?: Types.InputMaybe< Types.Scalars[ 'Int' ][ 'input' ] >; 17 | sync?: Types.InputMaybe< Types.Scalars[ 'Int' ][ 'input' ] >; 18 | } >; 19 | 20 | export type AppQuery = { 21 | __typename?: 'Query'; 22 | app?: { 23 | __typename?: 'App'; 24 | id?: number | null; 25 | name?: string | null; 26 | environments?: Array< { 27 | __typename?: 'AppEnvironment'; 28 | id?: number | null; 29 | name?: string | null; 30 | defaultDomain?: string | null; 31 | branch?: string | null; 32 | datacenter?: string | null; 33 | syncProgress?: { 34 | __typename?: 'AppEnvironmentSyncProgress'; 35 | status?: string | null; 36 | sync?: number | null; 37 | steps?: Array< { 38 | __typename?: 'AppEnvironmentSyncStep'; 39 | name?: string | null; 40 | status?: string | null; 41 | } | null > | null; 42 | } | null; 43 | } | null > | null; 44 | } | null; 45 | }; 46 | -------------------------------------------------------------------------------- /src/bin/vip-validate-preflight.generated.d.ts: -------------------------------------------------------------------------------- 1 | import * as Types from '../graphqlTypes'; 2 | 3 | export type BuildConfigQueryVariables = Types.Exact< { 4 | appId?: Types.InputMaybe< Types.Scalars[ 'Int' ][ 'input' ] >; 5 | envId?: Types.InputMaybe< Types.Scalars[ 'Int' ][ 'input' ] >; 6 | } >; 7 | 8 | export type BuildConfigQuery = { 9 | __typename?: 'Query'; 10 | app?: { 11 | __typename?: 'App'; 12 | environments?: Array< { 13 | __typename?: 'AppEnvironment'; 14 | id?: number | null; 15 | buildConfiguration?: { 16 | __typename?: 'BuildConfiguration'; 17 | buildType: string; 18 | nodeBuildDockerEnv: string; 19 | nodeJSVersion: string; 20 | npmToken?: string | null; 21 | } | null; 22 | } | null > | null; 23 | } | null; 24 | }; 25 | -------------------------------------------------------------------------------- /src/bin/vip-validate.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import command from '../lib/cli/command'; 4 | import { trackEvent } from '../lib/tracker'; 5 | 6 | command( { 7 | requiredArgs: 0, 8 | } ) 9 | .command( 10 | 'preflight', 11 | 'Run a full suite of validation tests against a local Node.js codebase to identify potential issues that could prevent successful building or deploying.' 12 | ) 13 | .argv( process.argv, async () => { 14 | await trackEvent( 'vip_validate_command_execute' ); 15 | } ); 16 | -------------------------------------------------------------------------------- /src/bin/vip-whoami.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { Me } from '../graphqlTypes'; 4 | import { getCurrentUserInfo } from '../lib/api/user'; 5 | import command from '../lib/cli/command'; 6 | import * as exit from '../lib/cli/exit'; 7 | import { trackEvent } from '../lib/tracker'; 8 | 9 | const baseUsage = 'vip whoami'; 10 | 11 | export async function whoamiCommand() { 12 | const trackingParams: { command: string } = { 13 | command: 'vip whoami', 14 | }; 15 | 16 | await trackEvent( 'whoami_command_execute', trackingParams ); 17 | 18 | let currentUser: Me; 19 | try { 20 | currentUser = await getCurrentUserInfo(); 21 | } catch ( err: unknown ) { 22 | const error = err instanceof Error ? err : new Error( 'Unknown error' ); 23 | await trackEvent( 'whoami_command_error', { ...trackingParams, error: error.message } ); 24 | 25 | exit.withError( 26 | `Failed to fetch information about the currently logged-in user error: ${ error.message }` 27 | ); 28 | } 29 | 30 | await trackEvent( 'whoami_command_success', trackingParams ); 31 | 32 | const output: string[] = [ 33 | `- Howdy ${ currentUser.displayName ?? 'user' }!`, 34 | `- Your user ID is ${ currentUser.id ?? ' not found' }`, 35 | ]; 36 | 37 | if ( currentUser.isVIP ) { 38 | output.push( '- Your account has VIP Staff permissions' ); 39 | } 40 | 41 | console.log( output.join( '\n' ) ); 42 | } 43 | 44 | void command( { usage: baseUsage } ) 45 | .examples( [ 46 | { 47 | usage: baseUsage + '\n' + ' - Howdy user@example.com!\n' + ' - Your user ID is 1234\n', 48 | description: 'Retrieve details about the current authenticated VIP-CLI user.', 49 | }, 50 | ] ) 51 | .argv( process.argv, whoamiCommand ); 52 | -------------------------------------------------------------------------------- /src/bin/vip-wp.generated.d.ts: -------------------------------------------------------------------------------- 1 | import * as Types from '../graphqlTypes'; 2 | 3 | export type TriggerWpcliCommandMutationMutationVariables = Types.Exact< { 4 | input?: Types.InputMaybe< Types.AppEnvironmentTriggerWpcliCommandInput >; 5 | } >; 6 | 7 | export type TriggerWpcliCommandMutationMutation = { 8 | __typename?: 'Mutation'; 9 | triggerWPCLICommandOnAppEnvironment: { 10 | __typename?: 'AppEnvironmentTriggerWPCLICommandPayload'; 11 | inputToken?: string | null; 12 | command?: { __typename?: 'WPCLICommand'; guid?: string | null } | null; 13 | }; 14 | }; 15 | -------------------------------------------------------------------------------- /src/commands/backup-db.generated.d.ts: -------------------------------------------------------------------------------- 1 | import * as Types from '../graphqlTypes'; 2 | 3 | export type TriggerDatabaseBackupMutationVariables = Types.Exact< { 4 | input?: Types.InputMaybe< Types.AppEnvironmentTriggerDbBackupInput >; 5 | } >; 6 | 7 | export type TriggerDatabaseBackupMutation = { 8 | __typename?: 'Mutation'; 9 | triggerDatabaseBackup: { 10 | __typename?: 'AppEnvironmentTriggerDBBackupPayload'; 11 | success?: boolean | null; 12 | }; 13 | }; 14 | 15 | export type AppBackupJobStatusQueryVariables = Types.Exact< { 16 | appId: Types.Scalars[ 'Int' ][ 'input' ]; 17 | envId: Types.Scalars[ 'Int' ][ 'input' ]; 18 | } >; 19 | 20 | export type AppBackupJobStatusQuery = { 21 | __typename?: 'Query'; 22 | app?: { 23 | __typename?: 'App'; 24 | id?: number | null; 25 | environments?: Array< { 26 | __typename?: 'AppEnvironment'; 27 | id?: number | null; 28 | jobs?: Array< 29 | | { 30 | __typename?: 'Job'; 31 | id?: number | null; 32 | type?: string | null; 33 | completedAt?: string | null; 34 | createdAt?: string | null; 35 | inProgressLock?: boolean | null; 36 | metadata?: Array< { 37 | __typename?: 'JobMetadata'; 38 | name?: string | null; 39 | value?: string | null; 40 | } | null > | null; 41 | progress?: { __typename?: 'JobProgress'; status?: string | null } | null; 42 | } 43 | | { 44 | __typename?: 'PrimaryDomainSwitchJob'; 45 | id?: number | null; 46 | type?: string | null; 47 | completedAt?: string | null; 48 | createdAt?: string | null; 49 | inProgressLock?: boolean | null; 50 | metadata?: Array< { 51 | __typename?: 'JobMetadata'; 52 | name?: string | null; 53 | value?: string | null; 54 | } | null > | null; 55 | progress?: { __typename?: 'JobProgress'; status?: string | null } | null; 56 | } 57 | | null 58 | > | null; 59 | } | null > | null; 60 | } | null; 61 | }; 62 | -------------------------------------------------------------------------------- /src/commands/phpmyadmin.generated.d.ts: -------------------------------------------------------------------------------- 1 | import * as Types from '../graphqlTypes'; 2 | 3 | export type GeneratePhpMyAdminAccessMutationVariables = Types.Exact< { 4 | input?: Types.InputMaybe< Types.GeneratePhpMyAdminAccessInput >; 5 | } >; 6 | 7 | export type GeneratePhpMyAdminAccessMutation = { 8 | __typename?: 'Mutation'; 9 | generatePHPMyAdminAccess?: { 10 | __typename?: 'GeneratePhpMyAdminAccessPayload'; 11 | expiresAt?: any | null; 12 | url?: string | null; 13 | } | null; 14 | }; 15 | 16 | export type PhpMyAdminStatusQueryVariables = Types.Exact< { 17 | appId: Types.Scalars[ 'Int' ][ 'input' ]; 18 | envId: Types.Scalars[ 'Int' ][ 'input' ]; 19 | } >; 20 | 21 | export type PhpMyAdminStatusQuery = { 22 | __typename?: 'Query'; 23 | app?: { 24 | __typename?: 'App'; 25 | environments?: Array< { 26 | __typename?: 'AppEnvironment'; 27 | phpMyAdminStatus?: { __typename?: 'PHPMyAdminStatus'; status?: string | null } | null; 28 | } | null > | null; 29 | } | null; 30 | }; 31 | 32 | export type EnablePhpMyAdminMutationVariables = Types.Exact< { 33 | input?: Types.InputMaybe< Types.EnablePhpMyAdminInput >; 34 | } >; 35 | 36 | export type EnablePhpMyAdminMutation = { 37 | __typename?: 'Mutation'; 38 | enablePHPMyAdmin?: { __typename?: 'EnablePhpMyAdminPayload'; success?: boolean | null } | null; 39 | }; 40 | -------------------------------------------------------------------------------- /src/lib/analytics/clients/client.ts: -------------------------------------------------------------------------------- 1 | import type { Response } from 'node-fetch'; 2 | 3 | export interface AnalyticsClient { 4 | trackEvent( name: string, props?: Record< string, unknown > ): Promise< Response | false >; 5 | } 6 | -------------------------------------------------------------------------------- /src/lib/analytics/clients/pendo.ts: -------------------------------------------------------------------------------- 1 | import debugLib from 'debug'; 2 | import { Response } from 'node-fetch'; 3 | 4 | import http from '../../../lib/api/http'; 5 | import { type Env } from '../../env'; 6 | 7 | import type { AnalyticsClient } from './client'; 8 | 9 | const debug = debugLib( '@automattic/vip:analytics:clients:pendo' ); 10 | 11 | interface PendoOptions { 12 | userId: string; 13 | eventPrefix: string; 14 | env: Env; 15 | } 16 | 17 | /** 18 | * Pendo analytics client. 19 | */ 20 | export default class Pendo implements AnalyticsClient { 21 | private eventPrefix: string; 22 | private userAgent: string; 23 | private userId: string; 24 | private context: Env & Record< string, unknown > & { userId?: string }; 25 | 26 | public static readonly ENDPOINT = '/pendo'; 27 | 28 | constructor( options: PendoOptions ) { 29 | this.eventPrefix = options.eventPrefix; 30 | this.userAgent = options.env.userAgent; 31 | this.userId = options.userId; 32 | this.context = { ...options.env }; 33 | } 34 | 35 | public async trackEvent( 36 | eventName: string, 37 | eventProps: Record< string, unknown > = {} 38 | ): Promise< Response | false > { 39 | if ( ! eventName.startsWith( this.eventPrefix ) ) { 40 | eventName = this.eventPrefix + eventName; 41 | } 42 | 43 | debug( 'trackEvent()', eventProps ); 44 | 45 | this.context = { 46 | ...this.context, 47 | org_id: eventProps.org_slug, 48 | org_slug: eventProps.org_slug, 49 | userAgent: this.userAgent, 50 | userId: this.userId, 51 | }; 52 | 53 | try { 54 | return await this.send( eventName, eventProps ); 55 | } catch ( error ) { 56 | debug( error ); 57 | return Promise.resolve( false ); 58 | } 59 | } 60 | 61 | public async send( 62 | eventName: string, 63 | eventProps: Record< string, unknown > 64 | ): Promise< Response > { 65 | const body = { 66 | context: this.context, 67 | event: eventName, 68 | properties: eventProps, 69 | timestamp: Date.now(), 70 | type: 'track', 71 | visitorId: `${ this.context.userId as string }`, 72 | }; 73 | 74 | debug( 'send()', body ); 75 | 76 | const response = await http( Pendo.ENDPOINT, { 77 | method: 'POST', 78 | body: JSON.stringify( body ), 79 | } ); 80 | 81 | const responseText = await response.text(); 82 | 83 | debug( 'response', responseText ); 84 | 85 | return response; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/lib/analytics/index.ts: -------------------------------------------------------------------------------- 1 | import debugLib from 'debug'; 2 | 3 | import env from '../env'; 4 | 5 | import type { AnalyticsClient } from './clients/client'; 6 | import type { Response } from 'node-fetch'; 7 | 8 | const debug = debugLib( '@automattic/vip:analytics' ); 9 | 10 | /* eslint-disable camelcase */ 11 | const client_info = { 12 | cli_version: env.app.version, 13 | os_name: env.os.name, 14 | os_version: env.os.version, 15 | node_version: env.node.version, 16 | }; 17 | /* eslint-enable camelcase */ 18 | 19 | export default class Analytics { 20 | private clients: AnalyticsClient[]; 21 | 22 | constructor( clients: AnalyticsClient[] ) { 23 | this.clients = clients; 24 | } 25 | 26 | public async trackEvent( 27 | name: string, 28 | props: Record< string, unknown > = {} 29 | ): Promise< ( Response | false )[] > { 30 | if ( process.env.DO_NOT_TRACK ) { 31 | debug( `trackEvent() for ${ name } => skipping per DO_NOT_TRACK variable` ); 32 | return []; 33 | } 34 | 35 | return Promise.all( 36 | this.clients.map( client => 37 | client.trackEvent( name, { 38 | // eslint-disable-next-line camelcase 39 | ...client_info, 40 | ...props, 41 | } ) 42 | ) 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/lib/api/app.ts: -------------------------------------------------------------------------------- 1 | import { DocumentNode } from '@apollo/client'; 2 | import { QueryOptions } from '@apollo/client/core/watchQueryOptions'; 3 | import gql from 'graphql-tag'; 4 | 5 | import { App, Exact, Scalars } from '../../graphqlTypes'; 6 | import API from '../../lib/api'; 7 | 8 | type AppQueryVariables = Exact< { 9 | name: Scalars[ 'String' ][ 'input' ]; 10 | } >; 11 | 12 | interface AppQueryResult { 13 | apps?: { 14 | edges?: App[]; 15 | }; 16 | } 17 | 18 | type AppByIdQueryVariables = Exact< { 19 | id: Scalars[ 'Int' ][ 'input' ]; 20 | } >; 21 | 22 | interface AppByIdQueryResult { 23 | app?: App; 24 | } 25 | 26 | interface AppQueryOptions extends QueryOptions { 27 | query: DocumentNode; 28 | variables: { 29 | id: number; 30 | }; 31 | context?: { 32 | headers: { 33 | Authorization: string; 34 | }; 35 | }; 36 | } 37 | 38 | export default async function ( 39 | app: string | number, 40 | fields: string = 'id,name', 41 | fragments: string = '' 42 | ): Promise< Partial< App > > { 43 | const api = API(); 44 | if ( isNaN( Number( app ) ) ) { 45 | const res = await api.query< AppQueryResult, AppQueryVariables >( { 46 | query: gql`query App( $name: String ) { 47 | apps( first: 1, name: $name ) { 48 | total, 49 | nextCursor, 50 | edges { 51 | ${ fields } 52 | } 53 | } 54 | } 55 | ${ fragments || '' }`, 56 | variables: { 57 | name: app as string, 58 | }, 59 | } ); 60 | 61 | if ( ! res.data.apps?.edges?.length ) { 62 | return {}; 63 | } 64 | 65 | return res.data.apps.edges[ 0 ]; 66 | } 67 | 68 | if ( typeof app === 'string' ) { 69 | app = parseInt( app, 10 ); 70 | } 71 | 72 | const appQuery: AppQueryOptions = { 73 | query: gql`query App( $id: Int ) { 74 | app( id: $id ){ 75 | ${ fields } 76 | } 77 | } 78 | ${ fragments || '' }`, 79 | variables: { 80 | id: app, 81 | }, 82 | }; 83 | 84 | const customDeployToken = process.env.WPVIP_DEPLOY_TOKEN; 85 | if ( customDeployToken ) { 86 | appQuery.context = { 87 | headers: { 88 | Authorization: `Bearer ${ customDeployToken }`, 89 | }, 90 | }; 91 | } 92 | 93 | const res = await api.query< AppByIdQueryResult, AppByIdQueryVariables >( appQuery ); 94 | 95 | if ( ! res.data.app ) { 96 | return {}; 97 | } 98 | 99 | return res.data.app; 100 | } 101 | -------------------------------------------------------------------------------- /src/lib/api/cache-purge.generated.d.ts: -------------------------------------------------------------------------------- 1 | import * as Types from '../../graphqlTypes'; 2 | 3 | export type PurgePageCacheMutationMutationVariables = Types.Exact< { 4 | appId: Types.Scalars[ 'Int' ][ 'input' ]; 5 | envId: Types.Scalars[ 'Int' ][ 'input' ]; 6 | urls: Array< Types.Scalars[ 'String' ][ 'input' ] > | Types.Scalars[ 'String' ][ 'input' ]; 7 | } >; 8 | 9 | export type PurgePageCacheMutationMutation = { 10 | __typename?: 'Mutation'; 11 | purgePageCache: { __typename?: 'PurgePageCachePayload'; success: boolean; urls: Array< string > }; 12 | }; 13 | -------------------------------------------------------------------------------- /src/lib/api/cache-purge.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | import API from '../../lib/api'; 4 | 5 | import type { 6 | PurgePageCacheMutationMutation, 7 | PurgePageCacheMutationMutationVariables, 8 | } from './cache-purge.generated'; 9 | import type { PurgePageCachePayload } from '../../graphqlTypes'; 10 | 11 | const mutation = gql` 12 | mutation PurgePageCacheMutation($appId: Int!, $envId: Int!, $urls: [String!]!) { 13 | purgePageCache(input: { appId: $appId, environmentId: $envId, urls: $urls }) { 14 | success 15 | urls 16 | } 17 | } 18 | `; 19 | 20 | // The subquery for environments lets users choose any environment, including production. 21 | export const appQuery = ` 22 | id 23 | name 24 | environments { 25 | id 26 | appId 27 | name 28 | primaryDomain { 29 | name 30 | } 31 | type 32 | } 33 | `; 34 | 35 | export async function purgeCache( 36 | appId: number, 37 | envId: number, 38 | urls: string[] 39 | ): Promise< PurgePageCachePayload | null > { 40 | const api = API(); 41 | 42 | const variables = { 43 | appId, 44 | envId, 45 | urls, 46 | }; 47 | 48 | const response = await api.mutate< 49 | PurgePageCacheMutationMutation, 50 | PurgePageCacheMutationMutationVariables 51 | >( { mutation, variables } ); 52 | return response.data?.purgePageCache ?? null; 53 | } 54 | -------------------------------------------------------------------------------- /src/lib/api/feature-flags.generated.d.ts: -------------------------------------------------------------------------------- 1 | import * as Types from '../../graphqlTypes'; 2 | 3 | export type IsVipQueryVariables = Types.Exact< { [ key: string ]: never } >; 4 | 5 | export type IsVipQuery = { 6 | __typename?: 'Query'; 7 | me?: { __typename?: 'Me'; isVIP?: boolean | null } | null; 8 | }; 9 | -------------------------------------------------------------------------------- /src/lib/api/feature-flags.ts: -------------------------------------------------------------------------------- 1 | import { ApolloClient, ApolloQueryResult, NormalizedCacheObject } from '@apollo/client'; 2 | import gql from 'graphql-tag'; 3 | 4 | import API from '../../lib/api'; 5 | 6 | import type { IsVipQuery, IsVipQueryVariables } from './feature-flags.generated'; 7 | 8 | const api: ApolloClient< NormalizedCacheObject > = API( { silenceAuthErrors: true } ); 9 | 10 | const isVipQuery = gql` 11 | query isVIP { 12 | me { 13 | isVIP 14 | } 15 | } 16 | `; 17 | 18 | export async function get(): Promise< ApolloQueryResult< IsVipQuery > | undefined > { 19 | return await api.query< IsVipQuery, IsVipQueryVariables >( { 20 | query: isVipQuery, 21 | fetchPolicy: 'cache-first', 22 | } ); 23 | } 24 | -------------------------------------------------------------------------------- /src/lib/api/http.ts: -------------------------------------------------------------------------------- 1 | import debugLib from 'debug'; 2 | import fetch, { 3 | type BodyInit, 4 | type Response, 5 | type RequestInit, 6 | type HeadersInit, 7 | } from 'node-fetch'; 8 | 9 | import { API_HOST } from '../../lib/api'; 10 | import env from '../../lib/env'; 11 | import { createProxyAgent } from '../../lib/http/proxy-agent'; 12 | import Token from '../../lib/token'; 13 | 14 | const debug = debugLib( '@automattic/vip:http' ); 15 | 16 | export type FetchOptions = Omit< RequestInit, 'body' > & { 17 | // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents 18 | body?: BodyInit | Record< string, unknown >; 19 | headers?: HeadersInit | Record< string, string >; 20 | }; 21 | 22 | /** 23 | * Call the Public API with an arbitrary path (e.g. to connect to REST endpoints). 24 | * This will include the token in an Authorization header so requests are "logged-in." 25 | * 26 | * This is simply a wrapper around node-fetch 27 | * 28 | * @param {string} path API path to pass to `fetch` -- will be prefixed by the API_HOST 29 | * @param {Object} options options to pass to `fetch` 30 | * @return {Promise} Return value of the `fetch` call 31 | */ 32 | export default async ( path: string, options: FetchOptions = {} ): Promise< Response > => { 33 | let url = path; 34 | 35 | // For convenience, we support just passing in the path to this function... 36 | // but some things (Apollo) always pass the full url 37 | if ( ! path.startsWith( API_HOST ) ) { 38 | url = `${ API_HOST }${ path }`; 39 | } 40 | 41 | const authToken = await Token.get(); 42 | 43 | const proxyAgent = createProxyAgent( url ); 44 | 45 | debug( 'running fetch', url ); 46 | 47 | return fetch( url, { 48 | ...options, 49 | agent: proxyAgent ?? undefined, 50 | headers: { 51 | Authorization: `Bearer ${ authToken.raw }`, 52 | 'User-Agent': env.userAgent, 53 | 'Content-Type': 'application/json', 54 | ...( options.headers ?? {} ), 55 | }, 56 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 57 | body: typeof options.body === 'object' ? JSON.stringify( options.body ) : options.body, 58 | } ); 59 | }; 60 | -------------------------------------------------------------------------------- /src/lib/api/user.generated.d.ts: -------------------------------------------------------------------------------- 1 | import * as Types from '../../graphqlTypes'; 2 | 3 | export type MeQueryVariables = Types.Exact< { [ key: string ]: never } >; 4 | 5 | export type MeQuery = { 6 | __typename?: 'Query'; 7 | me?: { 8 | __typename?: 'Me'; 9 | id?: number | null; 10 | displayName?: string | null; 11 | isVIP?: boolean | null; 12 | organizationRoles?: { 13 | __typename?: 'UserOrganizationRoleList'; 14 | nodes?: Array< { 15 | __typename?: 'UserOrganizationRole'; 16 | organizationId?: number | null; 17 | roleId?: Types.OrgRoleId | null; 18 | } | null > | null; 19 | } | null; 20 | } | null; 21 | }; 22 | -------------------------------------------------------------------------------- /src/lib/api/user.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | import { IsVipQuery, IsVipQueryVariables } from './feature-flags.generated'; 4 | import API from '../../lib/api'; 5 | 6 | import type { Me } from '../../graphqlTypes'; 7 | 8 | const QUERY_CURRENT_USER = gql` 9 | query Me { 10 | me { 11 | id 12 | displayName 13 | isVIP 14 | organizationRoles { 15 | nodes { 16 | organizationId 17 | roleId 18 | } 19 | } 20 | } 21 | } 22 | `; 23 | 24 | export async function getCurrentUserInfo( silenceAuthErrors = false ): Promise< Me > { 25 | const api = API( { silenceAuthErrors } ); 26 | 27 | const response = await api.query< IsVipQuery, IsVipQueryVariables >( { 28 | query: QUERY_CURRENT_USER, 29 | } ); 30 | const { me } = response.data; 31 | if ( ! me ) { 32 | throw new Error( 'The API did not return any information about the user.' ); 33 | } 34 | 35 | return me; 36 | } 37 | -------------------------------------------------------------------------------- /src/lib/app-logs/app-logs.generated.d.ts: -------------------------------------------------------------------------------- 1 | import * as Types from '../../graphqlTypes'; 2 | 3 | export type GetAppLogsQueryVariables = Types.Exact< { 4 | appId?: Types.InputMaybe< Types.Scalars[ 'Int' ][ 'input' ] >; 5 | envId?: Types.InputMaybe< Types.Scalars[ 'Int' ][ 'input' ] >; 6 | type?: Types.InputMaybe< Types.AppEnvironmentLogType >; 7 | limit?: Types.InputMaybe< Types.Scalars[ 'Int' ][ 'input' ] >; 8 | after?: Types.InputMaybe< Types.Scalars[ 'String' ][ 'input' ] >; 9 | } >; 10 | 11 | export type GetAppLogsQuery = { 12 | __typename?: 'Query'; 13 | app?: { 14 | __typename?: 'App'; 15 | environments?: Array< { 16 | __typename?: 'AppEnvironment'; 17 | id?: number | null; 18 | logs?: { 19 | __typename?: 'AppEnvironmentLogsList'; 20 | nextCursor?: string | null; 21 | pollingDelaySeconds: number; 22 | nodes?: Array< { 23 | __typename?: 'AppEnvironmentLog'; 24 | timestamp?: string | null; 25 | message?: string | null; 26 | } | null > | null; 27 | } | null; 28 | } | null > | null; 29 | } | null; 30 | }; 31 | -------------------------------------------------------------------------------- /src/lib/app-logs/app-logs.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | import { GetAppLogsQueryVariables } from './app-logs.generated'; 4 | import { AppEnvironmentLogType, Query } from '../../graphqlTypes'; 5 | import API from '../../lib/api'; 6 | 7 | export const LIMIT_MAX = 5000; 8 | 9 | const QUERY_ENVIRONMENT_LOGS = gql` 10 | query GetAppLogs( 11 | $appId: Int 12 | $envId: Int 13 | $type: AppEnvironmentLogType 14 | $limit: Int 15 | $after: String 16 | ) { 17 | app(id: $appId) { 18 | environments(id: $envId) { 19 | id 20 | logs(type: $type, limit: $limit, after: $after) { 21 | nodes { 22 | timestamp 23 | message 24 | } 25 | nextCursor 26 | pollingDelaySeconds 27 | } 28 | } 29 | } 30 | } 31 | `; 32 | 33 | interface GetRecentLogsResponse { 34 | nodes: { timestamp: string; message: string }[]; 35 | nextCursor: string; 36 | pollingDelaySeconds: number; 37 | } 38 | 39 | export async function getRecentLogs( 40 | appId: number, 41 | envId: number, 42 | type: AppEnvironmentLogType, 43 | limit: number, 44 | after?: string 45 | ): Promise< GetRecentLogsResponse > { 46 | const api = API( { exitOnError: false } ); 47 | 48 | const response = await api.query< Query, GetAppLogsQueryVariables >( { 49 | query: QUERY_ENVIRONMENT_LOGS, 50 | variables: { 51 | appId, 52 | envId, 53 | type, 54 | limit, 55 | after, 56 | }, 57 | } ); 58 | 59 | const logs = response.data.app?.environments?.[ 0 ]?.logs; 60 | 61 | if ( ! logs?.nodes ) { 62 | throw new Error( 'Unable to query logs' ); 63 | } 64 | 65 | return logs as GetRecentLogsResponse; 66 | } 67 | -------------------------------------------------------------------------------- /src/lib/app-slowlogs/app-slowlogs.generated.d.ts: -------------------------------------------------------------------------------- 1 | import * as Types from '../../graphqlTypes'; 2 | 3 | export type GetAppLogsQueryVariables = Types.Exact< { 4 | appId?: Types.InputMaybe< Types.Scalars[ 'Int' ][ 'input' ] >; 5 | envId?: Types.InputMaybe< Types.Scalars[ 'Int' ][ 'input' ] >; 6 | limit?: Types.InputMaybe< Types.Scalars[ 'Int' ][ 'input' ] >; 7 | after?: Types.InputMaybe< Types.Scalars[ 'String' ][ 'input' ] >; 8 | } >; 9 | 10 | export type GetAppLogsQuery = { 11 | __typename?: 'Query'; 12 | app?: { 13 | __typename?: 'App'; 14 | environments?: Array< { 15 | __typename?: 'AppEnvironment'; 16 | id?: number | null; 17 | slowlogs?: { 18 | __typename?: 'AppEnvironmentSlowlogsList'; 19 | nextCursor?: string | null; 20 | pollingDelaySeconds: number; 21 | nodes?: Array< { 22 | __typename?: 'AppEnvironmentSlowlog'; 23 | timestamp?: string | null; 24 | rowsSent?: string | null; 25 | rowsExamined?: string | null; 26 | queryTime?: string | null; 27 | requestUri?: string | null; 28 | query?: string | null; 29 | } | null > | null; 30 | } | null; 31 | } | null > | null; 32 | } | null; 33 | }; 34 | -------------------------------------------------------------------------------- /src/lib/app-slowlogs/app-slowlogs.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | import API from '../../lib/api'; 4 | 5 | import type { GetAppLogsQueryVariables } from './app-slowlogs.generated'; 6 | import type { GetRecentSlowlogsResponse } from './types'; 7 | import type { Query } from '../../graphqlTypes'; 8 | 9 | export const LIMIT_MAX = 5000; 10 | 11 | const QUERY_ENVIRONMENT_SLOWLOGS = gql` 12 | query GetAppLogs($appId: Int, $envId: Int, $limit: Int, $after: String) { 13 | app(id: $appId) { 14 | environments(id: $envId) { 15 | id 16 | slowlogs(limit: $limit, after: $after) { 17 | nodes { 18 | timestamp 19 | rowsSent 20 | rowsExamined 21 | queryTime 22 | requestUri 23 | query 24 | } 25 | nextCursor 26 | pollingDelaySeconds 27 | } 28 | } 29 | } 30 | } 31 | `; 32 | 33 | export async function getRecentSlowlogs( 34 | appId: number, 35 | envId: number, 36 | limit: number, 37 | after?: string | null 38 | ): Promise< GetRecentSlowlogsResponse > { 39 | const api = API( { exitOnError: false } ); 40 | 41 | const response = await api.query< Query, GetAppLogsQueryVariables >( { 42 | query: QUERY_ENVIRONMENT_SLOWLOGS, 43 | variables: { 44 | appId, 45 | envId, 46 | limit, 47 | after, 48 | }, 49 | } ); 50 | 51 | const slowlogs = response.data.app?.environments?.[ 0 ]?.slowlogs; 52 | 53 | if ( ! slowlogs?.nodes ) { 54 | throw new Error( 'Unable to query slowlogs' ); 55 | } 56 | 57 | return slowlogs as GetRecentSlowlogsResponse; 58 | } 59 | -------------------------------------------------------------------------------- /src/lib/app-slowlogs/types.ts: -------------------------------------------------------------------------------- 1 | export interface DefaultOptions { 2 | app: { 3 | id: number; 4 | organization: { 5 | id: number; 6 | name: string; 7 | }; 8 | }; 9 | env: { 10 | id: number; 11 | }; 12 | } 13 | 14 | export type SlowlogFormats = 'json' | 'csv' | 'table'; 15 | 16 | type Stringable = string | { toString: () => string }; 17 | 18 | export interface GetSlowLogsOptions extends DefaultOptions { 19 | limit: number; 20 | format: 'table' | 'json' | 'csv'; 21 | } 22 | 23 | export interface GetRecentSlowlogsResponse { 24 | nodes: Slowlog[]; 25 | nextCursor: string; 26 | pollingDelaySeconds: number; 27 | } 28 | 29 | export interface GetBaseTrackingParamsOptions extends DefaultOptions { 30 | limit: number; 31 | format: string; 32 | follow?: boolean; 33 | } 34 | 35 | export interface BaseTrackingParams extends Record< string, unknown > { 36 | command: string; 37 | org_id: number; 38 | app_id: number; 39 | env_id: number; 40 | limit: number; 41 | follow?: boolean; 42 | format: string; 43 | } 44 | 45 | export interface Slowlog extends Record< string, Stringable > { 46 | timestamp: string; 47 | rowsSent: string; 48 | rowsExamined: string; 49 | queryTime: string; 50 | requestUri: string; 51 | query: string; 52 | } 53 | -------------------------------------------------------------------------------- /src/lib/app.ts: -------------------------------------------------------------------------------- 1 | import { NODEJS_SITE_TYPE_IDS, WORDPRESS_SITE_TYPE_IDS } from './constants/vipgo'; 2 | 3 | /** 4 | * Is this a WordPress application? 5 | * 6 | * @param {number} appTypeId application type ID 7 | * @return {boolean} Whether this a WordPress application 8 | */ 9 | export function isAppWordPress( appTypeId: number ): boolean { 10 | return WORDPRESS_SITE_TYPE_IDS.includes( appTypeId ); 11 | } 12 | 13 | /** 14 | * Is this a Nodejs application? 15 | * 16 | * @param {number} appTypeId application type ID 17 | * @return {boolean} Whether this a Node.js application 18 | */ 19 | export function isAppNodejs( appTypeId: number ): boolean { 20 | return NODEJS_SITE_TYPE_IDS.includes( appTypeId ); 21 | } 22 | -------------------------------------------------------------------------------- /src/lib/backup-storage-availability/docker-machine-not-found-error.ts: -------------------------------------------------------------------------------- 1 | export class DockerMachineNotFoundError extends Error { 2 | constructor() { 3 | super( 'Docker machine not found' ); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/lib/cli/apiConfig.ts: -------------------------------------------------------------------------------- 1 | import * as exit from './exit'; 2 | import * as featureFlags from '../../lib/api/feature-flags'; 3 | import Token from '../../lib/token'; 4 | import { trackEvent } from '../../lib/tracker'; 5 | 6 | export async function checkFeatureEnabled( 7 | featureName: string, 8 | exitOnFalse: boolean = false 9 | ): Promise< boolean > { 10 | // TODO: eventually let's look at more feature flags coming from the public api, 11 | // for now, let's see if the user of the CLI is VIP 12 | await trackEvent( 'checkFeatureEnabled_start', { featureName, exitOnFalse } ); 13 | 14 | let isVIP; 15 | try { 16 | const res = await featureFlags.get(); 17 | if ( res?.data.me?.isVIP !== undefined ) { 18 | isVIP = res.data.me.isVIP; 19 | } else { 20 | isVIP = false; 21 | } 22 | } catch ( err ) { 23 | const message = ( err as Error ).toString(); 24 | await trackEvent( 'checkFeatureEnabled_fetch_error', { 25 | featureName, 26 | exitOnFalse, 27 | error: message, 28 | } ); 29 | 30 | exit.withError( `Failed to determine if feature is enabled: ${ message }` ); 31 | } 32 | 33 | if ( exitOnFalse && isVIP === false ) { 34 | exit.withError( 'The feature you are attempting to use is not currently enabled.' ); 35 | } 36 | 37 | return isVIP === true; 38 | } 39 | 40 | // Because this function is called by trackEvent: 41 | // - It cannot directly or indirectly call trackEvent, or it will cause a loop. 42 | // - It is mocked globally in jest.setupMocks.js. 43 | export async function checkIfUserIsVip() { 44 | const token = await Token.get(); 45 | 46 | if ( token.valid() ) { 47 | const res = await featureFlags.get(); 48 | 49 | return Boolean( res?.data.me?.isVIP ); 50 | } 51 | 52 | return false; 53 | } 54 | 55 | export async function exitWhenFeatureDisabled( featureName: string ): Promise< boolean > { 56 | return checkFeatureEnabled( featureName, true ); 57 | } 58 | -------------------------------------------------------------------------------- /src/lib/cli/config.ts: -------------------------------------------------------------------------------- 1 | import debugLib from 'debug'; 2 | 3 | interface Config { 4 | tracksUserType: string; 5 | tracksAnonUserType: string; 6 | tracksEventPrefix: string; 7 | environment: string; 8 | } 9 | 10 | const debug = debugLib( '@automattic/vip:lib:cli:config' ); 11 | 12 | let configFromFile: Config; 13 | try { 14 | // Get `local` config first; this will only exist in dev as it's npmignore-d. 15 | // eslint-disable-next-line @typescript-eslint/no-var-requires 16 | configFromFile = require( '../../../config/config.local.json' ) as Config; 17 | 18 | debug( 'Loaded config data from config.local.json' ); 19 | } catch { 20 | // Fall back to `publish` config file. 21 | // eslint-disable-next-line @typescript-eslint/no-var-requires 22 | configFromFile = require( '../../../config/config.publish.json' ) as Config; 23 | 24 | debug( 'Loaded config data from config.publish.json' ); 25 | } 26 | 27 | export default configFromFile; 28 | -------------------------------------------------------------------------------- /src/lib/cli/envAlias.ts: -------------------------------------------------------------------------------- 1 | export function isAlias( alias: string ): boolean { 2 | return /^@[A-Za-z0-9._-]+$/.test( alias ); 3 | } 4 | 5 | export function parseEnvAlias( alias: string ) { 6 | if ( ! isAlias( alias ) ) { 7 | throw new Error( 8 | 'Invalid environment alias. Aliases are in the format of @app-name or @app-name.environment-name' 9 | ); 10 | } 11 | 12 | // Remove the '@' 13 | const stripped = alias.slice( 1 ).toLowerCase(); 14 | 15 | // in JS, .split() with a limit discards the extra ones, so can't use it 16 | // Also convert to lowercase because mixed case environment names would cause problems 17 | const [ app, ...rest ] = stripped.split( '.' ); 18 | 19 | let env; 20 | 21 | // Rejoin the env on '.' (if present), to handle instance names (env.instance-01) 22 | if ( rest.length ) { 23 | env = rest.join( '.' ); 24 | } 25 | 26 | return { app, env }; 27 | } 28 | 29 | interface ParsedAlias { 30 | argv: string[]; 31 | app?: string; 32 | env?: string; 33 | } 34 | 35 | export function parseEnvAliasFromArgv( processArgv: string[] ): ParsedAlias { 36 | // Clone to not affect original arvg 37 | const argv = processArgv.slice( 0 ); 38 | 39 | // If command included a `--` to indicate end of named args, lets only consider aliases 40 | // _before_ it, so that it can be passed to other commands directly 41 | const dashDashIndex = argv.indexOf( '--' ); 42 | 43 | let argsBeforeDashDash = argv; 44 | 45 | if ( dashDashIndex > -1 ) { 46 | argsBeforeDashDash = argv.slice( 0, dashDashIndex ); 47 | } 48 | 49 | const alias = argsBeforeDashDash.find( arg => isAlias( arg ) ); 50 | 51 | if ( ! alias ) { 52 | return { argv }; 53 | } 54 | 55 | // If we did have an alias, split it up into app/env 56 | const parsedAlias = parseEnvAlias( alias ); 57 | 58 | // Splice out the alias 59 | argv.splice( argv.indexOf( alias ), 1 ); 60 | 61 | return { argv, ...parsedAlias }; 62 | } 63 | -------------------------------------------------------------------------------- /src/lib/cli/exit.ts: -------------------------------------------------------------------------------- 1 | import { red, yellow } from 'chalk'; 2 | import debug from 'debug'; 3 | 4 | import env from '../../lib/env'; 5 | 6 | export function withError( message: Error | string ): never { 7 | const msg = message instanceof Error ? message.message : message; 8 | console.error( `${ red( 'Error: ' ) } ${ msg.replace( /^Error:\s*/, '' ) }` ); 9 | 10 | // Debug ouput is printed below error output both for information 11 | // hierarchy and to make it more likely that the user copies it to their 12 | // clipboard when dragging across output. 13 | console.log( 14 | `${ yellow( 'Debug: ' ) } VIP-CLI v${ env.app.version }, Node ${ env.node.version }, ${ 15 | env.os.name 16 | } ${ env.os.version } ${ env.os.arch }` 17 | ); 18 | 19 | if ( debug.names.length > 0 && message instanceof Error ) { 20 | console.error( yellow( 'Debug: ' ), message ); 21 | } 22 | 23 | process.exit( 1 ); 24 | } 25 | -------------------------------------------------------------------------------- /src/lib/cli/prompt.ts: -------------------------------------------------------------------------------- 1 | import { prompt } from 'enquirer'; 2 | 3 | import { keyValue, type Tuple } from './format'; 4 | 5 | interface Answer { 6 | confirm: boolean; 7 | } 8 | 9 | export async function confirm( 10 | values: Tuple[], 11 | message: string, 12 | skipPrompt: boolean = false 13 | ): Promise< boolean > { 14 | console.log( keyValue( values ) ); 15 | 16 | if ( ! skipPrompt ) { 17 | const answer = await prompt< Answer >( { 18 | type: 'confirm', 19 | name: 'confirm', 20 | message, 21 | } ); 22 | 23 | return answer.confirm; 24 | } 25 | 26 | return true; 27 | } 28 | -------------------------------------------------------------------------------- /src/lib/constants/dev-environment.ts: -------------------------------------------------------------------------------- 1 | export const DEV_ENVIRONMENT_SUBCOMMAND = 'dev-env'; 2 | export const DEV_ENVIRONMENT_FULL_COMMAND = `vip ${ DEV_ENVIRONMENT_SUBCOMMAND }`; 3 | 4 | export const DEV_ENVIRONMENT_PROMPT_INTRO = 5 | 'This is a wizard to help you set up your local dev environment.\n\n' + 6 | 'Sensible default values were pre-selected for convenience. ' + 7 | 'You may also choose to create multiple environments with different settings using the --slug option.\n\n'; 8 | export const DEV_ENVIRONMENT_NOT_FOUND = 'Environment not found.'; 9 | 10 | export const DEV_ENVIRONMENT_COMPONENTS = [ 'appCode', 'muPlugins' ] as const; 11 | export const DEV_ENVIRONMENT_COMPONENTS_WITH_WP = [ 12 | 'wordpress', 13 | ...DEV_ENVIRONMENT_COMPONENTS, 14 | ] as const; 15 | 16 | export const DEV_ENVIRONMENT_RAW_GITHUB_HOST = 'raw.githubusercontent.com'; 17 | 18 | export const DEV_ENVIRONMENT_WORDPRESS_VERSIONS_URI = 19 | '/Automattic/vip-container-images/master/wordpress/versions.json'; 20 | 21 | export const DEV_ENVIRONMENT_WORDPRESS_CACHE_KEY = 'wordpress-versions.json'; 22 | 23 | export const DEV_ENVIRONMENT_WORDPRESS_VERSION_TTL = 86400; // once per day 24 | 25 | interface PhpImage { 26 | image: string; 27 | label: string; 28 | } 29 | 30 | export const DEV_ENVIRONMENT_PHP_VERSIONS: Record< string, PhpImage > = { 31 | 8.2: { 32 | image: 'ghcr.io/automattic/vip-container-images/php-fpm:8.2', 33 | label: '8.2 (recommended)', 34 | }, 35 | 8.1: { image: 'ghcr.io/automattic/vip-container-images/php-fpm:8.1', label: '8.1' }, 36 | 8.3: { 37 | image: 'ghcr.io/automattic/vip-container-images/php-fpm:8.3', 38 | label: '8.3', 39 | }, 40 | 8.4: { 41 | image: 'ghcr.io/automattic/vip-container-images/php-fpm:8.4', 42 | label: '8.4 (experimental)', 43 | }, 44 | } as const; 45 | 46 | export const DEV_ENVIRONMENT_DEFAULTS = { 47 | title: 'VIP Dev', 48 | multisite: false, 49 | phpVersion: Object.keys( DEV_ENVIRONMENT_PHP_VERSIONS )[ 0 ], 50 | } as const; 51 | 52 | export const DEV_ENVIRONMENT_VERSION = '2.3.0'; 53 | -------------------------------------------------------------------------------- /src/lib/constants/file-size.ts: -------------------------------------------------------------------------------- 1 | export const KB_IN_BYTES = 1024; 2 | export const MB_IN_BYTES = 1024 * KB_IN_BYTES; 3 | export const GB_IN_BYTES = 1024 * MB_IN_BYTES; 4 | export const TB_IN_BYTES = 1024 * GB_IN_BYTES; 5 | -------------------------------------------------------------------------------- /src/lib/constants/vipgo.ts: -------------------------------------------------------------------------------- 1 | export const WORDPRESS_APPLICATION_TYPE_ID = 2; 2 | export const WORDPRESS_NON_PROD_APPLICATION_TYPE_ID = 6; 3 | export const WORDPRESS_SITE_TYPE_IDS = [ 4 | WORDPRESS_APPLICATION_TYPE_ID, 5 | WORDPRESS_NON_PROD_APPLICATION_TYPE_ID, 6 | ]; 7 | 8 | export const NODEJS_APPLICATION_TYPE_ID = 3; 9 | export const NODEJS_MYSQL_APPLICATION_TYPE_ID = 5; 10 | export const NODEJS_REDIS_APPLICATION_TYPE_ID = 7; 11 | export const NODEJS_MYSQL_REDIS_APPLICATION_TYPE_ID = 8; 12 | export const NODEJS_SITE_TYPE_IDS = [ 13 | NODEJS_APPLICATION_TYPE_ID, 14 | NODEJS_MYSQL_APPLICATION_TYPE_ID, 15 | NODEJS_REDIS_APPLICATION_TYPE_ID, 16 | NODEJS_MYSQL_REDIS_APPLICATION_TYPE_ID, 17 | ]; 18 | 19 | export const DATABASE_APPLICATION_TYPE_IDS = [ 20 | WORDPRESS_APPLICATION_TYPE_ID, 21 | WORDPRESS_NON_PROD_APPLICATION_TYPE_ID, 22 | NODEJS_MYSQL_APPLICATION_TYPE_ID, 23 | NODEJS_MYSQL_REDIS_APPLICATION_TYPE_ID, 24 | ]; 25 | -------------------------------------------------------------------------------- /src/lib/custom-deploy/custom-deploy.generated.d.ts: -------------------------------------------------------------------------------- 1 | import * as Types from '../../graphqlTypes'; 2 | 3 | export type ValidateCustomDeployAccessMutationVariables = Types.Exact< { [ key: string ]: never } >; 4 | 5 | export type ValidateCustomDeployAccessMutation = { 6 | __typename?: 'Mutation'; 7 | validateCustomDeployAccess?: { 8 | __typename?: 'ValidateCustomDeployAccessPayload'; 9 | success?: boolean | null; 10 | appId?: number | null; 11 | envId?: number | null; 12 | envType?: string | null; 13 | envUniqueLabel?: string | null; 14 | primaryDomainName?: string | null; 15 | launched?: boolean | null; 16 | } | null; 17 | }; 18 | -------------------------------------------------------------------------------- /src/lib/env.ts: -------------------------------------------------------------------------------- 1 | import { arch, platform, release } from 'node:os'; 2 | 3 | import pkg from '../../package.json'; 4 | 5 | interface AppInfo { 6 | name: string; 7 | version: string; 8 | } 9 | 10 | interface OSInfo { 11 | name: string; 12 | version: string; 13 | arch: string; 14 | } 15 | 16 | interface NodeInfo { 17 | version: string; 18 | } 19 | 20 | export interface Env { 21 | app: AppInfo; 22 | os: OSInfo; 23 | node: NodeInfo; 24 | userAgent: string; 25 | } 26 | 27 | const app: AppInfo = { 28 | name: pkg.name, 29 | version: pkg.version, 30 | }; 31 | 32 | const os: OSInfo = { 33 | name: platform(), 34 | version: release(), 35 | arch: arch(), 36 | }; 37 | 38 | const node: NodeInfo = { 39 | version: process.version, 40 | }; 41 | 42 | const env: Env = { 43 | app, 44 | os, 45 | node, 46 | userAgent: `vip-cli/${ app.version } (node/${ node.version }; ${ os.name }/${ os.version }; +https://wpvip.com)`, 47 | }; 48 | 49 | export default env; 50 | -------------------------------------------------------------------------------- /src/lib/envvar/api-delete.generated.d.ts: -------------------------------------------------------------------------------- 1 | import * as Types from '../../graphqlTypes'; 2 | 3 | export type DeleteEnvironmentVariableMutationVariables = Types.Exact< { 4 | appId: Types.Scalars[ 'Int' ][ 'input' ]; 5 | envId: Types.Scalars[ 'Int' ][ 'input' ]; 6 | name: Types.Scalars[ 'String' ][ 'input' ]; 7 | } >; 8 | 9 | export type DeleteEnvironmentVariableMutation = { 10 | __typename?: 'Mutation'; 11 | deleteEnvironmentVariable?: { 12 | __typename?: 'EnvironmentVariablesPayload'; 13 | environmentVariables?: { 14 | __typename?: 'EnvironmentVariablesList'; 15 | total?: any | null; 16 | nodes?: Array< { __typename?: 'EnvironmentVariable'; name: string } | null > | null; 17 | } | null; 18 | } | null; 19 | }; 20 | -------------------------------------------------------------------------------- /src/lib/envvar/api-delete.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | import API from '../../lib/api'; 4 | 5 | import type { 6 | DeleteEnvironmentVariableMutation, 7 | DeleteEnvironmentVariableMutationVariables, 8 | } from './api-delete.generated'; 9 | import type { FetchResult } from '@apollo/client'; 10 | 11 | const mutation = gql` 12 | mutation DeleteEnvironmentVariable($appId: Int!, $envId: Int!, $name: String!) { 13 | deleteEnvironmentVariable( 14 | input: { applicationId: $appId, environmentId: $envId, name: $name, value: "" } 15 | ) { 16 | environmentVariables { 17 | total 18 | nodes { 19 | name 20 | } 21 | } 22 | } 23 | } 24 | `; 25 | 26 | export default async function deleteEnvVar( 27 | appId: number, 28 | envId: number, 29 | name: string 30 | ): Promise< FetchResult< DeleteEnvironmentVariableMutation > > { 31 | const api = API(); 32 | 33 | const variables = { 34 | appId, 35 | envId, 36 | name, 37 | }; 38 | 39 | return api.mutate< 40 | DeleteEnvironmentVariableMutation, 41 | DeleteEnvironmentVariableMutationVariables 42 | >( { mutation, variables } ); 43 | } 44 | -------------------------------------------------------------------------------- /src/lib/envvar/api-get-all.generated.d.ts: -------------------------------------------------------------------------------- 1 | import * as Types from '../../graphqlTypes'; 2 | 3 | export type GetEnvironmentVariablesWithValuesQueryVariables = Types.Exact< { 4 | appId: Types.Scalars[ 'Int' ][ 'input' ]; 5 | envId: Types.Scalars[ 'Int' ][ 'input' ]; 6 | } >; 7 | 8 | export type GetEnvironmentVariablesWithValuesQuery = { 9 | __typename?: 'Query'; 10 | app?: { 11 | __typename?: 'App'; 12 | id?: number | null; 13 | environments?: Array< { 14 | __typename?: 'AppEnvironment'; 15 | id?: number | null; 16 | environmentVariables?: { 17 | __typename?: 'EnvironmentVariablesList'; 18 | total?: any | null; 19 | nodes?: Array< { 20 | __typename?: 'EnvironmentVariable'; 21 | name: string; 22 | value?: string | null; 23 | } | null > | null; 24 | } | null; 25 | } | null > | null; 26 | } | null; 27 | }; 28 | -------------------------------------------------------------------------------- /src/lib/envvar/api-get-all.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | import { 4 | GetEnvironmentVariablesWithValuesQuery, 5 | GetEnvironmentVariablesWithValuesQueryVariables, 6 | } from './api-get-all.generated'; 7 | import { EnvironmentVariable } from '../../graphqlTypes'; 8 | import API from '../../lib/api'; 9 | 10 | const query = gql` 11 | query GetEnvironmentVariablesWithValues($appId: Int!, $envId: Int!) { 12 | app(id: $appId) { 13 | id 14 | environments(id: $envId) { 15 | id 16 | environmentVariables { 17 | total 18 | nodes { 19 | name 20 | value 21 | } 22 | } 23 | } 24 | } 25 | } 26 | `; 27 | 28 | export default async function getEnvVars( 29 | appId: number, 30 | envId: number 31 | ): Promise< EnvironmentVariable[] | null > { 32 | const api = API(); 33 | 34 | const variables = { 35 | appId, 36 | envId, 37 | }; 38 | 39 | const { data } = await api.query< 40 | GetEnvironmentVariablesWithValuesQuery, 41 | GetEnvironmentVariablesWithValuesQueryVariables 42 | >( { query, variables } ); 43 | 44 | return ( 45 | ( data.app?.environments?.[ 0 ]?.environmentVariables?.nodes as 46 | | EnvironmentVariable[] 47 | | null 48 | | undefined ) ?? null 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /src/lib/envvar/api-get.ts: -------------------------------------------------------------------------------- 1 | import getEnvVars from '../../lib/envvar/api-get-all'; 2 | 3 | import type { EnvironmentVariable } from '../../graphqlTypes'; 4 | 5 | export default async function getEnvVar( 6 | appId: number, 7 | envId: number, 8 | name: string 9 | ): Promise< EnvironmentVariable | undefined > { 10 | const envvars = await getEnvVars( appId, envId ); 11 | return envvars?.find( ( { name: foundName } ) => name === foundName ); 12 | } 13 | -------------------------------------------------------------------------------- /src/lib/envvar/api-list.generated.d.ts: -------------------------------------------------------------------------------- 1 | import * as Types from '../../graphqlTypes'; 2 | 3 | export type GetEnvironmentVariablesQueryVariables = Types.Exact< { 4 | appId: Types.Scalars[ 'Int' ][ 'input' ]; 5 | envId: Types.Scalars[ 'Int' ][ 'input' ]; 6 | } >; 7 | 8 | export type GetEnvironmentVariablesQuery = { 9 | __typename?: 'Query'; 10 | app?: { 11 | __typename?: 'App'; 12 | id?: number | null; 13 | environments?: Array< { 14 | __typename?: 'AppEnvironment'; 15 | id?: number | null; 16 | environmentVariables?: { 17 | __typename?: 'EnvironmentVariablesList'; 18 | total?: any | null; 19 | nodes?: Array< { __typename?: 'EnvironmentVariable'; name: string } | null > | null; 20 | } | null; 21 | } | null > | null; 22 | } | null; 23 | }; 24 | -------------------------------------------------------------------------------- /src/lib/envvar/api-list.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | import API from '../../lib/api'; 4 | 5 | import type { 6 | GetEnvironmentVariablesQuery, 7 | GetEnvironmentVariablesQueryVariables, 8 | } from './api-list.generated'; 9 | 10 | const query = gql` 11 | query GetEnvironmentVariables($appId: Int!, $envId: Int!) { 12 | app(id: $appId) { 13 | id 14 | environments(id: $envId) { 15 | id 16 | environmentVariables { 17 | total 18 | nodes { 19 | name 20 | } 21 | } 22 | } 23 | } 24 | } 25 | `; 26 | 27 | // List the names (but not values) of environment variables. 28 | export default async function listEnvVars( appId: number, envId: number ): Promise< string[] > { 29 | const api = API(); 30 | 31 | const variables = { 32 | appId, 33 | envId, 34 | }; 35 | 36 | const { data } = await api.query< 37 | GetEnvironmentVariablesQuery, 38 | GetEnvironmentVariablesQueryVariables 39 | >( { query, variables } ); 40 | 41 | const nodes = data.app?.environments?.[ 0 ]?.environmentVariables?.nodes ?? []; 42 | 43 | return nodes.map( entry => entry?.name ?? '' ); 44 | } 45 | -------------------------------------------------------------------------------- /src/lib/envvar/api-set.generated.d.ts: -------------------------------------------------------------------------------- 1 | import * as Types from '../../graphqlTypes'; 2 | 3 | export type AddEnvironmentVariableMutationVariables = Types.Exact< { 4 | appId: Types.Scalars[ 'Int' ][ 'input' ]; 5 | envId: Types.Scalars[ 'Int' ][ 'input' ]; 6 | name: Types.Scalars[ 'String' ][ 'input' ]; 7 | value: Types.Scalars[ 'String' ][ 'input' ]; 8 | } >; 9 | 10 | export type AddEnvironmentVariableMutation = { 11 | __typename?: 'Mutation'; 12 | addEnvironmentVariable?: { 13 | __typename?: 'EnvironmentVariablesPayload'; 14 | environmentVariables?: { 15 | __typename?: 'EnvironmentVariablesList'; 16 | total?: any | null; 17 | nodes?: Array< { __typename?: 'EnvironmentVariable'; name: string } | null > | null; 18 | } | null; 19 | } | null; 20 | }; 21 | -------------------------------------------------------------------------------- /src/lib/envvar/api-set.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | import API from '../../lib/api'; 4 | 5 | import type { 6 | AddEnvironmentVariableMutation, 7 | AddEnvironmentVariableMutationVariables, 8 | } from './api-set.generated'; 9 | import type { FetchResult } from '@apollo/client'; 10 | 11 | const mutation = gql` 12 | mutation AddEnvironmentVariable($appId: Int!, $envId: Int!, $name: String!, $value: String!) { 13 | addEnvironmentVariable( 14 | input: { applicationId: $appId, environmentId: $envId, name: $name, value: $value } 15 | ) { 16 | environmentVariables { 17 | total 18 | nodes { 19 | name 20 | } 21 | } 22 | } 23 | } 24 | `; 25 | 26 | export default async function setEnvVar( 27 | appId: number, 28 | envId: number, 29 | name: string, 30 | value: string 31 | ): Promise< FetchResult< AddEnvironmentVariableMutation > > { 32 | const api = API(); 33 | 34 | const variables = { 35 | appId, 36 | envId, 37 | name, 38 | value, 39 | }; 40 | 41 | return api.mutate< AddEnvironmentVariableMutation, AddEnvironmentVariableMutationVariables >( { 42 | mutation, 43 | variables, 44 | } ); 45 | } 46 | -------------------------------------------------------------------------------- /src/lib/envvar/api.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | import deleteEnvVar from './api-delete'; 4 | import getEnvVar from './api-get'; 5 | import getEnvVars from './api-get-all'; 6 | import listEnvVars from './api-list'; 7 | import setEnvVar from './api-set'; 8 | import { debug } from '../../lib/envvar/logging'; 9 | 10 | // Reexport for convenience 11 | export { deleteEnvVar, getEnvVar, getEnvVars, listEnvVars, setEnvVar }; 12 | 13 | // The subquery for environments lets users choose any environment, including production. 14 | export const appQuery = ` 15 | id 16 | name 17 | environments { 18 | id 19 | appId 20 | name 21 | primaryDomain { 22 | name 23 | } 24 | type 25 | } 26 | organization { 27 | id 28 | name 29 | } 30 | `; 31 | 32 | export function validateName( name: string ): boolean { 33 | const sanitizedName = name 34 | .trim() 35 | .toUpperCase() 36 | .replace( /[^A-Z0-9_]/g, '' ); 37 | return name === sanitizedName && /^[A-Z]/.test( sanitizedName ); 38 | } 39 | 40 | export function validateNameWithMessage( name: string ): boolean { 41 | debug( `Validating environment variable name "${ name }"` ); 42 | 43 | if ( ! validateName( name ) ) { 44 | const message = [ 45 | 'Environment variable name must consist of A-Z, 0-9, or _,', 46 | 'and must start with an uppercase letter.', 47 | ].join( '\n' ); 48 | 49 | console.log( chalk.bold.red( message ) ); 50 | return false; 51 | } 52 | 53 | return true; 54 | } 55 | -------------------------------------------------------------------------------- /src/lib/envvar/input.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { Confirm, prompt } from 'enquirer'; 3 | 4 | export function cancel(): void { 5 | console.log( chalk.yellow( 'Command cancelled by user.' ) ); 6 | process.exit(); 7 | } 8 | 9 | export function confirm( message: string ): Promise< boolean > { 10 | return new Confirm( { message } ).run().catch( () => false ); 11 | } 12 | 13 | interface Answer { 14 | // FIXME: can it really be undefined? 15 | str?: string; 16 | } 17 | 18 | export async function promptForValue( message: string, mustMatch?: string ): Promise< string > { 19 | const { str } = await prompt< Answer >( { 20 | message, 21 | name: 'str', 22 | type: 'input', 23 | validate: ( input: string ) => { 24 | if ( mustMatch && input !== mustMatch ) { 25 | return `Please type ${ mustMatch } to proceed or ESC to cancel`; 26 | } 27 | 28 | return true; 29 | }, 30 | } ); 31 | 32 | return str?.trim() ?? ''; 33 | } 34 | -------------------------------------------------------------------------------- /src/lib/envvar/logging.ts: -------------------------------------------------------------------------------- 1 | import debugLib from 'debug'; 2 | 3 | import { getEnvIdentifier } from '../../lib/cli/command'; 4 | 5 | // Shared debugger. 6 | export const debug = debugLib( '@automattic/vip:bin:config:envvar' ); 7 | 8 | // FIXME: Replace with a proper type 9 | interface App { 10 | id: number; 11 | } 12 | 13 | export function getEnvContext( app: App, env: string ): string { 14 | return `@${ app.id }.${ getEnvIdentifier( env ) }`; 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/envvar/read-file.ts: -------------------------------------------------------------------------------- 1 | import { debug } from '../../lib/envvar/logging'; 2 | import { readFromFile } from '../read-file'; 3 | 4 | export function readVariableFromFile( path: string ): Promise< string > { 5 | debug( `Loading variable value from file "${ path }"` ); 6 | 7 | return readFromFile( path ); 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/keychain.ts: -------------------------------------------------------------------------------- 1 | import debugLib from 'debug'; 2 | 3 | import Insecure from './keychain/insecure'; 4 | 5 | import type { Keychain, KeychainConstructor } from './keychain/keychain'; 6 | 7 | let exportValue: Keychain; 8 | const debug = debugLib( '@automattic/vip:keychain' ); 9 | 10 | try { 11 | // Try using Secure keychain ("keytar") first 12 | // eslint-disable-next-line @typescript-eslint/no-var-requires 13 | const Secure = require( './keychain/secure' ) as KeychainConstructor; 14 | exportValue = new Secure(); 15 | } catch ( error ) { 16 | debug( 'Cannot use Secure keychain; falling back to Insecure keychain (Details: %o)', error ); 17 | 18 | // Fallback to Insecure keychain if we can't 19 | exportValue = new Insecure( 'vip-go-cli' ); 20 | } 21 | 22 | export default exportValue; 23 | -------------------------------------------------------------------------------- /src/lib/keychain/insecure.ts: -------------------------------------------------------------------------------- 1 | import Configstore from 'configstore'; 2 | 3 | import type { Keychain } from './keychain'; 4 | 5 | export default class Insecure implements Keychain { 6 | private file: string; 7 | private configstore: Configstore; 8 | 9 | constructor( file: string ) { 10 | this.file = file; 11 | 12 | this.configstore = new Configstore( this.file ); 13 | } 14 | 15 | public getPassword( service: string ): Promise< string | null > { 16 | try { 17 | const value: unknown = this.configstore.get( service ); 18 | if ( null === value || undefined === value ) { 19 | return Promise.resolve( null ); 20 | } 21 | 22 | // eslint-disable-next-line @typescript-eslint/no-base-to-string 23 | return Promise.resolve( value.toString() ); // NOSONAR 24 | } catch ( err ) { 25 | return Promise.reject( err as Error ); 26 | } 27 | } 28 | 29 | public setPassword( service: string, password: string ): Promise< boolean > { 30 | try { 31 | this.configstore.set( service, password ); 32 | return Promise.resolve( true ); 33 | } catch ( err ) { 34 | return Promise.reject( err as Error ); 35 | } 36 | } 37 | 38 | public deletePassword( service: string ): Promise< boolean > { 39 | try { 40 | this.configstore.delete( service ); 41 | return Promise.resolve( true ); 42 | } catch ( err ) { 43 | return Promise.reject( err as Error ); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/lib/keychain/keychain.ts: -------------------------------------------------------------------------------- 1 | export interface Keychain { 2 | getPassword( service: string ): Promise< string | null >; 3 | setPassword( service: string, password: string ): Promise< boolean >; 4 | deletePassword( service: string ): Promise< boolean >; 5 | } 6 | 7 | export type KeychainConstructor = new () => Keychain; 8 | -------------------------------------------------------------------------------- /src/lib/keychain/secure.ts: -------------------------------------------------------------------------------- 1 | import keytar from '@postman/node-keytar'; 2 | 3 | import type { Keychain } from './keychain'; 4 | 5 | export default class Secure implements Keychain { 6 | public getPassword( service: string ): Promise< string | null > { 7 | return keytar.getPassword( service, service ); 8 | } 9 | 10 | public async setPassword( service: string, password: string ): Promise< boolean > { 11 | await keytar.setPassword( service, service, password ); 12 | return true; 13 | } 14 | 15 | public deletePassword( service: string ): Promise< boolean > { 16 | return keytar.deletePassword( service, service ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/lib/logout.ts: -------------------------------------------------------------------------------- 1 | import http from '../lib/api/http'; 2 | import Token from '../lib/token'; 3 | import { trackEvent } from '../lib/tracker'; 4 | 5 | export default async (): Promise< void > => { 6 | await http( '/logout', { method: 'post' } ); 7 | 8 | await Token.purge(); 9 | 10 | await trackEvent( 'logout_command_execute' ); 11 | }; 12 | -------------------------------------------------------------------------------- /src/lib/media-import/config.generated.d.ts: -------------------------------------------------------------------------------- 1 | import * as Types from '../../graphqlTypes'; 2 | 3 | export type MediaImportConfigQueryVariables = Types.Exact< { [ key: string ]: never } >; 4 | 5 | export type MediaImportConfigQuery = { 6 | __typename?: 'Query'; 7 | mediaImportConfig?: { 8 | __typename?: 'MediaImportConfig'; 9 | fileNameCharCount?: number | null; 10 | fileSizeLimitInBytes?: any | null; 11 | allowedFileTypes?: any | null; 12 | } | null; 13 | }; 14 | -------------------------------------------------------------------------------- /src/lib/media-import/config.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | import API from '../api'; 4 | 5 | import type { MediaImportConfigQuery } from './config.generated'; 6 | import type { MediaImportConfig } from '../../graphqlTypes'; 7 | 8 | const IMPORT_MEDIA_CONFIG_QUERY = gql` 9 | query MediaImportConfig { 10 | mediaImportConfig { 11 | fileNameCharCount 12 | fileSizeLimitInBytes 13 | allowedFileTypes 14 | } 15 | } 16 | `; 17 | 18 | export async function getMediaImportConfig(): Promise< MediaImportConfig | null > { 19 | const api = API(); 20 | 21 | const response = await api.query< MediaImportConfigQuery >( { 22 | query: IMPORT_MEDIA_CONFIG_QUERY, 23 | variables: {}, 24 | fetchPolicy: 'network-only', 25 | } ); 26 | 27 | return response?.data?.mediaImportConfig as unknown as MediaImportConfig | null; 28 | } 29 | -------------------------------------------------------------------------------- /src/lib/media-import/media-file-import.ts: -------------------------------------------------------------------------------- 1 | import { App } from '../../graphqlTypes'; 2 | import { GB_IN_BYTES } from '../../lib/constants/file-size'; 3 | 4 | export const MEDIA_IMPORT_FILE_SIZE_LIMIT = 30 * GB_IN_BYTES; 5 | 6 | export type AppForMediaImport = Pick< 7 | App, 8 | 'id' | 'environments' | 'name' | 'organization' | 'type' 9 | >; 10 | 11 | export function currentUserCanImportForApp( app: AppForMediaImport ): boolean { 12 | // TODO: implement 13 | return Boolean( app ); 14 | } 15 | 16 | export const SUPPORTED_MEDIA_FILE_IMPORT_SITE_TYPES = [ 'WordPress' ]; 17 | 18 | export const isSupportedApp = ( { type }: AppForMediaImport ) => 19 | SUPPORTED_MEDIA_FILE_IMPORT_SITE_TYPES.includes( type as string ); 20 | -------------------------------------------------------------------------------- /src/lib/media-import/status.generated.d.ts: -------------------------------------------------------------------------------- 1 | import * as Types from '../../graphqlTypes'; 2 | 3 | export type AppQueryVariables = Types.Exact< { 4 | appId?: Types.InputMaybe< Types.Scalars[ 'Int' ][ 'input' ] >; 5 | envId?: Types.InputMaybe< Types.Scalars[ 'Int' ][ 'input' ] >; 6 | } >; 7 | 8 | export type AppQuery = { 9 | __typename?: 'Query'; 10 | app?: { 11 | __typename?: 'App'; 12 | environments?: Array< { 13 | __typename?: 'AppEnvironment'; 14 | id?: number | null; 15 | name?: string | null; 16 | type?: string | null; 17 | repo?: string | null; 18 | mediaImportStatus?: { 19 | __typename?: 'AppEnvironmentMediaImportStatus'; 20 | importId?: number | null; 21 | siteId?: number | null; 22 | status?: string | null; 23 | filesTotal?: number | null; 24 | filesProcessed?: number | null; 25 | failureDetails?: { 26 | __typename?: 'AppEnvironmentMediaImportStatusFailureDetails'; 27 | previousStatus?: string | null; 28 | globalErrors?: Array< string | null > | null; 29 | fileErrorsUrl?: string | null; 30 | } | null; 31 | } | null; 32 | } | null > | null; 33 | } | null; 34 | }; 35 | -------------------------------------------------------------------------------- /src/lib/promise.ts: -------------------------------------------------------------------------------- 1 | export const createExternalizedPromise = < T >(): { 2 | promise: Promise< T >; 3 | resolve: ( value: T ) => void; 4 | reject: ( reason?: Error ) => void; 5 | } => { 6 | let externalResolve: ( ( value: T ) => void ) | null = null; 7 | let externalReject: ( ( reason?: Error ) => void ) | null = null; 8 | const externalizedPromise = new Promise< T >( ( resolve, reject ) => { 9 | externalResolve = resolve; 10 | externalReject = reject; 11 | } ); 12 | 13 | if ( ! externalReject || ! externalResolve ) { 14 | throw new Error( "Somehow, externalReject or externalResolve didn't get set." ); 15 | } 16 | 17 | return { 18 | promise: externalizedPromise, 19 | resolve: externalResolve, 20 | reject: externalReject, 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /src/lib/read-file.ts: -------------------------------------------------------------------------------- 1 | import { readFile } from 'node:fs/promises'; 2 | 3 | import * as exit from '../lib/cli/exit'; 4 | 5 | export async function readFromFile( path: string ): Promise< string > { 6 | try { 7 | const data = await readFile( path, 'utf-8' ); 8 | return data.trim(); 9 | } catch ( error ) { 10 | if ( ! ( error instanceof Error ) ) { 11 | exit.withError( 'Unknown error' ); 12 | } 13 | 14 | if ( 'code' in error && error.code === 'ENOENT' ) { 15 | exit.withError( `Could not load file "${ path }".` ); 16 | } 17 | 18 | exit.withError( error.message ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/site-import/db-file-import.ts: -------------------------------------------------------------------------------- 1 | import { App, AppEnvironment } from '../../graphqlTypes'; 2 | import { GB_IN_BYTES } from '../../lib/constants/file-size'; 3 | import { DATABASE_APPLICATION_TYPE_IDS } from '../../lib/constants/vipgo'; 4 | 5 | export const SQL_IMPORT_FILE_SIZE_LIMIT = 200 * GB_IN_BYTES; 6 | export const SQL_IMPORT_FILE_SIZE_LIMIT_LAUNCHED = 10 * GB_IN_BYTES; 7 | 8 | export type AppForImport = Pick< App, 'id' | 'environments' | 'name' | 'organization' | 'typeId' >; 9 | 10 | export interface ImportStatusType { 11 | dbOperationInProgress: boolean; 12 | importInProgress: boolean; 13 | } 14 | 15 | export type EnvForImport = Pick< 16 | AppEnvironment, 17 | 'id' | 'appId' | 'name' | 'type' | 'primaryDomain' | 'syncProgress' | 'importStatus' | 'launched' 18 | >; 19 | 20 | export function currentUserCanImportForApp( app: App | AppForImport ): boolean { 21 | // TODO: implement 22 | return Boolean( app ); 23 | } 24 | 25 | export const isSupportedApp = ( { typeId }: AppForImport ) => 26 | DATABASE_APPLICATION_TYPE_IDS.includes( typeId as number ); 27 | 28 | export const SYNC_STATUS_NOT_SYNCING = 'not_syncing'; 29 | 30 | export const isImportingBlockedBySync = ( env: EnvForImport ) => 31 | env.syncProgress?.status !== SYNC_STATUS_NOT_SYNCING; 32 | -------------------------------------------------------------------------------- /src/lib/site-import/status.generated.d.ts: -------------------------------------------------------------------------------- 1 | import * as Types from '../../graphqlTypes'; 2 | 3 | export type AppQueryVariables = Types.Exact< { 4 | appId?: Types.InputMaybe< Types.Scalars[ 'Int' ][ 'input' ] >; 5 | envId?: Types.InputMaybe< Types.Scalars[ 'Int' ][ 'input' ] >; 6 | } >; 7 | 8 | export type AppQuery = { 9 | __typename?: 'Query'; 10 | app?: { 11 | __typename?: 'App'; 12 | environments?: Array< { 13 | __typename?: 'AppEnvironment'; 14 | id?: number | null; 15 | isK8sResident?: boolean | null; 16 | launched?: boolean | null; 17 | jobs?: Array< 18 | | { 19 | __typename?: 'Job'; 20 | id?: number | null; 21 | type?: string | null; 22 | completedAt?: string | null; 23 | createdAt?: string | null; 24 | progress?: { 25 | __typename?: 'JobProgress'; 26 | status?: string | null; 27 | steps?: Array< { 28 | __typename?: 'JobProgressStep'; 29 | id?: string | null; 30 | name?: string | null; 31 | status?: string | null; 32 | } | null > | null; 33 | } | null; 34 | } 35 | | { 36 | __typename?: 'PrimaryDomainSwitchJob'; 37 | id?: number | null; 38 | type?: string | null; 39 | completedAt?: string | null; 40 | createdAt?: string | null; 41 | progress?: { 42 | __typename?: 'JobProgress'; 43 | status?: string | null; 44 | steps?: Array< { 45 | __typename?: 'JobProgressStep'; 46 | id?: string | null; 47 | name?: string | null; 48 | status?: string | null; 49 | } | null > | null; 50 | } | null; 51 | } 52 | | null 53 | > | null; 54 | importStatus?: { 55 | __typename?: 'AppEnvironmentImportStatus'; 56 | dbOperationInProgress?: boolean | null; 57 | importInProgress?: boolean | null; 58 | progress?: { 59 | __typename?: 'AppEnvironmentStatusProgress'; 60 | started_at?: number | null; 61 | finished_at?: number | null; 62 | steps?: Array< { 63 | __typename?: 'AppEnvironmentStatusProgressStep'; 64 | name?: string | null; 65 | started_at?: number | null; 66 | finished_at?: number | null; 67 | result?: string | null; 68 | output?: Array< string | null > | null; 69 | } | null > | null; 70 | } | null; 71 | } | null; 72 | } | null > | null; 73 | } | null; 74 | }; 75 | -------------------------------------------------------------------------------- /src/lib/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * https://github.com/sindresorhus/type-fest/blob/f361912c779dfb81c10cd5fcf860f60a80358058/source/omit-index-signature.d.ts#L103C1-L107C3 3 | */ 4 | export type OmitIndexSignature< ObjectType > = { 5 | // eslint-disable-next-line @typescript-eslint/ban-types 6 | [ KeyType in keyof ObjectType as {} extends Record< KeyType, unknown > 7 | ? never 8 | : KeyType ]: ObjectType[ KeyType ]; 9 | }; 10 | -------------------------------------------------------------------------------- /src/lib/types/graphql/rate-limit-exceeded-error.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLFormattedError } from 'graphql'; 2 | 3 | interface RateLimitExceededErrorExtension { 4 | errorHttpCode: 429; 5 | retryAfter: string; 6 | errorCode: string; 7 | } 8 | 9 | export interface RateLimitExceededError 10 | extends GraphQLFormattedError< RateLimitExceededErrorExtension > {} 11 | -------------------------------------------------------------------------------- /src/lib/user-error.ts: -------------------------------------------------------------------------------- 1 | export default class UserError extends Error { 2 | constructor( message: string, options?: ErrorOptions ) { 3 | super( message, options ); 4 | this.name = 'UserError'; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/validations/is-multi-site-sql-dump.ts: -------------------------------------------------------------------------------- 1 | const SQL_CREATE_TABLE_IS_MULTISITE_REGEX = 2 | /^CREATE TABLE(?: IF NOT EXISTS)? `?(wp_\d+_[a-z0-9_]*|wp_blogs)/i; 3 | const SQL_CONTAINS_MULTISITE_WP_USERS_REGEX = /`spam` tinyint\(2\)|`deleted` tinyint\(2\)/i; 4 | 5 | export function sqlDumpLineIsMultiSite( line: string ): boolean { 6 | // determine if we're on a CREATE TABLE statement line what has eg. wp_\d_options OR wp_blogs 7 | // also check if we're on a line that defines the additional two columns found on the wp_users table for multisites 8 | return ( 9 | SQL_CREATE_TABLE_IS_MULTISITE_REGEX.test( line ) || 10 | SQL_CONTAINS_MULTISITE_WP_USERS_REGEX.test( line ) 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/lib/validations/is-multi-site.generated.d.ts: -------------------------------------------------------------------------------- 1 | import * as Types from '../../graphqlTypes'; 2 | 3 | export type AppMultiSiteCheckQueryVariables = Types.Exact< { 4 | appId?: Types.InputMaybe< Types.Scalars[ 'Int' ][ 'input' ] >; 5 | envId?: Types.InputMaybe< Types.Scalars[ 'Int' ][ 'input' ] >; 6 | } >; 7 | 8 | export type AppMultiSiteCheckQuery = { 9 | __typename?: 'Query'; 10 | app?: { 11 | __typename?: 'App'; 12 | id?: number | null; 13 | name?: string | null; 14 | repo?: string | null; 15 | environments?: Array< { 16 | __typename?: 'AppEnvironment'; 17 | id?: number | null; 18 | appId?: number | null; 19 | name?: string | null; 20 | type?: string | null; 21 | isMultisite?: boolean | null; 22 | isSubdirectoryMultisite?: boolean | null; 23 | } | null > | null; 24 | } | null; 25 | }; 26 | -------------------------------------------------------------------------------- /src/lib/validations/is-multi-site.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | import { AppMultiSiteCheckQuery, AppMultiSiteCheckQueryVariables } from './is-multi-site.generated'; 4 | import { App, AppEnvironment } from '../../graphqlTypes'; 5 | import API from '../../lib/api'; 6 | import * as exit from '../../lib/cli/exit'; 7 | import { trackEventWithEnv } from '../../lib/tracker'; 8 | 9 | const isMultiSite = new WeakMap< Record< string, number >, boolean >(); 10 | 11 | export async function isMultiSiteInSiteMeta( appId: number, envId: number ): Promise< boolean > { 12 | const args = { 13 | 0: appId, 14 | 1: envId, 15 | }; 16 | 17 | // if we've already been through this, avoid doing it again within the same process 18 | const ret = isMultiSite.get( args ); 19 | if ( 'boolean' === typeof ret ) { 20 | return ret; 21 | } 22 | 23 | const api = API(); 24 | let res; 25 | try { 26 | res = await api.query< AppMultiSiteCheckQuery, AppMultiSiteCheckQueryVariables >( { 27 | query: gql` 28 | query AppMultiSiteCheck($appId: Int, $envId: Int) { 29 | app(id: $appId) { 30 | id 31 | name 32 | repo 33 | environments(id: $envId) { 34 | id 35 | appId 36 | name 37 | type 38 | isMultisite 39 | isSubdirectoryMultisite 40 | } 41 | } 42 | } 43 | `, 44 | variables: { 45 | appId, 46 | envId, 47 | }, 48 | } ); 49 | } catch ( GraphQlError ) { 50 | const track = trackEventWithEnv.bind( null, appId, envId ); 51 | await track( 'import_sql_command_error', { 52 | error_type: 'GraphQL-MultiSite-Check-failed', 53 | gql_err: GraphQlError, 54 | } ); 55 | // eslint-disable-next-line @typescript-eslint/restrict-template-expressions 56 | exit.withError( `StartImport call failed: ${ GraphQlError }` ); 57 | } 58 | 59 | if ( Array.isArray( res.data.app?.environments ) ) { 60 | const environments = ( res.data.app as App ).environments; 61 | if ( ! environments?.length ) { 62 | isMultiSite.set( args, false ); 63 | return false; 64 | } 65 | // we asked for one result with one appId and one envId, so... 66 | const thisEnv = environments[ 0 ] as AppEnvironment; 67 | if ( thisEnv.isMultisite || thisEnv.isSubdirectoryMultisite ) { 68 | isMultiSite.set( args, true ); 69 | return true; 70 | } 71 | } 72 | 73 | return false; 74 | } 75 | -------------------------------------------------------------------------------- /src/lib/validations/is-multisite-domain-mapped.generated.d.ts: -------------------------------------------------------------------------------- 1 | import * as Types from '../../graphqlTypes'; 2 | 3 | export type AppMappedDomainsQueryVariables = Types.Exact< { 4 | appId?: Types.InputMaybe< Types.Scalars[ 'Int' ][ 'input' ] >; 5 | envId?: Types.InputMaybe< Types.Scalars[ 'Int' ][ 'input' ] >; 6 | } >; 7 | 8 | export type AppMappedDomainsQuery = { 9 | __typename?: 'Query'; 10 | app?: { 11 | __typename?: 'App'; 12 | id?: number | null; 13 | name?: string | null; 14 | environments?: Array< { 15 | __typename?: 'AppEnvironment'; 16 | uniqueLabel?: string | null; 17 | isMultisite?: boolean | null; 18 | domains?: { 19 | __typename?: 'DomainList'; 20 | nodes?: Array< { 21 | __typename?: 'Domain'; 22 | name: string; 23 | isPrimary?: boolean | null; 24 | } | null > | null; 25 | } | null; 26 | } | null > | null; 27 | } | null; 28 | }; 29 | -------------------------------------------------------------------------------- /src/lib/validations/line-by-line.ts: -------------------------------------------------------------------------------- 1 | import debugLib from 'debug'; 2 | import { createReadStream } from 'node:fs'; 3 | import { open } from 'node:fs/promises'; 4 | import { type Interface, createInterface } from 'node:readline'; 5 | 6 | import * as exit from '../../lib/cli/exit'; 7 | 8 | const debug = debugLib( 'vip:validations:line-by-line' ); 9 | export interface PerLineValidationObject { 10 | execute: ( line: string ) => unknown; 11 | postLineExecutionProcessing?: ( params: PostLineExecutionProcessingParams ) => Promise< unknown >; 12 | } 13 | 14 | export interface PostLineExecutionProcessingParams { 15 | appId?: number; 16 | envId?: number; 17 | fileName?: string; 18 | isImport?: boolean; 19 | skipChecks?: string[]; 20 | searchReplace?: string | string[]; 21 | } 22 | 23 | export async function getReadInterface( filename: string ): Promise< Interface > { 24 | let fd; 25 | try { 26 | fd = await open( filename ); 27 | } catch ( err ) { 28 | exit.withError( 29 | 'The file at the provided path is either missing or not readable. Please check the input and try again.' 30 | ); 31 | } 32 | 33 | return createInterface( { 34 | input: createReadStream( '', { fd } ), 35 | output: undefined, 36 | } ); 37 | } 38 | 39 | export async function fileLineValidations( 40 | appId: number, 41 | envId: number, 42 | fileName: string, 43 | validations: PerLineValidationObject[], 44 | searchReplace: string | string[] 45 | ) { 46 | const isImport = true; 47 | const readInterface = await getReadInterface( fileName ); 48 | 49 | debug( 'Validations: ', validations ); 50 | 51 | readInterface.on( 'line', line => { 52 | validations.forEach( validation => { 53 | validation.execute( line ); 54 | } ); 55 | } ); 56 | 57 | readInterface.on( 'error', ( err: Error ) => { 58 | throw new Error( `Error validating input file: ${ err.toString() }`, { cause: err } ); 59 | } ); 60 | 61 | // Block until the processing completes 62 | await new Promise( resolve => readInterface.on( 'close', resolve ) ); 63 | readInterface.close(); 64 | 65 | return Promise.all( 66 | validations.map( ( validation: PerLineValidationObject ) => { 67 | if ( 68 | Object.prototype.hasOwnProperty.call( validation, 'postLineExecutionProcessing' ) && 69 | typeof validation.postLineExecutionProcessing === 'function' 70 | ) { 71 | return validation.postLineExecutionProcessing( { 72 | fileName, 73 | isImport, 74 | appId, 75 | envId, 76 | searchReplace, 77 | } ); 78 | } 79 | 80 | return Promise.resolve(); 81 | } ) 82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /src/lib/validations/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Get SQL statements matching a supplied pattern from a file stream 3 | * 4 | * @param {RegExp} statementRegex A RegExp pattern representing the start of the statement to capture 5 | * @return {Function} A function which processes individual lines to capture the matching statements 6 | */ 7 | export function getMultilineStatement( statementRegex: RegExp ): ( line: string ) => string[][] { 8 | const matchingStatements: string[][] = []; 9 | let isCapturing = false; 10 | let index = 0; 11 | 12 | /** 13 | * Processes each line of the file stream and builds an array of statements which start with the supplied pattern 14 | * 15 | * @param {string} line A line from the file stream 16 | * @return {Array} An array of matching statements where each statement is presented as an array of lines 17 | */ 18 | return ( line: string ): string[][] => { 19 | const shouldStartCapture = statementRegex.test( line ); 20 | const shouldEndCapture = ( shouldStartCapture || isCapturing ) && line.endsWith( ';' ); 21 | if ( shouldStartCapture ) { 22 | isCapturing = true; 23 | matchingStatements[ index ] = []; 24 | } 25 | 26 | if ( isCapturing ) { 27 | matchingStatements[ index ].push( line ); 28 | } 29 | 30 | if ( shouldEndCapture ) { 31 | isCapturing = false; 32 | index++; 33 | } 34 | 35 | return matchingStatements; 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ "ES2022" ], 4 | "module": "node16", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "skipLibCheck": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "moduleResolution": "node16", 10 | 11 | // Target latest version of ECMAScript. 12 | "target": "ES2022", 13 | // Don't parse types from JS as TS doesn't play well with Flow-ish JS. 14 | "allowJs": false, 15 | // Don't emit; allow Babel to transform files. 16 | "noEmit": true, 17 | // Can't checkJs if we don't allowJs, so this remains false 18 | "checkJs": false, 19 | // Disallow features that require cross-file information for emit as we're using Babel 20 | "isolatedModules": true, 21 | "resolveJsonModule": true, 22 | "baseUrl": ".", 23 | "paths": { 24 | "*": [ "./types/*", "./node_modules/@types/node/*" ] 25 | }, 26 | // It's worth enabling this, I promise! 27 | "noImplicitAny": true 28 | }, 29 | "include": [ "src", "helpers", "config", "__tests__", "__fixtures__" ], 30 | "exclude": [ "**/*.js", "codegen.ts" ] 31 | } 32 | -------------------------------------------------------------------------------- /types/copy-dir/index.d.ts: -------------------------------------------------------------------------------- 1 | interface CopyDir { 2 | sync( from: string, to: string ); 3 | } 4 | 5 | declare const exp: CopyDir; 6 | 7 | export default exp; 8 | export = exp; 9 | -------------------------------------------------------------------------------- /types/lando/index.d.ts: -------------------------------------------------------------------------------- 1 | import Lando = require('./lib/lando'); 2 | 3 | export = Lando; 4 | -------------------------------------------------------------------------------- /types/lando/lib/art.d.ts: -------------------------------------------------------------------------------- 1 | export function appDestroy( { name, phase }?: { name: any; phase?: string } ): any; 2 | export function appRebuild( { 3 | name, 4 | phase, 5 | warnings, 6 | }?: { 7 | name: any; 8 | phase?: string; 9 | warnings?: {}; 10 | } ): any; 11 | export function appRestart( { 12 | name, 13 | phase, 14 | warnings, 15 | }?: { 16 | name: any; 17 | phase?: string; 18 | warnings?: {}; 19 | } ): any; 20 | export function appStart( { 21 | name, 22 | phase, 23 | warnings, 24 | }?: { 25 | name: any; 26 | phase?: string; 27 | warnings?: {}; 28 | } ): any; 29 | export function appStop( { name, phase }?: { name: any; phase?: string } ): any; 30 | export function crash(): string; 31 | export function experimental( on?: boolean ): string; 32 | export function init(): string; 33 | export function newContent( type?: string ): string; 34 | export function noDockerDep( dep?: string ): string; 35 | export function poweroff( { phase }?: { phase?: string } ): any; 36 | export function print( { text, color }?: { text: any; color?: string } ): any; 37 | export function printFont( { 38 | text, 39 | color, 40 | font, 41 | }?: { 42 | text: any; 43 | color?: string; 44 | font?: string; 45 | } ): any; 46 | export function releaseChannel( channel?: string ): string; 47 | export function secretToggle( on?: boolean ): string; 48 | export function secretToggleDenied( on?: boolean ): string; 49 | export function badToken(): string; 50 | -------------------------------------------------------------------------------- /types/lando/lib/bootstrap.d.ts: -------------------------------------------------------------------------------- 1 | import { LandoConfig } from './lando'; 2 | 3 | export function buildConfig( options: Partial< LandoConfig > ): LandoConfig; 4 | export function dc( 5 | shell: any, 6 | bin: any, 7 | cmd: any, 8 | { 9 | compose, 10 | project, 11 | opts, 12 | }: { 13 | compose: any; 14 | project: any; 15 | opts?: {}; 16 | } 17 | ): any; 18 | export function getApp( files: any, userConfRoot: any ): any; 19 | export function getLandoFiles( files?: any[], startFrom?: string ): any; 20 | export function getTasks( config?: {}, argv?: {}, tasks?: any[] ): any[]; 21 | export function setupCache( log: any, config: any ): import('lando/lib/cache'); 22 | export function setupEngine( 23 | config: any, 24 | cache: any, 25 | events: any, 26 | log: any, 27 | shell: any, 28 | id: any 29 | ): import('lando/lib/engine'); 30 | export function setupMetrics( log: any, config: any ): import('lando/lib/metrics'); 31 | -------------------------------------------------------------------------------- /types/lando/lib/cache.d.ts: -------------------------------------------------------------------------------- 1 | export = Cache; 2 | declare class Cache extends NodeCache { 3 | constructor( { log, cacheDir }?: { log?: Log; cacheDir?: string } ); 4 | log: Log; 5 | cacheDir: string; 6 | /** 7 | * Sets an item in the cache 8 | * 9 | * @since 3.0.0 10 | * @alias lando.cache.set 11 | * @param {String} key The name of the key to store the data with. 12 | * @param {Any} data The data to store in the cache. 13 | * @param {Object} [opts] Options to pass into the cache 14 | * @param {Boolean} [opts.persist=false] Whether this cache data should persist between processes. Eg in a file instead of memory 15 | * @param {Integer} [opts.ttl=0] Seconds the cache should live. 0 mean forever. 16 | * @example 17 | * // Add a string to the cache 18 | * lando.cache.set('mykey', 'mystring'); 19 | * 20 | * // Add an object to persist in the file cache 21 | * lando.cache.set('mykey', data, {persist: true}); 22 | * 23 | * // Add an object to the cache for five seconds 24 | * lando.cache.set('mykey', data, {ttl: 5}); 25 | */ 26 | set( 27 | key: string, 28 | data: Any, 29 | { 30 | persist, 31 | ttl, 32 | }?: { 33 | persist?: boolean; 34 | ttl?: Integer; 35 | } 36 | ): void; 37 | /** 38 | * Gets an item in the cache 39 | * 40 | * @since 3.0.0 41 | * @alias lando.cache.get 42 | * @param {String} key The name of the key to retrieve the data. 43 | * @return {Any} The data stored in the cache if applicable. 44 | * @example 45 | * // Get the data stored with key mykey 46 | * const data = lando.cache.get('mykey'); 47 | */ 48 | get( key: string ): Any; 49 | /** 50 | * Manually remove an item from the cache. 51 | * 52 | * @since 3.0.0 53 | * @alias lando.cache.remove 54 | * @param {String} key The name of the key to remove the data. 55 | * @example 56 | * // Remove the data stored with key mykey 57 | * lando.cache.remove('mykey'); 58 | */ 59 | remove( key: string ): void; 60 | __get: < T >( key: string | number, cb?: NodeCache.Callback< T > ) => T; 61 | __set: { 62 | < T_1 >( 63 | key: string | number, 64 | value: T_1, 65 | ttl: string | number, 66 | cb?: NodeCache.Callback< boolean > 67 | ): boolean; 68 | < T_2 >( key: string | number, value: T_2, cb?: NodeCache.Callback< boolean > ): boolean; 69 | }; 70 | __del: ( 71 | keys: ( string | number ) | ( string | number )[], 72 | cb?: NodeCache.Callback< number > 73 | ) => number; 74 | } 75 | import NodeCache = require('node-cache'); 76 | import Log = require('lando/lib/logger'); 77 | -------------------------------------------------------------------------------- /types/lando/lib/compose.d.ts: -------------------------------------------------------------------------------- 1 | export function build( 2 | compose: any, 3 | project: any, 4 | opts?: {} 5 | ): { 6 | cmd: any; 7 | opts: { 8 | mode: string; 9 | cstdio: any; 10 | silent: any; 11 | }; 12 | }; 13 | export function getId( 14 | compose: any, 15 | project: any, 16 | opts?: {} 17 | ): { 18 | cmd: any; 19 | opts: { 20 | mode: string; 21 | cstdio: any; 22 | silent: any; 23 | }; 24 | }; 25 | export function logs( 26 | compose: any, 27 | project: any, 28 | opts?: {} 29 | ): { 30 | cmd: any; 31 | opts: { 32 | mode: string; 33 | cstdio: any; 34 | silent: any; 35 | }; 36 | }; 37 | export function pull( 38 | compose: any, 39 | project: any, 40 | opts?: {} 41 | ): { 42 | cmd: any; 43 | opts: { 44 | mode: string; 45 | cstdio: any; 46 | silent: any; 47 | }; 48 | }; 49 | export function remove( 50 | compose: any, 51 | project: any, 52 | opts?: {} 53 | ): { 54 | cmd: any; 55 | opts: { 56 | mode: string; 57 | cstdio: any; 58 | silent: any; 59 | }; 60 | }; 61 | export function run( 62 | compose: any, 63 | project: any, 64 | opts?: {} 65 | ): { 66 | cmd: any; 67 | opts: { 68 | mode: string; 69 | cstdio: any; 70 | silent: any; 71 | }; 72 | }; 73 | export function start( 74 | compose: any, 75 | project: any, 76 | opts?: {} 77 | ): { 78 | cmd: any; 79 | opts: { 80 | mode: string; 81 | cstdio: any; 82 | silent: any; 83 | }; 84 | }; 85 | export function stop( 86 | compose: any, 87 | project: any, 88 | opts?: {} 89 | ): { 90 | cmd: any; 91 | opts: { 92 | mode: string; 93 | cstdio: any; 94 | silent: any; 95 | }; 96 | }; 97 | -------------------------------------------------------------------------------- /types/lando/lib/config.d.ts: -------------------------------------------------------------------------------- 1 | export function tryConvertJson( value: string ): any; 2 | export function merge( old: any, ...fresh: any ): any; 3 | export function stripEnv( prefix: string ): any; 4 | export function defaults(): any; 5 | export function getEngineConfig( { engineConfig, env }: { engineConfig?: {}; env?: {} } ): {}; 6 | export function getOclifCacheDir( product?: string ): string; 7 | export function loadFiles( files: any[] ): any; 8 | export function loadEnvs( prefix: string ): any; 9 | -------------------------------------------------------------------------------- /types/lando/lib/daemon.d.ts: -------------------------------------------------------------------------------- 1 | export = LandoDaemon; 2 | declare class LandoDaemon { 3 | constructor( 4 | cache?: Cache, 5 | events?: Events, 6 | docker?: string | boolean, 7 | log?: Log, 8 | context?: string, 9 | compose?: string | boolean 10 | ); 11 | cache: Cache; 12 | compose: string | boolean; 13 | context: string; 14 | docker: string | boolean; 15 | events: Events; 16 | log: Log; 17 | up(): any; 18 | down(): any; 19 | isUp( log?: Log, cache?: Cache, docker?: string | boolean ): any; 20 | getVersions(): any; 21 | getComposeSeparator(): any; 22 | } 23 | import Cache = require('lando/lib/cache'); 24 | import Events = require('lando/lib/events'); 25 | import Log = require('lando/lib/logger'); 26 | -------------------------------------------------------------------------------- /types/lando/lib/docker.d.ts: -------------------------------------------------------------------------------- 1 | export = Landerode; 2 | declare class Landerode extends Dockerode { 3 | constructor( opts?: {}, id?: string, promise?: any ); 4 | id: string; 5 | createNet( name: any, opts?: {} ): Promise< Dockerode.Network >; 6 | scan( cid: string ): Promise< Dockerode.ContainerInspectInfo >; 7 | isRunning( cid: any ): any; 8 | list( options?: {} ): any; 9 | remove( 10 | cid: any, 11 | opts?: { 12 | v: boolean; 13 | force: boolean; 14 | } 15 | ): any; 16 | stop( cid: any, opts?: {} ): any; 17 | } 18 | import Dockerode = require('dockerode'); 19 | -------------------------------------------------------------------------------- /types/lando/lib/env.d.ts: -------------------------------------------------------------------------------- 1 | export function getDockerBinPath(): string | false; 2 | export function getDockerComposeBinPath(): string | false; 3 | export function getComposeExecutable(): string | false; 4 | export function getDockerExecutable(): string | false; 5 | export function getOclifCacheDir( product: any ): string; 6 | -------------------------------------------------------------------------------- /types/lando/lib/error.d.ts: -------------------------------------------------------------------------------- 1 | export = ErrorHandler; 2 | declare class ErrorHandler { 3 | constructor( log?: Log, metrics?: Metrics ); 4 | log: Log; 5 | metrics: Metrics; 6 | /** 7 | * Returns the lando options 8 | * 9 | * This means all the options passed in before the `--` flag. 10 | * 11 | * @since 3.0.0 12 | * @alias lando.error.handle 13 | * @param {Object} error Error object 14 | * @param {Boolean} report Whether to report the error or not 15 | * @return {Integer} the error code 16 | * @example 17 | * // Gets all the pre-global options that have been specified. 18 | * const argv = lando.tasks.argv(); 19 | * @todo make this static and then fix all call sites 20 | */ 21 | handle( { message, stack, code, hide, verbose }?: any, report?: boolean ): Integer; 22 | } 23 | import Log = require('lando/lib/logger'); 24 | import Metrics = require('lando/lib/metrics'); 25 | -------------------------------------------------------------------------------- /types/lando/lib/events.d.ts: -------------------------------------------------------------------------------- 1 | export = AsyncEvents; 2 | declare class AsyncEvents extends EventEmitter { 3 | constructor( log?: Log ); 4 | log: Log; 5 | _listeners: any[]; 6 | /** 7 | * Our overridden event on method. 8 | * 9 | * This optionally allows a priority to be specified. Lower priorities run first. 10 | * 11 | * @since 3.0.0 12 | * @alias lando.events.on 13 | * @param {String} name The name of the event 14 | * @param {Integer} [priority=5] The priority the event should run in. 15 | * @param {Function} fn The function to call. Should get the args specified in the corresponding `emit` declaration. 16 | * @return {Promise} A Promise 17 | * @example 18 | * // Print out all our apps as they get instantiated and do it before other `post-instantiate-app` events 19 | * lando.events.on('post-instantiate-app', 1, app => { 20 | * console.log(app); 21 | * }); 22 | * 23 | * // Log a helpful message after an app is started, don't worry about whether it runs before or 24 | * // after other `post-start` events 25 | * return app.events.on('post-start', () => { 26 | * lando.log.info('App %s started', app.name); 27 | * }); 28 | */ 29 | on( 30 | name: string, 31 | priority: number, 32 | fn: ( ...args: any[] ) => unknown | Promise< unknown > 33 | ): this; 34 | on( name: string, fn: ( ...args: any[] ) => unknown | Promise< unknown > ): this; 35 | once( 36 | eventName: string | symbol, 37 | listener: ( ...args: any[] ) => unknown | Promise< unknown > 38 | ): this; 39 | /** 40 | * Reimplements event emit method. 41 | * 42 | * This makes events blocking and promisified. 43 | * 44 | * @since 3.0.0 45 | * @alias lando.events.emit 46 | * @param {String} name The name of the event 47 | * @param {...Any} [args] Options args to pass. 48 | * @return {Promise} A Promise 49 | * @example 50 | * // Emits a global event with a config arg 51 | * return lando.events.emit('wolf359', config); 52 | * 53 | * // Emits an app event with a config arg 54 | * return app.events.emit('sector001', config); 55 | */ 56 | emit( ...args: any[] ): Promise< unknown >; 57 | __on: ( eventName: string | symbol, listener: ( ...args: any[] ) => void ) => EventEmitter; 58 | __emit: ( eventName: string | symbol, ...args: any[] ) => boolean; 59 | } 60 | import EventEmitter_1 = require('events'); 61 | import EventEmitter = EventEmitter_1.EventEmitter; 62 | import Log = require('lando/lib/logger'); 63 | -------------------------------------------------------------------------------- /types/lando/lib/factory.d.ts: -------------------------------------------------------------------------------- 1 | export = Factory; 2 | declare class Factory { 3 | constructor( 4 | classes?: ( 5 | | { 6 | name: string; 7 | builder: { 8 | new ( id: any, info?: {}, ...sources: any[] ): { 9 | id: any; 10 | info: {}; 11 | data: any; 12 | }; 13 | }; 14 | } 15 | | { 16 | name: string; 17 | builder: { 18 | new ( id: any, config?: {} ): { 19 | id: any; 20 | config: { 21 | proxy: any; 22 | services: any; 23 | tooling: any; 24 | }; 25 | }; 26 | }; 27 | } 28 | )[] 29 | ); 30 | registry: ( 31 | | { 32 | name: string; 33 | builder: { 34 | new ( id: any, info?: {}, ...sources: any[] ): { 35 | id: any; 36 | info: {}; 37 | data: any; 38 | }; 39 | }; 40 | } 41 | | { 42 | name: string; 43 | builder: { 44 | new ( id: any, config?: {} ): { 45 | id: any; 46 | config: { 47 | proxy: any; 48 | services: any; 49 | tooling: any; 50 | }; 51 | }; 52 | }; 53 | } 54 | )[]; 55 | add( { 56 | name, 57 | builder, 58 | config, 59 | parent, 60 | }: { 61 | name: any; 62 | builder: any; 63 | config?: {}; 64 | parent?: any; 65 | } ): any; 66 | get( name?: string ): any; 67 | } 68 | -------------------------------------------------------------------------------- /types/lando/lib/formatters.d.ts: -------------------------------------------------------------------------------- 1 | export function formatData( 2 | data: any, 3 | { 4 | path, 5 | format, 6 | filter, 7 | }?: { 8 | path?: string; 9 | format?: string; 10 | filter?: any[]; 11 | }, 12 | opts?: {} 13 | ): string; 14 | export function formatOptions( omit?: any[] ): any; 15 | export function getInteractive( options: any, argv: any ): any; 16 | export function handleInteractive( inquiry: any, argv: any, command: any, lando: any ): any; 17 | export function sortOptions( options: any ): any; 18 | -------------------------------------------------------------------------------- /types/lando/lib/logger.d.ts: -------------------------------------------------------------------------------- 1 | import winston = require('winston'); 2 | export = Log; 3 | declare class Log extends winston.Logger { 4 | constructor( { 5 | logDir, 6 | logLevelConsole, 7 | logLevel, 8 | logName, 9 | }?: { 10 | logDir: any; 11 | logLevelConsole?: string; 12 | logLevel?: string; 13 | logName?: string; 14 | } ); 15 | sanitizedKeys: string[]; 16 | alsoSanitize( key: any ): void; 17 | } 18 | -------------------------------------------------------------------------------- /types/lando/lib/metrics.d.ts: -------------------------------------------------------------------------------- 1 | export = Metrics; 2 | declare class Metrics { 3 | constructor( { 4 | id, 5 | log, 6 | endpoints, 7 | data, 8 | }?: { 9 | id?: string; 10 | log?: Log; 11 | endpoints?: any[]; 12 | data?: {}; 13 | } ); 14 | id: string; 15 | log: Log; 16 | endpoints: any[]; 17 | data: {}; 18 | report( action?: string, data?: {} ): any; 19 | } 20 | import Log = require('lando/lib/logger'); 21 | -------------------------------------------------------------------------------- /types/lando/lib/plugins.d.ts: -------------------------------------------------------------------------------- 1 | export = Plugins; 2 | declare class Plugins { 3 | constructor( log?: Log ); 4 | registry: any[]; 5 | log: Log; 6 | /** 7 | * Finds plugins 8 | * 9 | * @since 3.5.0 10 | * @alias lando.plugins.find 11 | * @param {Array} dirs Directories to scan for plugins 12 | * @param {Object} options Options to pass in 13 | * @param {Array} [options.disablePlugins=[]] Array of plugin names to not load 14 | * @param {Array} [options.plugins=[]] Array of additional plugins to consider loading 15 | * @return {Array} Array of plugin metadata 16 | */ 17 | find( 18 | dirs: any[], 19 | { 20 | disablePlugins, 21 | plugins, 22 | }?: { 23 | disablePlugins?: any[]; 24 | plugins?: any[]; 25 | } 26 | ): any[]; 27 | /** 28 | * Loads a plugin. 29 | * 30 | * @since 3.0.0 31 | * @alias lando.plugins.load 32 | * @param {String} plugin The name of the plugin 33 | * @param {String} [file=plugin.path] That path to the plugin 34 | * @param {Object} [...injected] Something to inject into the plugin 35 | * @return {Object} Data about our plugin. 36 | */ 37 | load( plugin: string, file?: string, ...injected: any[] ): any; 38 | } 39 | import Log = require('lando/lib/logger'); 40 | -------------------------------------------------------------------------------- /types/lando/lib/promise.d.ts: -------------------------------------------------------------------------------- 1 | export = Promise; 2 | -------------------------------------------------------------------------------- /types/lando/lib/router.d.ts: -------------------------------------------------------------------------------- 1 | export function eventWrapper( name: any, daemon: any, events: any, data: any, run: any ): any; 2 | export function build( data: any, compose: any ): any; 3 | export function destroy( data: any, compose: any, docker: any ): any; 4 | export function exists( data: any, compose: any, docker: any, ids?: any[] ): any; 5 | export function logs( data: any, compose: any ): any; 6 | export function run( data: any, compose: any, docker: any, started?: boolean ): any; 7 | export function scan( data: any, compose: any, docker: any ): any; 8 | export function start( data: any, compose: any ): any; 9 | export function stop( data: any, compose: any, docker: any ): any; 10 | -------------------------------------------------------------------------------- /types/lando/lib/scan.d.ts: -------------------------------------------------------------------------------- 1 | declare function _exports( log?: Log ): ( 2 | urls: any[], 3 | { 4 | max, 5 | waitCodes, 6 | }?: { 7 | max?: Integer; 8 | waitCode?: any[]; 9 | } 10 | ) => any[]; 11 | export = _exports; 12 | import Log = require('lando/lib/logger'); 13 | -------------------------------------------------------------------------------- /types/lando/lib/shell.d.ts: -------------------------------------------------------------------------------- 1 | export = Shell; 2 | declare class Shell { 3 | constructor( log?: Log ); 4 | log: Log; 5 | running: any[]; 6 | stdout: any; 7 | stderr: any; 8 | /** 9 | * Gets running processes. 10 | * 11 | * @since 3.0.0 12 | * @alias lando.shell.get 13 | * @return {Array} An array of the currently running processes 14 | */ 15 | get(): any[]; 16 | /** 17 | * Runs a command. 18 | * 19 | * This is an abstraction method that: 20 | * 21 | * 1. Delegates to either node's native `spawn` or `exec` methods. 22 | * 2. Promisifies the calling of these function 23 | * 3. Handles `stdout`, `stdin` and `stderr` 24 | * 25 | * @since 3.0.0 26 | * @alias lando.shell.sh 27 | * @see [extra exec options](https://nodejs.org/api/child_process.html#child_process_child_process_exec_command_options_callback) 28 | * @see [extra spawn options](https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options) 29 | * @param {Array} cmd The command to run as elements in an array. 30 | * @param {Object} [opts] Options to help determine how the exec is run. 31 | * @param {Boolean} [opts.mode='exec'] The mode to run in 32 | * @param {Boolean} [opts.detached=false] Whether we are running in detached mode or not (deprecated) 33 | * @param {Boolean} [opts.cwd=process.cwd()] The directory to run the command from 34 | * @return {Promise} A promise with collected results if applicable. 35 | * @example 36 | * // Run a command in collect mode 37 | * return lando.shell.sh(['ls', '-lsa', '/'], {mode: 'collect'}) 38 | * 39 | * // Catch and log any errors 40 | * .catch(err => { 41 | * lando.log.error(err); 42 | * }) 43 | * 44 | * // Print the collected results of the command 45 | * .then(results => { 46 | * console.log(results); 47 | * }); 48 | */ 49 | sh( 50 | cmd: any[], 51 | { 52 | mode, 53 | detached, 54 | cwd, 55 | cstdio, 56 | silent, 57 | }?: { 58 | mode?: boolean; 59 | detached?: boolean; 60 | cwd?: boolean; 61 | } 62 | ): Promise; 63 | /** 64 | * Returns the path of a specific command or binary. 65 | * 66 | * @since 3.0.0 67 | * @function 68 | * @alias lando.shell.which 69 | * @param {String} cmd A command to search for. 70 | * @return {String|null} The path to the command or null. 71 | * @example 72 | * // Determine the location of the 'docker' command 73 | * const which = lando.shell.which(DOCKER_EXECUTABLE); 74 | */ 75 | which( cmd: string ): string | null; 76 | } 77 | import Log = require('lando/lib/logger'); 78 | -------------------------------------------------------------------------------- /types/lando/lib/table.d.ts: -------------------------------------------------------------------------------- 1 | export = Table; 2 | declare class Table { 3 | constructor( 4 | data: any, 5 | { 6 | border, 7 | keyColor, 8 | joiner, 9 | sort, 10 | }?: { 11 | border?: boolean; 12 | keyColor?: string; 13 | joiner?: string; 14 | sort?: boolean; 15 | }, 16 | opts?: {} 17 | ); 18 | border: boolean; 19 | joiner: string; 20 | keyColor: string; 21 | sort: boolean; 22 | add( 23 | data: any, 24 | { 25 | joiner, 26 | sort, 27 | }?: { 28 | joiner?: string; 29 | sort?: boolean; 30 | } 31 | ): void; 32 | } 33 | -------------------------------------------------------------------------------- /types/lando/lib/user.d.ts: -------------------------------------------------------------------------------- 1 | export function getUid(): string; 2 | export function getGid(): string; 3 | export function getUsername(): string; 4 | -------------------------------------------------------------------------------- /types/lando/lib/utils.d.ts: -------------------------------------------------------------------------------- 1 | export function getAppMounts( app: any ): any; 2 | export function dockerComposify( data: string ): string; 3 | export function appMachineName( data: any ): string; 4 | export function dumpComposeData( data: any, dir: any ): any; 5 | export function loadComposeFiles( files: any, dir: any ): any; 6 | export function getCliEnvironment( more?: {} ): any; 7 | export function getId( c: any ): any; 8 | export function getInfoDefaults( app: any ): any; 9 | export function getGlobals( app: any ): any; 10 | export function getServices( composeData: any ): any; 11 | export function getUser( service: any, info?: any[] ): any; 12 | export function metricsParse( app: any ): { 13 | app: any; 14 | type: any; 15 | }; 16 | export function normalizer( data: any ): any; 17 | export function makeExecutable( files: any, base?: string ): void; 18 | export function moveConfig( src: any, dest?: string ): string; 19 | export function shellEscape( command: any, wrap?: boolean, args?: string[] ): any; 20 | export function toLandoContainer( { 21 | Names, 22 | Labels, 23 | Id, 24 | Status, 25 | }: { 26 | Names: any; 27 | Labels: any; 28 | Id: any; 29 | Status: any; 30 | } ): { 31 | id: any; 32 | service: any; 33 | name: any; 34 | app: any; 35 | src: any; 36 | kind: string; 37 | lando: boolean; 38 | instance: any; 39 | status: any; 40 | }; 41 | export function toObject( keys: any, data?: {} ): any; 42 | export function validateFiles( files?: any[], base?: string ): any; 43 | -------------------------------------------------------------------------------- /types/lando/lib/yaml.d.ts: -------------------------------------------------------------------------------- 1 | export = Yaml; 2 | declare class Yaml { 3 | constructor( log?: Log ); 4 | log: Log; 5 | /** 6 | * Loads a yaml object from a file. 7 | * 8 | * @since 3.0.0 9 | * @alias lando.yaml.load 10 | * @param {String} file The path to the file to be loaded 11 | * @return {Object} The loaded object 12 | * @example 13 | * // Add a string to the cache 14 | * const thing = lando.yaml.load('/tmp/myfile.yml'); 15 | */ 16 | load( file: string ): any; 17 | /** 18 | * Dumps an object to a YAML file 19 | * 20 | * @since 3.0.0 21 | * @alias lando.yaml.dump 22 | * @param {String} file The path to the file to be loaded 23 | * @param {Object} data The object to dump 24 | * @return {String} Flename 25 | */ 26 | dump( file: string, data?: any ): string; 27 | } 28 | import Log = require('lando/lib/logger'); 29 | -------------------------------------------------------------------------------- /types/lando/plugins/lando-core/lib/utils.d.ts: -------------------------------------------------------------------------------- 1 | import App from 'lando/lib/app'; 2 | 3 | export interface AppInfo { 4 | name: string; 5 | location: string; 6 | services: string[]; 7 | [ key: string ]: unknown; 8 | } 9 | 10 | export function getHostPath( mount: any ): any; 11 | export function getUrls( 12 | data: any, 13 | scan?: string[], 14 | secured?: string[], 15 | bindAddress?: string 16 | ): any; 17 | export function normalizePath( local: any, base?: string, excludes?: any[] ): any; 18 | export function normalizeOverrides( overrides: any, base?: string, volumes?: {} ): any; 19 | export function startTable( app: App ): AppInfo; 20 | export function stripPatch( version: any ): any; 21 | export function stripWild( versions: any ): any; 22 | -------------------------------------------------------------------------------- /types/lando/plugins/lando-tooling/lib/build.d.ts: -------------------------------------------------------------------------------- 1 | declare function _exports( 2 | config: any, 3 | injected: any 4 | ): { 5 | command: string; 6 | describe: string; 7 | run: ( answers: Record< string, unknown > ) => Promise< unknown >; 8 | options: Record< string, unknown >; 9 | }; 10 | export = _exports; 11 | --------------------------------------------------------------------------------