├── .editorconfig ├── .eslintrc.json ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── daily-install-cli.yml │ ├── node.js.yml │ ├── npm-audit.yml │ ├── on-push-publish-to-npm.yml │ ├── prerelease-main.yml │ ├── prerelease.yml │ └── version-bump-publish.yml ├── .gitignore ├── .npmrc ├── .tidelift.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── COPYRIGHT ├── LICENSE ├── README.md ├── RELEASING.md ├── bin ├── gen-health-table.js ├── run └── run.cmd ├── e2e ├── .eslintrc.json └── e2e.js ├── package-lock.json ├── package.json ├── src ├── commands │ ├── discover.js │ ├── rollback.js │ └── update.js ├── helpers.js ├── index.js └── types.jsdoc.js └── test ├── .eslintrc.json ├── commands ├── discover.test.js ├── rollback.test.js └── update.test.js ├── helpers.test.js ├── hookerror.test.js ├── index.test.js └── jest.setup.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@adobe/eslint-config-aio-lib-config", 3 | "settings": { 4 | "jsdoc": { 5 | "ignorePrivate": true 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.js text eol=lf 3 | *.ts text eol=lf 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Expected Behaviour 2 | 3 | ### Actual Behaviour 4 | 5 | ### Reproduce Scenario (including but not limited to) 6 | 7 | #### Steps to Reproduce 8 | 9 | #### Platform and Version 10 | 11 | #### Sample Code that illustrates the problem 12 | 13 | #### Logs taken while reproducing problem 14 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | 7 | ## Related Issue 8 | 9 | 10 | 11 | 12 | 13 | 14 | ## Motivation and Context 15 | 16 | 17 | 18 | ## How Has This Been Tested? 19 | 20 | 21 | 22 | 23 | 24 | ## Screenshots (if appropriate): 25 | 26 | ## Types of changes 27 | 28 | 29 | 30 | - [ ] Bug fix (non-breaking change which fixes an issue) 31 | - [ ] New feature (non-breaking change which adds functionality) 32 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 33 | 34 | ## Checklist: 35 | 36 | 37 | 38 | 39 | - [ ] I have signed the [Adobe Open Source CLA](http://opensource.adobe.com/cla.html). 40 | - [ ] My code follows the code style of this project. 41 | - [ ] My change requires a change to the documentation. 42 | - [ ] I have updated the documentation accordingly. 43 | - [ ] I have read the **CONTRIBUTING** document. 44 | - [ ] I have added tests to cover my changes. 45 | - [ ] All new and existing tests passed. 46 | -------------------------------------------------------------------------------- /.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 | - package-ecosystem: "npm" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/daily-install-cli.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do an install of the @adobe/aio-cli 2 | # This will surface any plugin/dependency issues when installing 3 | 4 | name: Daily - test install aio-cli 5 | 6 | on: 7 | schedule: 8 | # run daily at midnight 9 | - cron: '0 0 * * *' 10 | 11 | jobs: 12 | build: 13 | if: github.repository == 'adobe/aio-cli' 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | node: [14, 16] 18 | os: [ubuntu-latest] 19 | 20 | steps: 21 | - name: npm 22 | env: 23 | NPM_CONFIG_PREFIX: "~/.npm-global" 24 | run: | 25 | npm i -g @adobe/aio-cli 26 | - name: Slack Notification 27 | if: ${{ failure() }} 28 | uses: rtCamp/action-slack-notify@v2 29 | env: 30 | SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} 31 | SLACK_TITLE: 'Node version' 32 | SLACK_MESSAGE: ${{ matrix.node }} 33 | SLACK_COLOR: ${{ job.status == 'success' && 'good' || job.status == 'cancelled' && '#808080' || 'danger' }} 34 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | merge_group: 11 | branches: [ master ] 12 | 13 | jobs: 14 | build: 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | matrix: 18 | node-version: [18.x, 20.x] 19 | os: [ubuntu-latest, windows-latest] 20 | 21 | steps: 22 | - uses: actions/checkout@v2.4.2 23 | with: 24 | fetch-depth: 0 25 | - name: Use Node.js ${{ matrix.node-version }} 26 | uses: actions/setup-node@v3.4.1 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | - run: rm package-lock.json 30 | - run: npm i --package-lock --package-lock-only --legacy-peer-deps 31 | - run: npm ci --legacy-peer-deps 32 | - run: npm run build --if-present 33 | - run: npm test 34 | - name: run tests 35 | run: | 36 | ./bin/run --help 37 | ./bin/run plugins --core 38 | ./bin/run config --help 39 | ./bin/run console --help 40 | ./bin/run runtime --help 41 | - name: upload coverage 42 | if: success() 43 | uses: codecov/codecov-action@v4 44 | with: 45 | name: ${{ runner.os }} node.js ${{ matrix.node-version }} 46 | token: ${{ secrets.CODECOV_TOKEN }} 47 | fail_ci_if_error: false -------------------------------------------------------------------------------- /.github/workflows/npm-audit.yml: -------------------------------------------------------------------------------- 1 | name: Daily - npm audit 2 | 3 | on: 4 | schedule: 5 | # run daily at midnight 6 | - cron: '0 0 * * *' 7 | 8 | jobs: 9 | build: 10 | if: github.repository == 'adobe/aio-cli' 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | node: [16] 15 | os: [ubuntu-latest] 16 | 17 | steps: 18 | - name: Setup CLI 19 | uses: adobe/aio-cli-setup-action@1.0.1 20 | with: 21 | os: ${{ matrix.os }} 22 | - id: createandaudit 23 | name: create app and audit 24 | run: | 25 | mkdir testapp 26 | cd testapp 27 | aio app init -y --no-login --standalone-app 28 | npm audit --audit-level=high --json > auditoutput.txt 29 | - id: runaudit 30 | name: if audit had failed, run npm audit without json flag so we can see what is failing 31 | if: failure() && steps.createandaudit.outcome != 'success' 32 | run: | 33 | cd testapp 34 | npm audit 35 | - id: postslackmessage 36 | name: Slack Notification 37 | if: ${{ failure() }} 38 | uses: rtCamp/action-slack-notify@v2 39 | env: 40 | SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} 41 | SLACK_TITLE: 'npm audit' 42 | SLACK_MESSAGE: ${{ env.issueurl || 'Vulnerabilities found' }} 43 | SLACK_COLOR: ${{ job.status == 'success' && 'good' || job.status == 'cancelled' && '#808080' || 'danger' }} 44 | -------------------------------------------------------------------------------- /.github/workflows/on-push-publish-to-npm.yml: -------------------------------------------------------------------------------- 1 | name: on-push-publish-to-npm 2 | on: 3 | push: 4 | branches: 5 | - master # Change this if not your default branch 6 | paths: 7 | - 'package.json' 8 | jobs: 9 | publish: 10 | if: github.repository == 'adobe/aio-cli' 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions/setup-node@v1 15 | with: 16 | node-version: 18 17 | - run: npm install 18 | - run: npm test 19 | - uses: JS-DevTools/npm-publish@v1 20 | with: 21 | token: ${{ secrets.ADOBE_BOT_NPM_TOKEN }} 22 | access: 'public' 23 | -------------------------------------------------------------------------------- /.github/workflows/prerelease-main.yml: -------------------------------------------------------------------------------- 1 | name: publish-prerelease (main branch) 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | pre-release-tag: 6 | description: 'The pre-release tag use in the version' 7 | required: false 8 | default: 'pre' 9 | dist-tag: 10 | description: 'The dist-tag use' 11 | required: false 12 | default: 'latest' 13 | dependencies-dist-tag: 14 | description: 'The dist-tag use for dependencies' 15 | required: false 16 | default: 'next' 17 | package-name: 18 | description: 'npm package name that pre-release testers will `npm install`' 19 | required: false 20 | default: '@adobe/aio-cli-next' 21 | jobs: 22 | checkout: 23 | name: checkout 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v2 27 | - run: | 28 | git config user.name github-actions 29 | git config user.email github-actions@github.com 30 | git branch 31 | - uses: actions/setup-node@v1 32 | with: 33 | node-version: 18 34 | - run: | 35 | npm pkg set name=${{ github.event.inputs.package-name }} 36 | npm pkg set bin.aio-next="./bin/run" 37 | npm pkg delete bin.aio 38 | npm pkg set oclif.bin="aio-next" 39 | - name: Update your package.json with an npm pre-release version 40 | id: pre-release-version 41 | uses: adobe/update-prerelease-npm-version@v1.0.2 42 | with: 43 | pre-release-tag: ${{ github.event.inputs.pre-release-tag }} 44 | dependencies-to-update-version-tag: ${{ github.event.inputs.dependencies-dist-tag }} 45 | - run: echo pre-release-version - ${{ steps.pre-release-version.outputs.pre-release-version }} 46 | - run: | 47 | npm install 48 | npm test 49 | - uses: JS-DevTools/npm-publish@v1 50 | with: 51 | token: ${{ secrets.ADOBE_BOT_NPM_TOKEN }} 52 | tag: ${{ github.event.inputs.dist-tag }} 53 | access: 'public' 54 | -------------------------------------------------------------------------------- /.github/workflows/prerelease.yml: -------------------------------------------------------------------------------- 1 | name: publish-prerelease (next versions of dependencies) 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | pre-release-tag: 6 | description: 'The pre-release tag use in the version' 7 | required: false 8 | default: 'pre' 9 | dist-tag: 10 | description: 'The dist-tag use' 11 | required: false 12 | default: 'latest' 13 | dependencies-dist-tag: 14 | description: 'The dist-tag use for dependencies' 15 | required: false 16 | default: 'next' 17 | dependencies-to-update: 18 | description: 'csv of dependencies to update with the dist-tag' 19 | required: false 20 | default: '@adobe/aio-cli-plugin-app,@adobe/aio-cli-plugin-auth,@adobe/aio-cli-plugin-certificate,@adobe/aio-cli-plugin-config,@adobe/aio-cli-plugin-console,@adobe/aio-cli-plugin-events,@adobe/aio-cli-plugin-info,@adobe/aio-cli-plugin-runtime,@adobe/aio-cli-plugin-telemetry,@adobe/aio-cli-plugin-app-templates' 21 | package-name: 22 | description: 'npm package name that pre-release testers will `npm install`' 23 | required: false 24 | default: '@adobe/aio-cli-next' 25 | jobs: 26 | checkout: 27 | name: checkout 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@v2 31 | - run: | 32 | git config user.name github-actions 33 | git config user.email github-actions@github.com 34 | git branch 35 | - uses: actions/setup-node@v1 36 | with: 37 | node-version: 18 38 | - run: | 39 | npm pkg set name=${{ github.event.inputs.package-name }} 40 | npm pkg set bin.aio-next="./bin/run" 41 | npm pkg delete bin.aio 42 | npm pkg set oclif.bin="aio-next" 43 | - name: Update your package.json with an npm pre-release version 44 | id: pre-release-version 45 | uses: adobe/update-prerelease-npm-version@v1.0.2 46 | with: 47 | pre-release-tag: ${{ github.event.inputs.pre-release-tag }} 48 | dependencies-to-update: ${{ github.event.inputs.dependencies-to-update }} 49 | dependencies-to-update-version-tag: ${{ github.event.inputs.dependencies-dist-tag }} 50 | - run: echo pre-release-version - ${{ steps.pre-release-version.outputs.pre-release-version }} 51 | - run: | 52 | npm install 53 | npm test 54 | - uses: JS-DevTools/npm-publish@v1 55 | with: 56 | token: ${{ secrets.ADOBE_BOT_NPM_TOKEN }} 57 | tag: ${{ github.event.inputs.dist-tag }} 58 | access: 'public' 59 | -------------------------------------------------------------------------------- /.github/workflows/version-bump-publish.yml: -------------------------------------------------------------------------------- 1 | name: version-bump-publish 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | level: 6 | description: ' | major | minor | patch | premajor | preminor | prepatch | prerelease' 7 | required: true 8 | default: 'patch' 9 | tag: 10 | description: 'The tag to publish to.' 11 | required: false 12 | default: 'latest' 13 | jobs: 14 | checkout: 15 | if: github.repository == 'adobe/aio-cli' 16 | name: checkout 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v2 20 | - run: | 21 | git config user.name github-actions 22 | git config user.email github-actions@github.com 23 | - uses: actions/setup-node@v1 24 | with: 25 | node-version: 18 26 | - run: | 27 | npm install 28 | npm test 29 | - name: bump and pub 30 | if: ${{ github.event.inputs.level != '' }} 31 | run: | 32 | npm version ${{ github.event.inputs.level }} 33 | git push 34 | - uses: JS-DevTools/npm-publish@v1 35 | with: 36 | token: ${{ secrets.ADOBE_BOT_NPM_TOKEN }} 37 | tag: ${{ github.event.inputs.tag }} 38 | access: 'public' 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *-debug.log 2 | *-error.log 3 | oclif.manifest.json 4 | /.nyc_output 5 | /dist 6 | /lib 7 | /tmp 8 | /yarn.lock 9 | node_modules 10 | .DS_Store 11 | /test-results.xml 12 | 13 | .vscode/ 14 | coverage/ 15 | 16 | junit.xml 17 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=true 2 | tag-version-prefix="" -------------------------------------------------------------------------------- /.tidelift.yml: -------------------------------------------------------------------------------- 1 | # relax the tidelift configuration, so that issues that fail under normal circumstances 2 | # will only create a warning 3 | tests: 4 | deprecated: warn 5 | vulnerable: warn 6 | broken: warn 7 | 8 | # keep the list of disallowed licenses, as suggested by Adobe Legal 9 | # any transitive dependency with these licenses will fail the build 10 | 11 | licensing: 12 | disallowed: 13 | - AGPL-1.0-only 14 | - AGPL-1.0-or-later 15 | - AGPL-3.0-only 16 | - AGPL-3.0-or-later 17 | - AGPL-1.0 18 | - AGPL-3.0 19 | - CC-BY-NC-ND-1.0 20 | - CC-BY-NC-ND-2.0 21 | - CC-BY-NC-ND-2.5 22 | - CC-BY-NC-ND-3.0 23 | - CC-BY-NC-ND-4.0 24 | - CC-BY-NC-SA-1.0 25 | - CC-BY-NC-SA-2.0 26 | - CC-BY-NC-SA-2.5 27 | - CC-BY-NC-SA-3.0 28 | - CC-BY-NC-SA-4.0 29 | - CC-BY-SA-1.0 30 | - CC-BY-SA-2.0 31 | - CC-BY-SA-2.5 32 | - CC-BY-SA-3.0 33 | - CC-BY-SA-4.0 34 | - GPL-1.0-only 35 | - GPL-1.0-or-later 36 | - GPL-2.0-only 37 | - GPL-2.0-or-later 38 | - GPL-3.0-only 39 | - GPL-3.0-or-later 40 | - SSPL-1.0 41 | - Sleepycat 42 | - Facebook 43 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Adobe Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at Grp-opensourceoffice@adobe.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for choosing to contribute! 4 | 5 | The following are a set of guidelines to follow when contributing to this project. 6 | 7 | ## Code Of Conduct 8 | 9 | This project adheres to the Adobe [code of conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to [Grp-opensourceoffice@adobe.com](mailto:Grp-opensourceoffice@adobe.com). 10 | 11 | ## Contributor License Agreement 12 | 13 | All third-party contributions to this project must be accompanied by a signed contributor license agreement. This gives Adobe permission to redistribute your contributions as part of the project. [Sign our CLA](http://opensource.adobe.com/cla.html). You only need to submit an Adobe CLA one time, so if you have submitted one previously, you are good to go! 14 | 15 | ## Code Reviews 16 | 17 | All submissions should come in the form of pull requests and need to be reviewed by project committers. Read [GitHub's pull request documentation](https://help.github.com/articles/about-pull-requests/) for more information on sending pull requests. 18 | 19 | Lastly, please follow the [pull request template](PULL_REQUEST_TEMPLATE.md) when submitting a pull request! 20 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | Copyright 2018 Adobe 2 | 3 | Adobe holds the copyright for all the files found in this repository. 4 | 5 | See the LICENSE file for licensing information. 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | ## 1. Update your plugins 2 | 3 | ``` 4 | rm -rf node_modules 5 | npm install 6 | npm outdated @adobe/aio-cli-plugin-console @adobe/aio-cli-plugin-jwt-auth @adobe/aio-cli-plugin-config @adobe/aio-cli-plugin-runtime @adobe/aio-cli-plugin-certificate @adobe/aio-cli-plugin-auth @adobe/aio-cli-plugin-app 7 | ``` 8 | 9 | If there is a `major` version update in any of the plugins, only update the `major` version of the CLI. 10 | Else if there is a `minor` version update in any of the plugins, only update the `minor` version of the CLI. 11 | Else if there is a `patch` version update in any of the plugins, only update the `patch` version of the CLI. 12 | 13 | Update the updated module(s). For example, if it was the console plugin: 14 | ``` 15 | npm install @adobe/aio-cli-plugin-console@latest --save 16 | ``` 17 | 18 | ## 2. Test with the updated modules (if any) 19 | 20 | ``` 21 | npm test 22 | ``` 23 | 24 | ## 3. Increment a version 25 | 26 | ``` 27 | npm --no-git-tag-version version [major | minor | patch] 28 | # get the package.json version in a variable 29 | export PKG_VER=`node -e "console.log(require('./package.json').version)"` 30 | ``` 31 | ## 4. Commit the changed files 32 | ``` 33 | git commit -m "Incremented version to $PKG_VER" package.json package-lock.json README.md 34 | ``` 35 | 36 | ## 5. Tag a version 37 | 38 | ``` 39 | git tag $PKG_VER 40 | ``` 41 | 42 | ## 6. Push version and tag 43 | 44 | ``` 45 | git push origin master 46 | git push origin $PKG_VER 47 | ``` 48 | 49 | ## 7. publish to npm 50 | 51 | ``` 52 | npm publish 53 | ``` 54 | 55 | ## 8. prepare the next pre-release 56 | 57 | ``` 58 | npm --no-git-tag-version version prerelease --preid=dev 59 | git add . 60 | git commit -m "bumped master prerelease" 61 | git push origin master 62 | ``` 63 | 64 | -------------------------------------------------------------------------------- /bin/gen-health-table.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable node/no-extraneous-require */ 2 | /* eslint-disable node/no-unpublished-require */ 3 | /* 4 | Copyright 2020 Adobe. All rights reserved. 5 | This file is licensed to you under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. You may obtain a copy 7 | of the License at http://www.apache.org/licenses/LICENSE-2.0 8 | Unless required by applicable law or agreed to in writing, software distributed under 9 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 10 | OF ANY KIND, either express or implied. See the License for the specific language 11 | governing permissions and limitations under the License. 12 | */ 13 | 14 | const fs = require('fs-extra') 15 | const path = require('node:path') 16 | const ReadmeGenerator = require('oclif/lib/readme-generator').default 17 | const READMEpath = path.resolve(path.join(__dirname, '..', 'README.md')) 18 | 19 | /** @private */ 20 | function genRow (pkName) { 21 | return `| [@${pkName}](https://github.com/${pkName}) ` + 22 | `| [![Version](https://img.shields.io/npm/v/@${pkName}.svg)](https://npmjs.org/package/@${pkName})` + 23 | `| [![Downloads/week](https://img.shields.io/npm/dw/@${pkName}.svg)](https://npmjs.org/package/@${pkName})` + 24 | `| [![Node.js CI](https://github.com/${pkName}/actions/workflows/node.js.yml/badge.svg)](https://github.com/${pkName}/actions/workflows/node.js.yml)` + 25 | `| [![Codecov Coverage](https://img.shields.io/codecov/c/github/${pkName}/master.svg?style=flat-square)](https://codecov.io/gh/${pkName}/)` + 26 | `| [![Github Issues](https://img.shields.io/github/issues/${pkName}.svg)](https://github.com/${pkName}/issues)` + 27 | `| [![Github Pull Requests](https://img.shields.io/github/issues-pr/${pkName}.svg)](https://github.com/${pkName}/pulls)|` 28 | } 29 | 30 | /** @private */ 31 | function replaceTag (readme, tag, body) { 32 | const oclDev = new ReadmeGenerator({}, { readmePath: READMEpath }) 33 | return oclDev.replaceTag(readme, tag, body) 34 | 35 | // this is the code that oclif/dev-cli/readme runs: 36 | // if (readme.includes(``)) { 37 | // if (readme.includes(``)) { 38 | // readme = readme.replace(new RegExp(`(.|\n)*`, 'm'), ``) 39 | // } 40 | // console.log(`replacing in README.md`) 41 | // } 42 | // return readme.replace(``, `\n${body}\n`) 43 | } 44 | 45 | // load package.json and get @adobe dependencies 46 | // only adobe cli plugins, and remove the '@' 47 | const pkjson = fs.readJSONSync('package.json') 48 | const adobeDeps = pkjson.oclif.plugins 49 | .filter(item => item.indexOf('@adobe/aio-cli-plugin') === 0) 50 | .concat([ // additional repos to show in health table 51 | '@adobe/generator-aio-app', 52 | '@adobe/generator-aio-console' 53 | ]) 54 | .map(item => item.substring(1)) 55 | 56 | // add the aio-cli itself ... 57 | adobeDeps.unshift(pkjson.name.substring(1)) 58 | 59 | // prime tableData with headers, and hrs 60 | const tableData = ['| Module | Version | Downloads | Build Status | Coverage | Issues | Pull Requests |', 61 | '|---|---|---|---|---|---|---|'] 62 | 63 | // add a row for each item 64 | adobeDeps.forEach(item => { tableData.push(genRow(item)) }) 65 | 66 | // replace the text in README 67 | const readme = fs.readFileSync(READMEpath, 'utf8') 68 | fs.writeFileSync(READMEpath, replaceTag(readme, 'health', tableData.join('\n'))) 69 | -------------------------------------------------------------------------------- /bin/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* 4 | Copyright 2018 Adobe. All rights reserved. 5 | This file is licensed to you under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. You may obtain a copy 7 | of the License at http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software distributed under 10 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 11 | OF ANY KIND, either express or implied. See the License for the specific language 12 | governing permissions and limitations under the License. 13 | */ 14 | 15 | require('../src/').run() 16 | .then(require('@oclif/core/flush')) 17 | .catch(require('@oclif/core/handle')) 18 | -------------------------------------------------------------------------------- /bin/run.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | rem Copyright 2018 Adobe. All rights reserved. 4 | rem This file is licensed to you under the Apache License, Version 2.0 (the "License"); 5 | rem you may not use this file except in compliance with the License. You may obtain a copy 6 | rem of the License at http://www.apache.org/licenses/LICENSE-2.0 7 | rem 8 | rem Unless required by applicable law or agreed to in writing, software distributed under 9 | rem the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 10 | rem OF ANY KIND, either express or implied. See the License for the specific language 11 | rem governing permissions and limitations under the License. 12 | 13 | node "%~dp0\run" %* 14 | -------------------------------------------------------------------------------- /e2e/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "node/no-unpublished-require": 0 4 | } 5 | } -------------------------------------------------------------------------------- /e2e/e2e.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe. All rights reserved. 3 | This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. You may obtain a copy 5 | of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | Unless required by applicable law or agreed to in writing, software distributed under 7 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 8 | OF ANY KIND, either express or implied. See the License for the specific language 9 | governing permissions and limitations under the License. 10 | */ 11 | 12 | const execa = require('execa') 13 | const fs = jest.requireActual('fs') 14 | const util = require('util') 15 | const fse = { 16 | mkdir: util.promisify(fs.mkdir), 17 | rm: util.promisify(fs.rm) 18 | } 19 | 20 | jest.setTimeout(120000) 21 | 22 | test('cli init test', async () => { 23 | const testFolder = 'e2e_test_run' 24 | 25 | await fse.rm(testFolder, { recursive: true, force: true }) 26 | await fse.mkdir(testFolder) 27 | process.chdir(testFolder) 28 | 29 | await execa('node', ['../bin/run', 'app', 'init', '-y', '--no-login', '--no-extensions'], { stderr: 'inherit' }) 30 | 31 | const files = [ 32 | 'actions/generic/index.js', 33 | 'actions/publish-events/index.js', 34 | 'actions/utils.js', 35 | 'test/generic.test.js', 36 | 'test/publish-events.test.js', 37 | 'test/utils.test.js', 38 | 'jest.setup.js', 39 | 'web-src/src/index.js', 40 | 'web-src/src/exc-runtime.js', 41 | 'web-src/index.html', 42 | '.babelrc', 43 | '.env', 44 | 'package.json', 45 | 'README.md', 46 | 'app.config.yaml' 47 | ] 48 | 49 | const missingFiles = [] 50 | 51 | files.forEach(file => { 52 | const fileExists = fs.existsSync(file) 53 | if (!fileExists) { 54 | console.error(`File ${file} does not exist.`) 55 | missingFiles.push(file) 56 | } 57 | }) 58 | 59 | expect(missingFiles).toEqual([]) 60 | 61 | process.chdir('..') 62 | await fse.rm(testFolder, { recursive: true, force: true }) 63 | }) 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@adobe/aio-cli", 3 | "description": "Adobe I/O Extensible CLI\n\n******* *******\n****** ******\n***** *****\n**** * ****\n*** *** ***\n** ***** **\n* ** *\n", 4 | "version": "10.3.4", 5 | "author": "Adobe Inc.", 6 | "bin": { 7 | "aio": "bin/run" 8 | }, 9 | "bugs": "https://github.com/adobe/aio-cli/issues", 10 | "dependencies": { 11 | "@adobe/aio-cli-plugin-app": "^13", 12 | "@adobe/aio-cli-plugin-app-dev": "^2", 13 | "@adobe/aio-cli-plugin-app-storage": "^1", 14 | "@adobe/aio-cli-plugin-app-templates": "^2", 15 | "@adobe/aio-cli-plugin-auth": "^4", 16 | "@adobe/aio-cli-plugin-certificate": "^2", 17 | "@adobe/aio-cli-plugin-config": "^5", 18 | "@adobe/aio-cli-plugin-console": "^5", 19 | "@adobe/aio-cli-plugin-events": "^4", 20 | "@adobe/aio-cli-plugin-info": "^4", 21 | "@adobe/aio-cli-plugin-runtime": "^7", 22 | "@adobe/aio-cli-plugin-telemetry": "^2", 23 | "@oclif/core": "2.11.7", 24 | "@oclif/plugin-autocomplete": "^3.0.5", 25 | "@oclif/plugin-help": "^6.0.12", 26 | "@oclif/plugin-not-found": "^2.3.26", 27 | "@oclif/plugin-plugins": "^5", 28 | "@oclif/plugin-warn-if-update-available": "^3", 29 | "@types/jest": "^29.5.2", 30 | "chalk": "^4.1.2", 31 | "inquirer": "^8.2.3", 32 | "node-fetch": "^2.x", 33 | "ora": "^5.4.1", 34 | "semver": "^7.5.2" 35 | }, 36 | "overrides": { 37 | "node-fetch@^2.x": { 38 | "whatwg-url": "14.x" 39 | } 40 | }, 41 | "devDependencies": { 42 | "@adobe/eslint-config-aio-lib-config": "^4.0.0", 43 | "acorn": "^8.8.2", 44 | "babel-runtime": "^6.26.0", 45 | "eslint": "^8.57.1", 46 | "eslint-config-standard": "^17.1.0", 47 | "eslint-plugin-import": "^2.31.0", 48 | "eslint-plugin-jest": "^27.9.0", 49 | "eslint-plugin-jsdoc": "^48.11.0", 50 | "eslint-plugin-n": "^15.7.0", 51 | "eslint-plugin-node": "^11.1.0", 52 | "eslint-plugin-promise": "^6.6.0", 53 | "execa": "^5.1.1", 54 | "jest": "^29.0.1", 55 | "jest-fetch-mock": "^3.0.0", 56 | "jest-junit": "^16.0.0", 57 | "jest-plugin-fs": "^2.9.0", 58 | "oclif": "^4", 59 | "rimraf": "^5.0.7", 60 | "stdout-stderr": "^0.1.9", 61 | "typescript": "^5.3.3" 62 | }, 63 | "engines": { 64 | "node": "^18 || ^20 || ^22" 65 | }, 66 | "files": [ 67 | "/bin", 68 | "/src", 69 | "/oclif.manifest.json", 70 | "/package-lock.json" 71 | ], 72 | "homepage": "https://github.com/adobe/aio-cli", 73 | "keywords": [ 74 | "oclif" 75 | ], 76 | "license": "Apache-2.0", 77 | "main": "src/index.js", 78 | "oclif": { 79 | "additionalHelpFlags": [ 80 | "-h" 81 | ], 82 | "additionalVersionFlags": [ 83 | "-v" 84 | ], 85 | "topicSeparator": " ", 86 | "commands": "./src/commands", 87 | "bin": "aio", 88 | "plugins": [ 89 | "@oclif/plugin-help", 90 | "@oclif/plugin-plugins", 91 | "@oclif/plugin-autocomplete", 92 | "@oclif/plugin-not-found", 93 | "@oclif/plugin-warn-if-update-available", 94 | "@adobe/aio-cli-plugin-config", 95 | "@adobe/aio-cli-plugin-console", 96 | "@adobe/aio-cli-plugin-runtime", 97 | "@adobe/aio-cli-plugin-app", 98 | "@adobe/aio-cli-plugin-app-dev", 99 | "@adobe/aio-cli-plugin-app-storage", 100 | "@adobe/aio-cli-plugin-app-templates", 101 | "@adobe/aio-cli-plugin-auth", 102 | "@adobe/aio-cli-plugin-certificate", 103 | "@adobe/aio-cli-plugin-info", 104 | "@adobe/aio-cli-plugin-events", 105 | "@adobe/aio-cli-plugin-telemetry" 106 | ], 107 | "warn-if-update-available": { 108 | "timeoutInDays": 7, 109 | "message": "<%= config.name %> update available from <%= chalk.yellowBright(config.version) %> to <%= chalk.yellowBright(latest) %>.\nRun <%= chalk.greenBright('npm install -g ' + config.name) %> to update." 110 | }, 111 | "repositoryPrefix": "<%- repo %>/blob/<%- version %>/<%- commandPath %>" 112 | }, 113 | "jest": { 114 | "collectCoverage": true, 115 | "collectCoverageFrom": [ 116 | "src/**/*.js" 117 | ], 118 | "coverageThreshold": { 119 | "global": { 120 | "branches": 100, 121 | "lines": 100, 122 | "statements": 100, 123 | "functions": 100 124 | } 125 | }, 126 | "testPathIgnorePatterns": [ 127 | "/test/jest.setup.js" 128 | ], 129 | "reporters": [ 130 | "default", 131 | "jest-junit" 132 | ], 133 | "testEnvironment": "node", 134 | "setupFilesAfterEnv": [ 135 | "/test/jest.setup.js" 136 | ] 137 | }, 138 | "repository": "adobe/aio-cli", 139 | "scripts": { 140 | "postpack": "rimraf -I oclif.manifest.json", 141 | "test": "jest --ci", 142 | "posttest": "npm run lint", 143 | "lint": "eslint src test e2e", 144 | "gen-health": "node bin/gen-health-table.js", 145 | "prepack": "oclif manifest && oclif readme", 146 | "version": "oclif readme && git add README.md", 147 | "e2e": "jest --collectCoverage=false --testRegex './e2e/e2e.js'" 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/commands/discover.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Adobe Inc. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | const { Command, Flags, ux } = require('@oclif/core') 14 | const fetch = require('node-fetch') 15 | const inquirer = require('inquirer') 16 | const { sortValues } = require('../helpers') 17 | 18 | /* 19 | This is how cordova does it: 20 | https://npmsearch.com/query?fields=name,keywords,license,description,author,modified,homepage,version,rating&q=keywords:%22ecosystem:cordova%22&sort=rating:desc&size=500&start=0 21 | 22 | future: use keywords ecosytem:aio-cli-plugin 23 | */ 24 | 25 | class DiscoCommand extends Command { 26 | async _install (plugins) { 27 | // get installed plugins 28 | const installedPlugins = this.config.commands.map(elem => { 29 | return elem.pluginName 30 | }) 31 | 32 | const inqChoices = plugins 33 | .filter(elem => { // remove any installed plugins from the list 34 | return !installedPlugins.includes(elem.name) 35 | }) 36 | .map(elem => { // map to expected inquirer format 37 | return { 38 | name: `${elem.name}@${elem.version}`, 39 | value: elem.name 40 | } 41 | }) 42 | 43 | if (!(inqChoices.length)) { 44 | this.log('All available plugins appear to be installed.') 45 | return [] 46 | } 47 | 48 | const response = await inquirer.prompt([{ 49 | name: 'plugins', 50 | message: 'Select plugins to install', 51 | type: 'checkbox', 52 | choices: inqChoices 53 | }]) 54 | 55 | // install the plugins in sequence 56 | for (const plugin of response.plugins) { 57 | await this.config.runCommand('plugins:install', [plugin]) 58 | } 59 | 60 | return response.plugins 61 | } 62 | 63 | async _list (plugins) { 64 | const options = { 65 | year: 'numeric', 66 | month: 'long', 67 | day: 'numeric' 68 | } 69 | 70 | const columns = { 71 | name: { 72 | width: 10, 73 | get: row => `${row.name}` 74 | }, 75 | version: { 76 | minWidth: 10, 77 | get: row => `${row.version}` 78 | }, 79 | description: { 80 | get: row => `${row.description}` 81 | }, 82 | published: { 83 | get: row => `${new Date(row.date).toLocaleDateString('en', options)}` 84 | } 85 | } 86 | // skip ones that aren't from us 87 | ux.table(plugins, columns) 88 | } 89 | 90 | async run () { 91 | const { flags } = await this.parse(DiscoCommand) 92 | 93 | try { 94 | const url = 'https://registry.npmjs.org/-/v1/search?text=aio-cli-plugin' 95 | const response = await fetch(url) 96 | const json = await response.json() 97 | 98 | // ours only, this could become a flag, searching for oclif-plugin reveals some more 99 | const adobeOnly = json.objects 100 | .map(e => e.package) 101 | .filter(elem => elem.name && elem.name.startsWith('@adobe/aio-cli-plugin')) 102 | 103 | sortValues(adobeOnly, { 104 | descending: flags['sort-order'] !== 'asc', 105 | field: flags['sort-field'] 106 | }) 107 | 108 | if (flags.install) { 109 | return this._install(adobeOnly) 110 | } else { 111 | return this._list(adobeOnly) 112 | } 113 | } catch (error) { 114 | this.error(error.toString()) 115 | } 116 | } 117 | } 118 | 119 | DiscoCommand.description = `Discover plugins to install 120 | Lists only plugins with prefix '@adobe/aio-cli-plugin' 121 | To install a plugin, run 'aio plugins install NAME' 122 | ` 123 | 124 | DiscoCommand.aliases = ['plugins:discover'] 125 | DiscoCommand.flags = { 126 | install: Flags.boolean({ 127 | char: 'i', 128 | default: false, 129 | description: 'interactive install mode' 130 | }), 131 | 'sort-field': Flags.string({ 132 | char: 'f', 133 | default: 'date', 134 | options: ['date', 'name'], 135 | description: 'which column to sort, use the sort-order flag to specify sort direction' 136 | }), 137 | 'sort-order': Flags.string({ 138 | char: 'o', 139 | default: 'desc', 140 | options: ['asc', 'desc'], 141 | description: 'sort order for a column, use the sort-field flag to specify which column to sort' 142 | }) 143 | } 144 | 145 | module.exports = DiscoCommand 146 | -------------------------------------------------------------------------------- /src/commands/rollback.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe Inc. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | const { Command, Flags, ux } = require('@oclif/core') 14 | const inquirer = require('inquirer') 15 | const { prompt, hideNPMWarnings } = require('../helpers') 16 | 17 | require('../types.jsdoc') // get types 18 | /* global InstalledPlugin */ 19 | 20 | class RollbackCommand extends Command { 21 | /** 22 | * List the plugins that are installed. 23 | * 24 | * @param {Array} plugins the installed plugins 25 | */ 26 | async __list (plugins) { 27 | const columns = { 28 | plugin: { 29 | width: 10, 30 | get: row => `${row.name}` 31 | }, 32 | version: { 33 | minWidth: 10, 34 | get: row => `${row.version}` 35 | } 36 | } 37 | 38 | ux.table(plugins, columns) 39 | } 40 | 41 | /** 42 | * Clear the installed plugins (uninstall all) 43 | * 44 | * @private 45 | * @param {Array} plugins the installed plugins 46 | * @param {boolean} needsConfirm true to show confirmation prompt 47 | */ 48 | async __clear (plugins, needsConfirm, verbose) { 49 | await this.__list(plugins) 50 | let _doClear = true 51 | 52 | this.log() // newline 53 | 54 | if (needsConfirm) { 55 | _doClear = await prompt(`Uninstall ${plugins.length} plugin(s)?`) 56 | } 57 | 58 | if (_doClear) { 59 | if (!verbose) { 60 | // Intercept the stderr stream to hide npm warnings 61 | hideNPMWarnings() 62 | } 63 | 64 | // uninstall the plugins in sequence 65 | for (const plugin of plugins) { 66 | await this.config.runCommand('plugins:uninstall', [plugin.name]) 67 | } 68 | } 69 | } 70 | 71 | /** 72 | * Clear the installed plugins, with an interactive uninstall. 73 | * 74 | * @private 75 | * @param {Array} plugins the plugins to update 76 | */ 77 | async __interactiveClear (plugins, verbose) { 78 | const inqChoices = plugins 79 | .map(plugin => { // map to expected inquirer format 80 | return { 81 | name: `${plugin.name}@${plugin.version}`, 82 | value: plugin.name 83 | } 84 | }) 85 | 86 | const response = await inquirer.prompt([{ 87 | name: 'plugins', 88 | message: 'Select plugins to uninstall', 89 | type: 'checkbox', 90 | choices: inqChoices 91 | }]) 92 | 93 | if (!verbose) { 94 | // Intercept the stderr stream to hide npm warnings 95 | hideNPMWarnings() 96 | } 97 | 98 | // uninstall the plugins in sequence 99 | for (const plugin of response.plugins) { 100 | await this.config.runCommand('plugins:uninstall', [plugin]) 101 | } 102 | } 103 | 104 | /** 105 | * Command entry point 106 | * 107 | * @returns {Promise} promise that lists/interactive clear/clears the installed updates 108 | */ 109 | async run () { 110 | const { flags } = await this.parse(RollbackCommand) 111 | const plugins = this.config.plugins.filter(p => p.type === 'user') 112 | 113 | if (plugins.length === 0) { 114 | this.log('no installed plugins to clear') 115 | return 116 | } 117 | 118 | if (flags.list) { 119 | return this.__list(plugins) 120 | } else if (flags.interactive) { 121 | return this.__interactiveClear(plugins, flags.verbose) 122 | } else { 123 | return this.__clear(plugins, flags.confirm, flags.verbose) 124 | } 125 | } 126 | } 127 | 128 | RollbackCommand.description = 'Clears all installed plugins.' 129 | 130 | RollbackCommand.flags = { 131 | interactive: Flags.boolean({ 132 | char: 'i', 133 | default: false, 134 | description: 'interactive clear mode' 135 | }), 136 | list: Flags.boolean({ 137 | char: 'l', 138 | default: false, 139 | description: 'list plugins that will be cleared' 140 | }), 141 | confirm: Flags.boolean({ 142 | char: 'c', 143 | default: true, 144 | description: 'confirmation needed for clear (defaults to true)', 145 | allowNo: true 146 | }), 147 | verbose: Flags.boolean({ 148 | char: 'v', 149 | default: false, 150 | description: 'Verbose output' 151 | }) 152 | } 153 | 154 | module.exports = RollbackCommand 155 | -------------------------------------------------------------------------------- /src/commands/update.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe Inc. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | const { Command, Flags, ux } = require('@oclif/core') 14 | const inquirer = require('inquirer') 15 | const chalk = require('chalk') 16 | const ora = require('ora') 17 | const semver = require('semver') 18 | const { prompt, getNpmLatestVersion, getNpmLocalVersion, hideNPMWarnings } = require('../helpers') 19 | 20 | require('../types.jsdoc') // get types 21 | /* global ToUpdatePlugin */ 22 | 23 | class UpdateCommand extends Command { 24 | /** 25 | * List the plugins that have updates. 26 | * 27 | * @param {Array} plugins the plugins to update 28 | * @param {object} colOptions the column options 29 | * @param {string} colOptions.col1 the heading text for the first column 30 | * @param {string} colOptions.col2 the heading text for the second column 31 | * @param {string} colOptions.col3 the heading text for the third column 32 | */ 33 | async __list (plugins, { col1 = 'user plugin updates available', col2 = 'current', col3 = 'latest' } = {}) { 34 | const columns = { 35 | [col1]: { 36 | width: 10, 37 | get: row => row.asterisk ? `${row.name}${chalk.yellow('*')}` : `${row.name}` 38 | }, 39 | [col2]: { 40 | minWidth: 10, 41 | get: row => `${row.currentVersion}` 42 | }, 43 | [col3]: { 44 | get: row => `${row.latestVersion}` 45 | } 46 | } 47 | 48 | ux.table(plugins, columns) 49 | } 50 | 51 | /** 52 | * Install the plugins that have updates. 53 | * 54 | * @private 55 | * @param {Array} plugins the plugins to update 56 | * @param {boolean} needsConfirm true to show confirmation prompt 57 | */ 58 | async __install (plugins, needsConfirm, verbose) { 59 | await this.__list(plugins) 60 | let _doUpdate = true 61 | 62 | this.log() // newline 63 | 64 | if (needsConfirm) { 65 | _doUpdate = await prompt(`Update ${plugins.length} user plugin(s)?`) 66 | } 67 | if (_doUpdate) { 68 | if (!verbose) { 69 | // Intercept the stderr stream to hide npm warnings 70 | hideNPMWarnings() 71 | } 72 | 73 | // install the plugins in sequence 74 | for (const plugin of plugins) { 75 | await this.config.runCommand('plugins:install', [plugin.name]) 76 | } 77 | } 78 | } 79 | 80 | /** 81 | * Install the plugins that have updates, with an interactive install. 82 | * 83 | * @private 84 | * @param {Array} plugins the plugins to update 85 | */ 86 | async __interactiveInstall (plugins, verbose) { 87 | const inqChoices = plugins 88 | .map(plugin => { // map to expected inquirer format 89 | return { 90 | name: `${plugin.name}@${plugin.latestVersion}`, 91 | value: plugin.name 92 | } 93 | }) 94 | 95 | const response = await inquirer.prompt([{ 96 | name: 'plugins', 97 | message: 'Select plugins to update', 98 | type: 'checkbox', 99 | choices: inqChoices 100 | }]) 101 | 102 | if (!verbose) { 103 | // Intercept the stderr stream to hide npm warnings 104 | hideNPMWarnings() 105 | } 106 | 107 | // install the plugins in sequence 108 | for (const plugin of response.plugins) { 109 | await this.config.runCommand('plugins:install', [plugin]) 110 | } 111 | } 112 | 113 | __coreUpdateable (plugin) { 114 | const corePlugins = this.config.pjson.oclif.plugins 115 | return !( 116 | corePlugins.includes(plugin.name) && 117 | !(plugin.name.startsWith('@adobe/')) 118 | ) 119 | } 120 | 121 | /** 122 | * Process the plugins, determine if they need updates or warnings. 123 | * 124 | * @returns {Array} the process plugins 125 | */ 126 | async __processPlugins () { 127 | const corePlugins = this.config.pjson.oclif.plugins 128 | const plugins = [] 129 | 130 | // Filter installed plugins: 131 | // - remove any plugin that is in core, that is not from the @adobe namespace 132 | // These will not be updateable for compatibility reasons 133 | const installedPlugins = this.config.plugins 134 | .filter(p => this.__coreUpdateable(p)) 135 | // remove the cli itself from the plugin list 136 | .filter(plugin => plugin.name !== this.config.pjson.name) 137 | 138 | for (const plugin of installedPlugins) { 139 | const { type, name, version: currentVersion } = plugin 140 | const latestVersion = await getNpmLatestVersion(name) 141 | if (!latestVersion) { 142 | continue 143 | } 144 | 145 | let coreVersion = (type === 'core') ? currentVersion : null 146 | const needsUpdate = semver.gt(latestVersion, currentVersion) 147 | let needsWarning = false 148 | 149 | if (!needsUpdate) { 150 | // a user installed plugin could be older or the same version as a core plugin it overrides 151 | // usually this is not an intended case, and we warn the user 152 | // (scenario: a user installs an updated version of the cli with all the updated plugins, 153 | // but the user already has these updated as user-installed plugins) 154 | const pluginOverridesCorePlugin = (type === 'user' && corePlugins.includes(name)) 155 | if (pluginOverridesCorePlugin) { 156 | coreVersion = await getNpmLocalVersion(this.config.root, name) 157 | needsWarning = semver.gte(coreVersion, currentVersion) 158 | } 159 | } 160 | 161 | plugins.push({ 162 | type, 163 | name, 164 | currentVersion, 165 | latestVersion, 166 | coreVersion, 167 | needsUpdate, 168 | needsWarning 169 | }) 170 | } 171 | 172 | return plugins 173 | } 174 | 175 | /** 176 | * Command entry point 177 | * 178 | * @returns {Promise} promise that lists/interactive install/installs the update 179 | */ 180 | async run () { 181 | const { flags } = await this.parse(UpdateCommand) 182 | const spinner = ora() 183 | 184 | spinner.start() 185 | const plugins = await this.__processPlugins() 186 | spinner.stop() 187 | 188 | const corePlugins = this.config.pjson.oclif.plugins 189 | const needsUpdateCore = plugins.filter(p => p.needsUpdate && p.type === 'core') 190 | const needsUpdateUser = plugins.filter(p => p.needsUpdate && p.type !== 'core') 191 | const needsUpdateCoreButUserInstalled = needsUpdateUser.filter(p => corePlugins.includes(p.name)) 192 | const needsUpdateUserNonCore = needsUpdateUser.filter(p => !corePlugins.includes(p.name)) 193 | 194 | const needsWarning = plugins.filter(p => p.needsWarning) 195 | 196 | if (needsWarning.length > 0) { 197 | this.log(`${chalk.red('warning:')} the user-installed plugin(s) below have versions older or equal to the core plugin versions, and should be uninstalled via ${chalk.yellow(`${this.config.pjson.oclif.bin} plugins:uninstall `)}`) 198 | this.__list(needsWarning, { col1: 'plugin(s) to uninstall' }) 199 | this.log() 200 | } 201 | 202 | const corePluginTotal = needsUpdateCore.length + needsUpdateCoreButUserInstalled.length 203 | 204 | this.log(`There are ${chalk.yellow(corePluginTotal)} core plugin update(s), and ${chalk.yellow(needsUpdateUserNonCore.length)} user plugin update(s) available.`) 205 | this.log() 206 | 207 | if (corePluginTotal > 0) { 208 | const pluginsToRollback = needsUpdateCoreButUserInstalled 209 | .map(plugin => ({ ...plugin, asterisk: true })) 210 | await this.__list([...needsUpdateCore, ...pluginsToRollback], { col1: 'Core plugin updates available' }) 211 | this.log() 212 | if (needsUpdateCoreButUserInstalled.length > 0) { 213 | this.log(`${chalk.red('warning:')} these plugins need to be rolled-back first via ${chalk.yellow('aio rollback -i')}:\n${pluginsToRollback.map(p => ` - ${p.name}`).join('\n')}`) 214 | } 215 | this.log(`${chalk.blueBright('note:')} to update the core plugins, please reinstall the cli: ${chalk.yellow('npm install -g @adobe/aio-cli')}`) 216 | this.log() 217 | } 218 | 219 | if (needsUpdateUserNonCore.length > 0) { 220 | if (flags.list) { 221 | return this.__list(needsUpdateUserNonCore) 222 | } else if (flags.interactive) { 223 | return this.__interactiveInstall(needsUpdateUserNonCore, flags.verbose) 224 | } else { 225 | return this.__install(needsUpdateUserNonCore, flags.confirm, flags.verbose) 226 | } 227 | } 228 | } 229 | } 230 | 231 | UpdateCommand.description = `Update all installed plugins. 232 | This command will only: 233 | - update user-installed plugins that are not core 234 | ` 235 | 236 | UpdateCommand.flags = { 237 | interactive: Flags.boolean({ 238 | char: 'i', 239 | default: false, 240 | description: 'interactive update mode' 241 | }), 242 | list: Flags.boolean({ 243 | char: 'l', 244 | default: false, 245 | description: 'list plugins that will be updated' 246 | }), 247 | confirm: Flags.boolean({ 248 | char: 'c', 249 | default: true, 250 | description: 'confirmation needed for update (defaults to true)', 251 | allowNo: true 252 | }), 253 | verbose: Flags.boolean({ 254 | char: 'v', 255 | default: false, 256 | description: 'Verbose output' 257 | }) 258 | } 259 | 260 | module.exports = UpdateCommand 261 | -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe Inc. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | const fetch = require('node-fetch') 14 | const fs = require('fs') 15 | const inquirer = require('inquirer') 16 | 17 | require('./types.jsdoc') // get types 18 | 19 | /** 20 | * Sort array values according to the sort order and/or sort-field. 21 | * 22 | * Note that this will use the Javascript sort() function, thus the values will 23 | * be sorted in-place. 24 | * 25 | * @param {Array} values array of objects (with fields to sort by) 26 | * @param {object} [options] sort options to pass 27 | * @param {boolean} [options.descending] true by default, sort order 28 | * @param {string} [options.field] 'date' by default, sort field ('name', 'date' options) 29 | * @returns {Array} the sorted values array (input values array sorted in place) 30 | */ 31 | function sortValues (values, { descending = true, field = 'date' } = {}) { 32 | const supportedFields = ['name', 'date'] 33 | if (!supportedFields.includes(field)) { // unknown field, we just return the array 34 | console.log('unknown field ... ', field) 35 | return values 36 | } 37 | 38 | values.sort((left, right) => { 39 | const d1 = left[field] 40 | const d2 = right[field] 41 | 42 | if (descending) { 43 | return (d1 > d2) ? -1 : (d1 < d2) ? 1 : 0 44 | } else { 45 | return (d1 > d2) ? 1 : (d1 < d2) ? -1 : 0 46 | } 47 | }) 48 | return values 49 | } 50 | 51 | /** 52 | * Gets the latest version of a plugin from npm. 53 | * 54 | * @param {string} npmPackageName the npm package name of the plugin 55 | * @returns {string} the latest version of the plugin from the npm registry 56 | */ 57 | async function getNpmLatestVersion (npmPackageName) { 58 | const res = await fetch(`https://registry.npmjs.com/${npmPackageName}`) 59 | const { 'dist-tags': distTags } = await res.json() 60 | return distTags && distTags.latest 61 | } 62 | 63 | /** 64 | * Gets the npm package version of an npm package installed in the cli. 65 | * 66 | * @param {string} cliRoot the root path of the cli 67 | * @param {string} npmPackageName the npm package name 68 | * @returns {string} the version of the package from the cli node_modules 69 | */ 70 | async function getNpmLocalVersion (cliRoot, npmPackageName) { 71 | const pjsonPath = `${cliRoot}/node_modules/${npmPackageName}/package.json` 72 | const pjson = JSON.parse(fs.readFileSync(pjsonPath)) 73 | 74 | return pjson.version 75 | } 76 | 77 | /** 78 | * Prompt for confirmation. 79 | * 80 | * @param {string} [message] the message to show 81 | * @param {boolean} [defaultValue] the default value if the user presses 'Enter' 82 | * @returns {boolean} true or false chosen for the confirmation 83 | */ 84 | async function prompt (message = 'Confirm?', defaultValue = false) { 85 | return inquirer.prompt({ 86 | name: 'confirm', 87 | type: 'confirm', 88 | message, 89 | default: defaultValue 90 | }).then(function (answers) { 91 | return answers.confirm 92 | }) 93 | } 94 | 95 | /** 96 | * Hide NPM Warnings by intercepting process.stderr.write stream 97 | * 98 | */ 99 | function hideNPMWarnings () { 100 | const fn = process.stderr.write 101 | 102 | /** 103 | * Function to override the process.stderr.write and hide npm warnings 104 | * 105 | * @private 106 | */ 107 | function write () { 108 | const msg = Buffer.isBuffer(arguments[0]) ? arguments[0].toString() : arguments[0] 109 | if (!msg.startsWith('warning')) { 110 | fn.apply(process.stderr, arguments) 111 | } 112 | return true 113 | } 114 | process.stderr.write = write 115 | } 116 | 117 | module.exports = { 118 | prompt, 119 | sortValues, 120 | getNpmLatestVersion, 121 | getNpmLocalVersion, 122 | hideNPMWarnings 123 | } 124 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Adobe Inc. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | const { Command, run, Config } = require('@oclif/core') 14 | const semver = require('semver') 15 | const chalk = require('chalk') 16 | 17 | class AIOCommand extends Command { } 18 | 19 | AIOCommand.run = async (argv, opts) => { 20 | if (!argv) { 21 | argv = process.argv.slice(2) 22 | } 23 | 24 | // oclif originally included the following too ... 25 | // this resulted in an uncovered line in the tests, and it appeared to never happen anyway 26 | // seem like it would only 27 | // || module.parent && module.parent.parent && module.parent.parent.filename 28 | const config = await Config.load(opts || __dirname) 29 | 30 | // Check Node.js version 31 | const nodeVersion = process.version 32 | if (!semver.satisfies(nodeVersion, config.pjson.engines.node)) { 33 | console.log(chalk.yellow(`⚠️ Warning: Node.js version ${nodeVersion} is not supported. Supported versions are ${config.pjson.engines.node}.`)) 34 | } 35 | 36 | // the second parameter is the root path to the CLI containing the command 37 | try { 38 | return await run(argv, config.options) 39 | } catch (error) { 40 | // oclif throws if the user typed --help ... ? 41 | if (error.oclif && error.oclif.exit === 0) { 42 | await config.runHook('postrun') 43 | } else { 44 | await config.runHook('command_error', { message: error.message }) 45 | throw (error) 46 | } 47 | } 48 | } 49 | 50 | module.exports = AIOCommand 51 | -------------------------------------------------------------------------------- /src/types.jsdoc.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe. All rights reserved. 3 | This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. You may obtain a copy 5 | of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | Unless required by applicable law or agreed to in writing, software distributed under 7 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 8 | OF ANY KIND, either express or implied. See the License for the specific language 9 | governing permissions and limitations under the License. 10 | */ 11 | 12 | /** 13 | * InstalledPlugin 14 | * Metadata for a plugin that is installed in the CLI. 15 | * 16 | * @typedef {object} InstalledPlugin 17 | * @property {string} type - the type of the plugin (user, core, link) 18 | * @property {string} name - the npm package name of the plugin 19 | * @property {string} version - the version of the npm package 20 | */ 21 | 22 | /** 23 | * ToUpdatePlugin. 24 | * Metadata regarding a plugin that needs to be updated. 25 | * 26 | * @typedef {object} ToUpdatePlugin 27 | * @property {string} type - the type of the plugin (user, core, link) 28 | * @property {string} name - the npm package name of the plugin 29 | * @property {string} coreVersion - if it's a core plugin, the version of the core plugin that is installed with the cli 30 | * @property {string} currentVersion - the current version of the npm package 31 | * @property {string} latestVersion - the latest version of the npm package (in the npm registry) 32 | * @property {boolean} needsUpdate - true if the package needs to be updated 33 | * @property {boolean} needsWarning - true if the package needs a warning 34 | */ 35 | -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "node/no-unpublished-require": 0 4 | } 5 | } -------------------------------------------------------------------------------- /test/commands/discover.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe Inc. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | const fetch = require('node-fetch') 14 | const inquirer = require('inquirer') 15 | const TheCommand = require('../../src/commands/discover') 16 | const { stdout } = require('stdout-stderr') 17 | 18 | jest.mock('inquirer') 19 | 20 | let command 21 | 22 | beforeEach(() => { 23 | fetch.resetMocks() 24 | command = new TheCommand([]) 25 | command.config = { 26 | commands: [{ pluginName: '@adobe/aio-cli-plugin-baz' }], 27 | runCommand: jest.fn() 28 | } 29 | }) 30 | 31 | test('exports a run function', async () => { 32 | expect(typeof TheCommand.run).toEqual('function') 33 | }) 34 | 35 | describe('sorting', () => { 36 | const genesis = new Date() 37 | const later = new Date(genesis.valueOf()) 38 | later.setDate(later.getDate() + 10) 39 | 40 | const expectedResult = { 41 | objects: [ 42 | { package: { name: '@adobe/aio-cli-plugin-a', description: 'plugin a', version: '1.0.0', date: genesis } }, 43 | { package: { name: '@adobe/aio-cli-plugin-b', description: 'plugin b', version: '1.0.1', date: later } } 44 | ] 45 | } 46 | beforeEach(() => { 47 | fetch.mockResponseOnce(JSON.stringify(expectedResult)) 48 | }) 49 | 50 | test('unknown sort-field', async () => { 51 | fetch.mockResponseOnce(JSON.stringify({ 52 | objects: [] 53 | })) 54 | command.argv = ['--sort-field', 'unknown'] 55 | await expect(command.run()).rejects.toThrow('Expected --sort-field=') 56 | }) 57 | 58 | test('sort-field=name, ascending', async () => { 59 | command.argv = ['--sort-field', 'name', '--sort-order', 'asc'] 60 | await command.run() 61 | const splitOutput = stdout.output.split('\n') 62 | expect(splitOutput[2]).toMatch('@adobe/aio-cli-plugin-a') // @adobe/aio-cli-plugin-a is first 63 | expect(splitOutput[3]).toMatch('@adobe/aio-cli-plugin-b') // @adobe/aio-cli-plugin-b is second 64 | }) 65 | 66 | test('sort-field=name, descending', async () => { 67 | command.argv = ['--sort-field', 'name', '--sort-order', 'desc'] 68 | await command.run() 69 | const splitOutput = stdout.output.split('\n') 70 | expect(splitOutput[2]).toMatch('@adobe/aio-cli-plugin-b') // @adobe/aio-cli-plugin-b is first 71 | expect(splitOutput[3]).toMatch('@adobe/aio-cli-plugin-a') // @adobe/aio-cli-plugin-a is second 72 | }) 73 | 74 | test('sort-field=date, ascending', async () => { 75 | command.argv = ['--sort-field', 'date', '--sort-order', 'asc'] 76 | await command.run() 77 | const splitOutput = stdout.output.split('\n') 78 | expect(splitOutput[2]).toMatch('@adobe/aio-cli-plugin-a') // @adobe/aio-cli-plugin-a is first 79 | expect(splitOutput[3]).toMatch('@adobe/aio-cli-plugin-b') // @adobe/aio-cli-plugin-b is second 80 | }) 81 | 82 | test('sort-field=date, descending', async () => { 83 | command.argv = ['--sort-field', 'date', '--sort-order', 'desc'] 84 | await command.run() 85 | const splitOutput = stdout.output.split('\n') 86 | expect(splitOutput[2]).toMatch('@adobe/aio-cli-plugin-b') // @adobe/aio-cli-plugin-b is first 87 | expect(splitOutput[3]).toMatch('@adobe/aio-cli-plugin-a') // @adobe/aio-cli-plugin-a is second 88 | }) 89 | }) 90 | 91 | test('interactive install', async () => { 92 | const now = new Date() 93 | const tomorrow = new Date(now.valueOf() + 86400000) 94 | const dayAfter = new Date(tomorrow.valueOf() + 86400000) 95 | 96 | const expectedResult = { 97 | objects: [ 98 | { package: { name: '@adobe/aio-cli-plugin-foo', description: 'some foo', version: '1.0.0', date: now } }, 99 | { package: { name: '@adobe/aio-cli-plugin-bar', description: 'some bar', version: '1.0.1', date: tomorrow } }, 100 | { package: { name: '@adobe/aio-cli-plugin-baz', description: 'some baz', version: '1.0.2', date: dayAfter } } 101 | ] 102 | } 103 | fetch.mockResponseOnce(JSON.stringify(expectedResult)) 104 | 105 | command.argv = ['-i'] 106 | inquirer.prompt = jest.fn().mockResolvedValue({ 107 | plugins: ['@adobe/aio-cli-plugin-bar', '@adobe/aio-cli-plugin-foo'] 108 | }) 109 | 110 | const result = await command.run() 111 | expect(result).toEqual(['@adobe/aio-cli-plugin-bar', '@adobe/aio-cli-plugin-foo']) 112 | const arg = inquirer.prompt.mock.calls[0][0] // first arg of first call 113 | expect(arg[0].choices.map(elem => elem.value)).toEqual(['@adobe/aio-cli-plugin-bar', '@adobe/aio-cli-plugin-foo']) // @adobe/aio-cli-plugin-baz was an existing plugin, filtered out 114 | }) 115 | 116 | test('interactive install - no choices', async () => { 117 | const now = new Date() 118 | 119 | const expectedResult = { 120 | objects: [ 121 | { package: { name: '@adobe/aio-cli-plugin-baz', description: 'some baz', version: '1.0.2', date: now } } 122 | ] 123 | } 124 | fetch.mockResponseOnce(JSON.stringify(expectedResult)) 125 | 126 | command.argv = ['-i'] 127 | inquirer.prompt = jest.fn().mockResolvedValue({ 128 | plugins: [] 129 | }) 130 | const result = await command.run() 131 | expect(result).toEqual([]) 132 | }) 133 | 134 | test('json result error', async () => { 135 | fetch.mockResponse() 136 | command.argv = [] 137 | await expect(command.run()).rejects.toThrow('FetchError: invalid json response body') 138 | }) 139 | -------------------------------------------------------------------------------- /test/commands/rollback.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe Inc. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | const fetch = require('node-fetch') 14 | const inquirer = require('inquirer') 15 | const { stdout } = require('stdout-stderr') 16 | const helpers = require('../../src/helpers') 17 | 18 | jest.mock('../../src/helpers') 19 | jest.mock('inquirer') 20 | jest.mock('ora') 21 | const ora = require('ora') 22 | ora.mockImplementation(() => ({ 23 | start: jest.fn(), 24 | stop: jest.fn() 25 | })) 26 | 27 | const TheCommand = require('../../src/commands/rollback') 28 | 29 | let command 30 | 31 | beforeEach(() => { 32 | jest.clearAllMocks() 33 | fetch.resetMocks() 34 | command = new TheCommand([]) 35 | command.config = { 36 | commands: [{ pluginName: 'baz' }], 37 | runCommand: jest.fn() 38 | } 39 | }) 40 | 41 | /** @private */ 42 | function mockConfig (corePlugins, installedPlugins) { 43 | return { 44 | root: '/cliroot', 45 | pjson: { 46 | oclif: { 47 | plugins: corePlugins 48 | } 49 | }, 50 | plugins: installedPlugins, 51 | runCommand: jest.fn() 52 | } 53 | } 54 | 55 | /** @private */ 56 | function doRunCommand (argv, onSuccess, onFailure) { 57 | return new Promise((resolve, reject) => { 58 | command.argv = argv 59 | return command.run() 60 | .then(async () => { 61 | if (typeof onSuccess === 'function') { 62 | await onSuccess() 63 | } 64 | resolve() 65 | }) 66 | .catch(async e => { 67 | if (typeof onFailure === 'function') { 68 | await onFailure() 69 | } 70 | reject(e) 71 | }) 72 | }) 73 | } 74 | 75 | test('exports a run function', async () => { 76 | expect(typeof TheCommand.run).toEqual('function') 77 | }) 78 | 79 | test('no installed plugins', () => { 80 | const corePlugins = [] 81 | const installedPlugins = [] 82 | 83 | command.config = mockConfig(corePlugins, installedPlugins) 84 | 85 | return doRunCommand([], async () => { 86 | expect(stdout.output).toMatch('no installed plugins to clear') 87 | }) 88 | }) 89 | 90 | test('clear (--no-confirm)', () => { 91 | const corePlugins = ['core1'] 92 | const installedPlugins = [ 93 | { name: 'core1', version: '0.1', type: 'core' }, 94 | { name: 'plugin1', version: '0.1', type: 'user' }, 95 | { name: 'plugin2', version: '0.1', type: 'user' }, 96 | { name: 'plugin3', version: '0.1', type: 'user' } 97 | ] 98 | 99 | command.config = mockConfig(corePlugins, installedPlugins) 100 | const spy = jest.spyOn(command, '__clear') 101 | 102 | return doRunCommand(['--no-confirm'], async () => { 103 | const results = (await spy.mock.calls[0][0]).filter(p => p.type === 'user') 104 | expect(results.length).toEqual(3) 105 | }) 106 | }) 107 | 108 | test('clear (--confirm)', () => { 109 | const corePlugins = ['core1'] 110 | const installedPlugins = [ 111 | { name: 'core1', version: '0.1', type: 'core' }, 112 | { name: 'plugin1', version: '0.1', type: 'user' }, 113 | { name: 'plugin2', version: '0.1', type: 'user' }, 114 | { name: 'plugin3', version: '0.1', type: 'user' } 115 | ] 116 | 117 | command.config = mockConfig(corePlugins, installedPlugins) 118 | const spy = jest.spyOn(command, '__clear') 119 | 120 | return doRunCommand(['--confirm'], async () => { 121 | const results = (await spy.mock.calls[0][0]).filter(p => p.type === 'user') 122 | expect(results.length).toEqual(3) 123 | }) 124 | }) 125 | 126 | test('clear (--interactive)', () => { 127 | const corePlugins = ['core1'] 128 | const installedPlugins = [ 129 | { name: 'core1', version: '0.1', type: 'core' }, 130 | { name: 'plugin1', version: '0.1', type: 'user' }, 131 | { name: 'plugin2', version: '0.1', type: 'user' }, 132 | { name: 'plugin3', version: '0.1', type: 'user' } 133 | ] 134 | 135 | inquirer.prompt = jest.fn().mockResolvedValue({ 136 | plugins: ['plugin1', 'plugin2'] 137 | }) 138 | 139 | command.config = mockConfig(corePlugins, installedPlugins) 140 | const spy = jest.spyOn(command, '__interactiveClear') 141 | 142 | return doRunCommand(['--interactive'], async () => { 143 | const results = (await spy.mock.calls[0][0]).filter(p => p.type === 'user') 144 | expect(results.length).toEqual(3) 145 | }) 146 | }) 147 | 148 | test('clear (--verbose)', () => { 149 | const corePlugins = ['core1'] 150 | const installedPlugins = [ 151 | { name: 'core1', version: '0.1', type: 'core' }, 152 | { name: 'plugin1', version: '0.1', type: 'user' }, 153 | { name: 'plugin2', version: '0.1', type: 'user' }, 154 | { name: 'plugin3', version: '0.1', type: 'user' } 155 | ] 156 | 157 | inquirer.prompt = jest.fn().mockResolvedValue({ 158 | plugins: ['plugin1', 'plugin2'] 159 | }) 160 | 161 | helpers.hideNPMWarnings.mockImplementation(() => {}) 162 | command.config = mockConfig(corePlugins, installedPlugins) 163 | 164 | return doRunCommand(['--no-confirm', '--verbose'], async () => { 165 | expect(helpers.hideNPMWarnings).not.toHaveBeenCalled() 166 | }) 167 | }) 168 | 169 | test('clear (--interactive. --verbose)', () => { 170 | const corePlugins = ['core1'] 171 | const installedPlugins = [ 172 | { name: 'core1', version: '0.1', type: 'core' }, 173 | { name: 'plugin1', version: '0.1', type: 'user' }, 174 | { name: 'plugin2', version: '0.1', type: 'user' }, 175 | { name: 'plugin3', version: '0.1', type: 'user' } 176 | ] 177 | 178 | inquirer.prompt = jest.fn().mockResolvedValue({ 179 | plugins: ['plugin1', 'plugin2'] 180 | }) 181 | 182 | helpers.hideNPMWarnings.mockImplementation(() => {}) 183 | command.config = mockConfig(corePlugins, installedPlugins) 184 | 185 | return doRunCommand(['--interactive', '--verbose'], async () => { 186 | expect(helpers.hideNPMWarnings).not.toHaveBeenCalled() 187 | }) 188 | }) 189 | 190 | test('clear (--list)', () => { 191 | const corePlugins = ['core1'] 192 | const installedPlugins = [ 193 | { name: 'core1', version: '0.1', type: 'core' }, 194 | { name: 'plugin1', version: '0.1', type: 'user' }, 195 | { name: 'plugin2', version: '0.1', type: 'user' }, 196 | { name: 'plugin3', version: '0.1', type: 'user' } 197 | ] 198 | 199 | command.config = mockConfig(corePlugins, installedPlugins) 200 | const spy = jest.spyOn(command, '__list') 201 | 202 | return doRunCommand(['--list'], async () => { 203 | expect(spy).toHaveBeenCalledTimes(1) 204 | }) 205 | }) 206 | -------------------------------------------------------------------------------- /test/commands/update.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe Inc. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | const fetch = require('node-fetch') 14 | const inquirer = require('inquirer') 15 | const helpers = require('../../src/helpers') 16 | const { stdout } = require('stdout-stderr') 17 | 18 | jest.mock('../../src/helpers') 19 | jest.mock('inquirer') 20 | jest.mock('ora') 21 | const ora = require('ora') 22 | ora.mockImplementation(() => ({ 23 | start: jest.fn(), 24 | stop: jest.fn() 25 | })) 26 | 27 | const TheCommand = require('../../src/commands/update') 28 | 29 | let command 30 | 31 | beforeEach(() => { 32 | jest.clearAllMocks() 33 | fetch.resetMocks() 34 | command = new TheCommand([]) 35 | command.config = { 36 | commands: [], 37 | runCommand: jest.fn() 38 | } 39 | }) 40 | 41 | /** @private */ 42 | function mockConfig (corePlugins, installedPlugins) { 43 | return { 44 | root: '/cliroot', 45 | pjson: { 46 | oclif: { 47 | plugins: corePlugins 48 | } 49 | }, 50 | plugins: installedPlugins, 51 | runCommand: jest.fn() 52 | } 53 | } 54 | 55 | /** @private */ 56 | function doRunCommand (argv, onSuccess, onFailure) { 57 | return new Promise((resolve, reject) => { 58 | command.argv = argv 59 | return command.run() 60 | .then(async () => { 61 | if (typeof onSuccess === 'function') { 62 | await onSuccess() 63 | } 64 | resolve() 65 | }) 66 | .catch(async e => { 67 | if (typeof onFailure === 'function') { 68 | await onFailure() 69 | } 70 | reject(e) 71 | }) 72 | }) 73 | } 74 | 75 | test('exports a run function', async () => { 76 | expect(typeof TheCommand.run).toEqual('function') 77 | }) 78 | 79 | test('no updates', () => { 80 | const corePlugins = ['@adobe/core1'] 81 | const installedPlugins = [ 82 | { name: '@adobe/core1', version: '0.1.0', type: 'core' }, 83 | { name: 'plugin1', version: '0.1.0', type: 'user' }, 84 | { name: 'plugin2', version: '0.1.0', type: 'user' }, 85 | { name: 'plugin3', version: '0.1.0', type: 'user' } 86 | ] 87 | 88 | helpers.getNpmLatestVersion.mockImplementation(() => '0.1.0') 89 | helpers.getNpmLocalVersion.mockImplementation(() => '0.1.0') 90 | command.config = mockConfig(corePlugins, installedPlugins) 91 | 92 | const spy = jest.spyOn(command, '__processPlugins') 93 | 94 | return doRunCommand([], async () => { 95 | const results = (await spy.mock.results[0].value).filter(p => p.needsUpdate) 96 | expect(results.length).toEqual(0) 97 | expect(stdout.output).toMatch('There are 0 core plugin update(s), and 0 user plugin update(s) available.') 98 | }) 99 | }) 100 | 101 | test('updates needed? (via various semver versions)', async () => { 102 | const corePlugins = ['@adobe/core1'] 103 | const installedPlugins = [ 104 | { name: '@adobe/core1', version: '0.1.0', type: 'core' } 105 | ] 106 | 107 | helpers.getNpmLocalVersion.mockImplementation(() => '0.1.0') 108 | command.config = mockConfig(corePlugins, installedPlugins) 109 | 110 | const spy = jest.spyOn(command, '__processPlugins') 111 | 112 | const createDoRunCommand = ({ needsUpdateCount = 0, spyCalledTimes = 1 } = {}) => { 113 | return doRunCommand([], async () => { 114 | const results = (await spy.mock.results[spyCalledTimes - 1].value).filter(p => p.needsUpdate) 115 | expect(results.length).toEqual(needsUpdateCount) 116 | expect(spy).toHaveBeenCalledTimes(spyCalledTimes) 117 | expect(stdout.output).toMatch('There are 0 core plugin update(s), and 0 user plugin update(s) available.') 118 | }) 119 | } 120 | 121 | helpers.getNpmLatestVersion.mockImplementation(() => '0.1.0-pre') 122 | await createDoRunCommand({ needsUpdateCount: 0, spyCalledTimes: 1 }) // no updates for same version with a `-` suffixed tag 123 | 124 | helpers.getNpmLatestVersion.mockImplementation(() => '0.0.1') 125 | await createDoRunCommand({ needsUpdateCount: 0, spyCalledTimes: 2 }) // no updates for same version 126 | 127 | helpers.getNpmLatestVersion.mockImplementation(() => '0.2.0') 128 | await createDoRunCommand({ needsUpdateCount: 1, spyCalledTimes: 3 }) // update needed for newer version 129 | 130 | helpers.getNpmLatestVersion.mockImplementation(() => '0.2.0-beta') 131 | await createDoRunCommand({ needsUpdateCount: 1, spyCalledTimes: 4 }) // update needed for newer version with `-` suffix tag 132 | 133 | helpers.getNpmLatestVersion.mockImplementation(() => 'v0.1.0') 134 | await createDoRunCommand({ needsUpdateCount: 0, spyCalledTimes: 5 }) // version prefixed with `v` is parsed out 135 | 136 | helpers.getNpmLatestVersion.mockImplementation(() => 'v0.2.0') 137 | await createDoRunCommand({ needsUpdateCount: 1, spyCalledTimes: 6 }) // version prefixed with `v` is parsed out 138 | 139 | helpers.getNpmLatestVersion.mockImplementation(() => undefined) 140 | await createDoRunCommand({ needsUpdateCount: 0, spyCalledTimes: 7 }) // no npm latest 141 | }) 142 | 143 | test('needs update (--no-confirm)', () => { 144 | const corePlugins = ['@adobe/core1', 'core2-non-adobe'] 145 | const installedPlugins = [ 146 | { name: '@adobe/core1', version: '0.1.0', type: 'user' }, // core plugins will never be updateable 147 | { name: 'core2-non-adobe', version: '0.1.0', type: 'user' }, // core plugins will never be updateable 148 | { name: 'plugin1', version: '0.1.0', type: 'user' }, 149 | { name: 'plugin2', version: '0.1.0', type: 'user' }, 150 | { name: 'plugin3', version: '0.1.0', type: 'user' } 151 | ] 152 | 153 | helpers.getNpmLatestVersion.mockImplementation(() => '0.2.0') 154 | helpers.getNpmLocalVersion.mockImplementation(() => '0.1.0') 155 | command.config = mockConfig(corePlugins, installedPlugins) 156 | 157 | const spy = jest.spyOn(command, '__processPlugins') 158 | const spyInstall = jest.spyOn(command, '__install') 159 | 160 | return doRunCommand(['--no-confirm'], async () => { 161 | const results = (await spy.mock.results[0].value).filter(p => p.needsUpdate) 162 | expect(results.length).toEqual(4) 163 | 164 | const lastInstallCall = spyInstall.mock.calls[spyInstall.mock.calls.length - 1] 165 | const filteredPlugins = installedPlugins.filter(p => !corePlugins.includes(p.name)) 166 | expect(lastInstallCall[0].length).toEqual(filteredPlugins.length) 167 | expect(lastInstallCall[0].length).toEqual(3) 168 | }) 169 | }) 170 | 171 | test('needs update (--confirm)', () => { 172 | const corePlugins = ['@adobe/core1', 'core2-non-adobe'] 173 | const installedPlugins = [ 174 | { name: '@adobe/core1', version: '0.1.0', type: 'core' }, 175 | { name: 'core2-non-adobe', version: '0.1.0', type: 'core' }, 176 | { name: 'plugin1', version: '0.1.0', type: 'user' }, 177 | { name: 'plugin2', version: '0.1.0', type: 'user' }, 178 | { name: 'plugin3', version: '0.1.0', type: 'user' } 179 | ] 180 | 181 | helpers.getNpmLatestVersion.mockImplementation(() => '0.2.0') 182 | helpers.getNpmLocalVersion.mockImplementation(() => '0.1.0') 183 | command.config = mockConfig(corePlugins, installedPlugins) 184 | 185 | const spyProcessPlugins = jest.spyOn(command, '__processPlugins') 186 | const spyInstall = jest.spyOn(command, '__install') 187 | 188 | return doRunCommand(['--confirm'], async () => { 189 | const results = (await spyProcessPlugins.mock.results[0].value).filter(p => p.needsUpdate) 190 | expect(results.length).toEqual(4) 191 | 192 | const lastInstallCall = spyInstall.mock.calls[spyInstall.mock.calls.length - 1] 193 | const filteredPlugins = installedPlugins.filter(p => p.type === 'user') 194 | expect(lastInstallCall[0].length).toEqual(filteredPlugins.length) 195 | expect(lastInstallCall[0].length).toEqual(3) 196 | }) 197 | }) 198 | 199 | test('needs warning', () => { 200 | const corePlugins = ['@adobe/core1', '@adobe/core2'] 201 | const installedPlugins = [ 202 | { name: '@adobe/core1', version: '0.1.0', type: 'user' }, 203 | { name: '@adobe/core2', version: '0.1.0', type: 'user' }, 204 | { name: 'plugin2', version: '0.1.0', type: 'user' }, 205 | { name: 'plugin3', version: '0.1.0', type: 'user' } 206 | ] 207 | 208 | helpers.getNpmLatestVersion.mockImplementation(() => '0.1.0') 209 | helpers.getNpmLocalVersion.mockImplementation(() => '0.1.0') 210 | command.config = mockConfig(corePlugins, installedPlugins) 211 | 212 | const spy = jest.spyOn(command, '__processPlugins') 213 | 214 | return doRunCommand([], async () => { 215 | const results = (await spy.mock.results[0].value).filter(p => p.needsWarning) 216 | expect(results.length).toEqual(2) 217 | }) 218 | }) 219 | 220 | test('list', () => { 221 | const corePlugins = ['@adobe/core1'] 222 | const installedPlugins = [ 223 | { name: '@adobe/core1', version: '0.1.0', type: 'core' }, 224 | { name: 'plugin1', version: '0.1.0', type: 'user' }, 225 | { name: 'plugin2', version: '0.1.0', type: 'user' }, 226 | { name: 'plugin3', version: '0.1.0', type: 'user' } 227 | ] 228 | 229 | helpers.getNpmLatestVersion.mockImplementation(() => '0.2.0') 230 | helpers.getNpmLocalVersion.mockImplementation(() => '0.1.0') 231 | command.config = mockConfig(corePlugins, installedPlugins) 232 | 233 | const spyList = jest.spyOn(command, '__list') 234 | const spyInstall = jest.spyOn(command, '__install') 235 | const spyInteractiveInstall = jest.spyOn(command, '__interactiveInstall') 236 | 237 | return doRunCommand(['--list'], async () => { 238 | expect(spyList).toHaveBeenCalled() 239 | expect(spyInstall).not.toHaveBeenCalled() 240 | expect(spyInteractiveInstall).not.toHaveBeenCalled() 241 | 242 | const lastListCall = spyList.mock.calls[spyList.mock.calls.length - 1] 243 | const filteredPlugins = installedPlugins.filter(p => p.type === 'user') 244 | expect(lastListCall[0].length).toEqual(filteredPlugins.length) 245 | expect(lastListCall[0].length).toEqual(3) 246 | }) 247 | }) 248 | 249 | test('interactive', () => { 250 | const corePlugins = ['@adobe/core1'] 251 | const installedPlugins = [ 252 | { name: '@adobe/core1', version: '0.1.0', type: 'core' }, 253 | { name: 'plugin1', version: '0.1.0', type: 'user' }, 254 | { name: 'plugin2', version: '0.1.0', type: 'user' }, 255 | { name: 'plugin3', version: '0.1.0', type: 'user' } 256 | ] 257 | 258 | helpers.getNpmLatestVersion.mockImplementation(() => '0.2.0') 259 | helpers.getNpmLocalVersion.mockImplementation(() => '0.1.0') 260 | helpers.hideNPMWarnings.mockImplementation(() => {}) 261 | command.config = mockConfig(corePlugins, installedPlugins) 262 | 263 | const spyInstall = jest.spyOn(command, '__install') 264 | const spyInteractiveInstall = jest.spyOn(command, '__interactiveInstall') 265 | 266 | inquirer.prompt = jest.fn().mockResolvedValue({ 267 | plugins: ['plugin1', 'plugin2'] 268 | }) 269 | 270 | return doRunCommand(['--interactive'], async () => { 271 | expect(spyInstall).not.toHaveBeenCalled() 272 | expect(spyInteractiveInstall).toHaveBeenCalled() 273 | expect(helpers.hideNPMWarnings).toHaveBeenCalled() 274 | 275 | const lastInteractiveCall = spyInteractiveInstall.mock.calls[spyInteractiveInstall.mock.calls.length - 1] 276 | const filteredPlugins = installedPlugins.filter(p => p.type === 'user') 277 | expect(lastInteractiveCall[0].length).toEqual(filteredPlugins.length) 278 | expect(lastInteractiveCall[0].length).toEqual(3) 279 | }) 280 | }) 281 | 282 | test('interactive [--verbose]', () => { 283 | const corePlugins = ['@adobe/core1'] 284 | const installedPlugins = [ 285 | { name: '@adobe/core1', version: '0.1.0', type: 'core' }, 286 | { name: 'plugin1', version: '0.1.0', type: 'user' }, 287 | { name: 'plugin2', version: '0.1.0', type: 'user' }, 288 | { name: 'plugin3', version: '0.1.0', type: 'user' } 289 | ] 290 | 291 | helpers.getNpmLatestVersion.mockImplementation(() => '0.2.0') 292 | helpers.getNpmLocalVersion.mockImplementation(() => '0.1.0') 293 | helpers.hideNPMWarnings.mockImplementation(() => {}) 294 | command.config = mockConfig(corePlugins, installedPlugins) 295 | 296 | inquirer.prompt = jest.fn().mockResolvedValue({ 297 | plugins: ['plugin1', 'plugin2'] 298 | }) 299 | 300 | return doRunCommand(['--interactive', '--verbose'], async () => { 301 | expect(helpers.hideNPMWarnings).not.toHaveBeenCalled() 302 | }) 303 | }) 304 | 305 | test('no-confirm [--verbose]', () => { 306 | const corePlugins = ['@adobe/core1'] 307 | const installedPlugins = [ 308 | { name: '@adobe/core1', version: '0.1.0', type: 'core' }, 309 | { name: 'plugin1', version: '0.1.0', type: 'user' }, 310 | { name: 'plugin2', version: '0.1.0', type: 'user' }, 311 | { name: 'plugin3', version: '0.1.0', type: 'user' } 312 | ] 313 | 314 | helpers.getNpmLatestVersion.mockImplementation(() => '0.2.0') 315 | helpers.getNpmLocalVersion.mockImplementation(() => '0.1.0') 316 | helpers.hideNPMWarnings.mockImplementation(() => {}) 317 | command.config = mockConfig(corePlugins, installedPlugins) 318 | 319 | inquirer.prompt = jest.fn().mockResolvedValue({ 320 | plugins: ['plugin1', 'plugin2'] 321 | }) 322 | 323 | return doRunCommand(['--no-confirm', '--verbose'], async () => { 324 | expect(helpers.hideNPMWarnings).not.toHaveBeenCalled() 325 | }) 326 | }) 327 | -------------------------------------------------------------------------------- /test/helpers.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe Inc. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | const fetch = require('node-fetch') 14 | const { prompt, sortValues, getNpmLatestVersion, getNpmLocalVersion, hideNPMWarnings } = require('../src/helpers') 15 | const inquirer = require('inquirer') 16 | const { stderr } = require('stdout-stderr') 17 | const fs = require('fs') 18 | 19 | jest.mock('fs') 20 | jest.mock('inquirer') 21 | 22 | beforeEach(() => { 23 | fetch.resetMocks() 24 | fs.readFileSync.mockRestore() 25 | }) 26 | 27 | describe('sort tests', () => { 28 | const now = new Date().valueOf() 29 | 30 | const descendingDate = [ 31 | new Date(now + 110), 32 | new Date(now + 100), 33 | new Date(now - 50), 34 | new Date(now - 80), 35 | new Date(now - 100) 36 | ] 37 | 38 | const ascendingDate = [ 39 | new Date(now - 100), 40 | new Date(now - 80), 41 | new Date(now - 50), 42 | new Date(now + 100), 43 | new Date(now + 110) 44 | ] 45 | 46 | const descendingName = ['Zoltan', 'Siobhan', 'Saoirse', 'Deidre', 'Aiofe'] 47 | const ascendingName = ['Aiofe', 'Deidre', 'Saoirse', 'Siobhan', 'Zoltan'] 48 | 49 | const descendingName2 = ['Zoltan', 'Saoirse', 'Saoirse', 'Deidre', 'Aiofe'] // has a duplicate entry 50 | const ascendingName2 = ['Aiofe', 'Deidre', 'Saoirse', 'Saoirse', 'Zoltan'] // has a duplicate entry 51 | 52 | test('sort defaults (descending, date)', () => { 53 | const toSort = [ 54 | { date: descendingDate[2] }, 55 | { date: descendingDate[3] }, 56 | { date: descendingDate[4] }, 57 | { date: descendingDate[0] }, 58 | { date: descendingDate[1] } 59 | ] 60 | sortValues(toSort) 61 | 62 | const expectedSort = [ 63 | { date: descendingDate[0] }, 64 | { date: descendingDate[1] }, 65 | { date: descendingDate[2] }, 66 | { date: descendingDate[3] }, 67 | { date: descendingDate[4] } 68 | ] 69 | 70 | expect(toSort).toEqual(expectedSort) 71 | }) 72 | 73 | test('sort defaults (ascending, date)', () => { 74 | const toSort = [ 75 | { date: ascendingDate[2] }, 76 | { date: ascendingDate[3] }, 77 | { date: ascendingDate[4] }, 78 | { date: ascendingDate[0] }, 79 | { date: ascendingDate[1] } 80 | ] 81 | sortValues(toSort, { descending: false }) 82 | 83 | const expectedSort = [ 84 | { date: ascendingDate[0] }, 85 | { date: ascendingDate[1] }, 86 | { date: ascendingDate[2] }, 87 | { date: ascendingDate[3] }, 88 | { date: ascendingDate[4] } 89 | ] 90 | 91 | expect(toSort).toEqual(expectedSort) 92 | }) 93 | 94 | test('sort (descending, name)', () => { 95 | const toSort = [ 96 | { name: descendingName[2] }, 97 | { name: descendingName[3] }, 98 | { name: descendingName[4] }, 99 | { name: descendingName[0] }, 100 | { name: descendingName[1] } 101 | ] 102 | sortValues(toSort, { field: 'name' }) 103 | 104 | const expectedSort = [ 105 | { name: descendingName[0] }, 106 | { name: descendingName[1] }, 107 | { name: descendingName[2] }, 108 | { name: descendingName[3] }, 109 | { name: descendingName[4] } 110 | ] 111 | 112 | expect(toSort).toEqual(expectedSort) 113 | }) 114 | 115 | test('sort with duplicate entry, for coverage (descending, name)', () => { 116 | const toSort = [ 117 | { name: descendingName2[2] }, 118 | { name: descendingName2[3] }, 119 | { name: descendingName2[4] }, 120 | { name: descendingName2[0] }, 121 | { name: descendingName2[1] } 122 | ] 123 | sortValues(toSort, { field: 'name' }) 124 | 125 | const expectedSort = [ 126 | { name: descendingName2[0] }, 127 | { name: descendingName2[1] }, 128 | { name: descendingName2[2] }, 129 | { name: descendingName2[3] }, 130 | { name: descendingName2[4] } 131 | ] 132 | 133 | expect(toSort).toEqual(expectedSort) 134 | }) 135 | 136 | test('sort with duplicate entry, for coverage (ascending, name)', () => { 137 | const toSort = [ 138 | { name: ascendingName2[2] }, 139 | { name: ascendingName2[3] }, 140 | { name: ascendingName2[4] }, 141 | { name: ascendingName2[0] }, 142 | { name: ascendingName2[1] } 143 | ] 144 | sortValues(toSort, { descending: false, field: 'name' }) 145 | 146 | const expectedSort = [ 147 | { name: ascendingName2[0] }, 148 | { name: ascendingName2[1] }, 149 | { name: ascendingName2[2] }, 150 | { name: ascendingName2[3] }, 151 | { name: ascendingName2[4] } 152 | ] 153 | 154 | expect(toSort).toEqual(expectedSort) 155 | }) 156 | 157 | test('sort (ascending, name)', () => { 158 | const toSort = [ 159 | { name: ascendingName[2] }, 160 | { name: ascendingName[3] }, 161 | { name: ascendingName[4] }, 162 | { name: ascendingName[0] }, 163 | { name: ascendingName[1] } 164 | ] 165 | sortValues(toSort, { descending: false, field: 'name' }) 166 | 167 | const expectedSort = [ 168 | { name: ascendingName[0] }, 169 | { name: ascendingName[1] }, 170 | { name: ascendingName[2] }, 171 | { name: ascendingName[3] }, 172 | { name: ascendingName[4] } 173 | ] 174 | 175 | expect(toSort).toEqual(expectedSort) 176 | }) 177 | 178 | test('sort (unknown field)', () => { 179 | const toSort = [ 180 | { name: ascendingName[2] }, 181 | { name: ascendingName[3] }, 182 | { name: ascendingName[4] }, 183 | { name: ascendingName[0] }, 184 | { name: ascendingName[1] } 185 | ] 186 | sortValues(toSort, { field: 'unknown-field' }) 187 | 188 | // unknown field, it should return the unsorted array 189 | const expectedSort = [ 190 | { name: ascendingName[2] }, 191 | { name: ascendingName[3] }, 192 | { name: ascendingName[4] }, 193 | { name: ascendingName[0] }, 194 | { name: ascendingName[1] } 195 | ] 196 | 197 | expect(toSort).toEqual(expectedSort) 198 | }) 199 | }) 200 | 201 | test('getNpmLatestVersion', async () => { 202 | const json = { 203 | 'dist-tags': { 204 | latest: '1.2.3' 205 | } 206 | } 207 | 208 | fetch.mockResponseOnce(JSON.stringify(json)) 209 | return expect(getNpmLatestVersion('foo')).resolves.toEqual(json['dist-tags'].latest) 210 | }) 211 | 212 | test('getNpmLocalVersion', async () => { 213 | const cliRoot = '/myroot' 214 | const npmPackage = 'mypackage' 215 | const packageVersion = '1.2.3' 216 | 217 | const packageJson = { 218 | version: packageVersion 219 | } 220 | 221 | fs.readFileSync.mockImplementation((filePath) => { 222 | if (filePath === `${cliRoot}/node_modules/${npmPackage}/package.json`) { 223 | return JSON.stringify(packageJson) 224 | } 225 | }) 226 | 227 | return expect(getNpmLocalVersion(cliRoot, npmPackage)).resolves.toEqual(packageVersion) 228 | }) 229 | 230 | test('prompt', async () => { 231 | inquirer.prompt = jest.fn().mockResolvedValue({ 232 | confirm: true 233 | }) 234 | await expect(prompt()).resolves.toEqual(true) 235 | }) 236 | 237 | describe('hideNPMWarnings', () => { 238 | test('with string output', () => { 239 | stderr.start() 240 | hideNPMWarnings() 241 | process.stderr.write('string') 242 | stderr.stop() 243 | expect(stderr.output).toBe('string') 244 | }) 245 | 246 | test('with buffer output', () => { 247 | stderr.start() 248 | hideNPMWarnings() 249 | process.stderr.write(Buffer.from('string')) 250 | stderr.stop() 251 | expect(stderr.output).toBe('string') 252 | }) 253 | 254 | test('string output of warning should be stripped', () => { 255 | stderr.start() 256 | hideNPMWarnings() 257 | process.stderr.write('warning') 258 | stderr.stop() 259 | expect(stderr.output).toBe('') 260 | }) 261 | 262 | test('buffer output of warning should be stripped', () => { 263 | stderr.start() 264 | hideNPMWarnings() 265 | process.stderr.write(Buffer.from('warning ...')) 266 | stderr.stop() 267 | expect(stderr.output).toBe('') 268 | }) 269 | }) 270 | -------------------------------------------------------------------------------- /test/hookerror.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Adobe Inc. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | const testCommand = require('../src/index') 14 | 15 | const mockRunHook = jest.fn() 16 | 17 | class MockOclifError extends Error { 18 | constructor (message) { 19 | super(message) 20 | this.name = 'OclifError' 21 | this.oclif = { exit: 0 } 22 | } 23 | } 24 | 25 | jest.mock('@oclif/core', () => { 26 | return { 27 | ...jest.requireActual('@oclif/core'), 28 | Config: { 29 | load: () => { 30 | return { 31 | pjson: { 32 | engines: { 33 | node: '>=18 <23' 34 | } 35 | }, 36 | runHook: mockRunHook, 37 | options: {} 38 | } 39 | } 40 | }, 41 | ux: { 42 | cli: { 43 | open: jest.fn() 44 | } 45 | }, 46 | Command: jest.fn(), 47 | run: function (cmd) { 48 | if (cmd.indexOf('--help') > -1) { 49 | // this error has extra props, so base command knows not to re-throw it 50 | throw new MockOclifError('maybe things will turn out okay') 51 | } else { 52 | throw new Error('things do not look good') 53 | } 54 | } 55 | } 56 | }) 57 | 58 | describe('when command run throws', () => { 59 | test('fire hook when command throws', async () => { 60 | await expect(testCommand.run(['a', 'c', 'd'])).rejects.toThrow('things do not look good') 61 | expect(mockRunHook).toHaveBeenCalledWith('command_error', expect.objectContaining({ message: 'things do not look good' })) 62 | }) 63 | 64 | test('when command throws oclif-error, swallow error and fire `postrun` event : --help', async () => { 65 | await expect(testCommand.run(['a', 'c', 'd', '--help'])).resolves.toEqual(undefined) 66 | expect(mockRunHook).toHaveBeenCalledWith('postrun') 67 | }) 68 | }) 69 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Adobe Inc. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | const testCommand = require('../src/index') 14 | 15 | jest.mock('@oclif/core', () => { 16 | return { 17 | ...jest.requireActual('@oclif/core'), 18 | Config: { 19 | load: () => ({ 20 | pjson: { 21 | engines: { 22 | node: '>=18 <23' 23 | } 24 | }, 25 | options: {} 26 | }) 27 | }, 28 | Command: jest.fn(), 29 | run: async function (cmd) { 30 | return cmd 31 | } 32 | } 33 | }) 34 | 35 | describe('run command', () => { 36 | test('index exports a run function', async () => { 37 | expect(typeof testCommand.run).toEqual('function') 38 | }) 39 | 40 | test('run function returns a promise', async () => { 41 | const result = testCommand.run(['a']) 42 | expect(typeof result).toEqual('object') 43 | expect(typeof result.then).toEqual('function') 44 | expect(typeof result.catch).toEqual('function') 45 | }) 46 | 47 | test('run using process.argv', async () => { 48 | const temp = process.argv 49 | process.argv = [0, 0, 'a'] 50 | const result = await testCommand.run() 51 | expect(result[0]).toEqual('a') 52 | process.argv = temp 53 | }) 54 | }) 55 | 56 | describe('Node.js version check', () => { 57 | const originalVersion = process.version 58 | let logSpy 59 | 60 | beforeEach(() => { 61 | logSpy = jest.spyOn(console, 'log').mockImplementation() 62 | }) 63 | 64 | afterEach(() => { 65 | jest.restoreAllMocks() 66 | Object.defineProperty(process, 'version', { 67 | value: originalVersion 68 | }) 69 | }) 70 | 71 | test('should not show warning for supported Node.js version', async () => { 72 | Object.defineProperty(process, 'version', { 73 | value: 'v22.14.0' 74 | }) 75 | 76 | const AIOCommand = require('../src/index') 77 | await AIOCommand.run(['--version']) 78 | 79 | // Check warning is not displayed 80 | expect(logSpy).not.toHaveBeenCalledWith( 81 | expect.stringContaining('Warning: Node.js version') 82 | ) 83 | }) 84 | 85 | test('should show warning for unsupported Node.js version', async () => { 86 | Object.defineProperty(process, 'version', { 87 | value: 'v23.0.0' 88 | }) 89 | 90 | const AIOCommand = require('../src/index') 91 | await AIOCommand.run(['--version']) 92 | 93 | // Check warning is displayed 94 | expect(logSpy).toHaveBeenCalledWith( 95 | expect.stringContaining('Warning: Node.js version v23.0.0 is not supported') 96 | ) 97 | }) 98 | }) 99 | -------------------------------------------------------------------------------- /test/jest.setup.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe Inc. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | const { stdout } = require('stdout-stderr') 14 | 15 | jest.setTimeout(3000) 16 | jest.useFakeTimers() 17 | 18 | const fetch = require('jest-fetch-mock') 19 | jest.setMock('node-fetch', fetch) 20 | 21 | // trap console log 22 | // note: if you use console.log, some of these tests will start failing because they depend on the order/position of the output 23 | beforeEach(() => { stdout.start(); stdout.print = false }) 24 | afterEach(() => { stdout.stop() }) 25 | --------------------------------------------------------------------------------