[
  {
    "path": ".cargo/config.toml",
    "content": "[target.x86_64-pc-windows-msvc]\r\nrustflags = [\"-C\", \"target-feature=+crt-static\"]\r\n[target.aarch64-pc-windows-msvc]\r\nrustflags = [\"-C\", \"target-feature=+crt-static\"]\r\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"cargo\"\n    open-pull-requests-limit: 5\n    schedule:\n      interval: \"daily\"\n    directories:\n      - \"/\"\n      - \"/crates/*\"\n"
  },
  {
    "path": ".github/workflows/api-docs.yml",
    "content": "on:\n  push:\n    branches:\n      - main\n\nname: API Docs\n\njobs:\n  publish:\n    name: Build and publish\n    runs-on: ubuntu-latest\n    steps:\n      - name: Check out code\n        uses: actions/checkout@v4\n        with:\n          persist-credentials: false\n      - name: Set up cargo\n        uses: actions-rust-lang/setup-rust-toolchain@v1\n      - name: Cargo Cache\n        uses: Swatinem/rust-cache@v2\n      - name: Build docs\n        run: |\n          cargo doc --all --features cross-platform-docs --no-deps --document-private-items\n      - name: Prepare docs for publication\n        run: |\n          mkdir -p publish\n          mv target/doc publish/main\n          echo '<!doctype html><a href=\"volta\">volta</a>' > publish/main/index.html\n          echo '<!doctype html><a href=\"main\">main</a>' > publish/index.html\n      - name: Publish docs to GitHub pages\n        uses: JamesIves/github-pages-deploy-action@releases/v3\n        with:\n          COMMIT_MESSAGE: Publishing GitHub Pages\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          BRANCH: gh-pages\n          FOLDER: publish\n          SINGLE_COMMIT: true\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "on:\n  push:\n    tags:\n      - v*\n  pull_request:\n    branches:\n      - main\n\nname: Production\n\njobs:\n  linux:\n    name: Build - Linux\n    runs-on: ubuntu-latest\n    steps:\n      - name: Check out code\n        uses: actions/checkout@v4\n      - name: Set up docker buildx\n        uses: docker/setup-buildx-action@v2\n      - name: Build docker image\n        uses: docker/build-push-action@v3\n        with:\n          cache-from: type=gha\n          cache-to: type=gha,mode=max\n          context: ./ci/docker\n          push: false\n          load: true\n          tags: volta\n      - name: Compile and package Volta\n        run: docker run --volume ${PWD}:/root/workspace --workdir /root/workspace --rm --init --tty volta /root/workspace/ci/build-linux.sh volta-linux\n      - name: Upload release artifact\n        uses: actions/upload-artifact@v4\n        with:\n          name: linux\n          path: target/release/volta-linux.tar.gz\n\n  linux-arm:\n    name: Build - Linux ARM\n    runs-on: ubuntu-latest\n    steps:\n      - name: Check out code\n        uses: actions/checkout@v4\n      - name: Install cross-rs\n        uses: taiki-e/install-action@v2\n        with:\n          tool: cross\n      - name: Compile Volta\n        run: cross build --release --target aarch64-unknown-linux-gnu\n      - name: Package Volta\n        run: |\n          cd target/aarch64-unknown-linux-gnu/release && tar -zcvf \"volta-linux-arm.tar.gz\" volta volta-shim volta-migrate\n      - name: Upload release artifact\n        uses: actions/upload-artifact@v4\n        with:\n          name: linux-arm\n          path: target/aarch64-unknown-linux-gnu/release/volta-linux-arm.tar.gz\n\n  macos:\n    name: Build - MacOS\n    runs-on: macos-latest\n    steps:\n      - name: Check out code\n        uses: actions/checkout@v4\n      - name: Set up cargo\n        uses: actions-rust-lang/setup-rust-toolchain@v1\n        with:\n          target: aarch64-apple-darwin,x86_64-apple-darwin\n      - name: Compile and package Volta\n        run: ./ci/build-macos.sh volta-macos\n      - name: Upload release artifact\n        uses: actions/upload-artifact@v4\n        with:\n          name: macos\n          path: target/universal-apple-darwin/release/volta-macos.tar.gz\n\n  windows:\n    name: Build - Windows\n    runs-on: windows-latest\n    steps:\n      - name: Check out code\n        uses: actions/checkout@v4\n      - name: Set up cargo\n        uses: actions-rust-lang/setup-rust-toolchain@v1\n        with:\n          rustflags: \"\"\n      - name: Add cargo-wix subcommand\n        run: cargo install --locked cargo-wix\n      - name: Compile and package installer\n        run: |\n          cargo wix --nocapture --package volta --output target\\wix\\volta-windows.msi\n      - name: Create zip of binaries\n        run: powershell Compress-Archive volta*.exe volta-windows.zip\n        working-directory: ./target/release\n      - name: Upload installer\n        uses: actions/upload-artifact@v4\n        with:\n          name: windows-installer\n          path: target/wix/volta-windows.msi\n      - name: Upload zip\n        uses: actions/upload-artifact@v4\n        with:\n          name: windows-zip\n          path: target/release/volta-windows.zip\n\n  windows-arm:\n    name: Build - Windows ARM\n    runs-on: windows-latest\n    steps:\n      - name: Check out code\n        uses: actions/checkout@v4\n      - name: Set up cargo\n        uses: actions-rust-lang/setup-rust-toolchain@v1\n        with:\n          target: aarch64-pc-windows-msvc\n          rustflags: \"\"\n      - name: Add cargo-wix subcommand\n        run: cargo install --locked cargo-wix\n      - name: Compile and package installer\n        run: |\n          cargo wix --nocapture --package volta --target aarch64-pc-windows-msvc --output target\\wix\\volta-windows-arm.msi\n      - name: Create zip of binaries\n        run: powershell Compress-Archive volta*.exe volta-windows-arm.zip\n        working-directory: ./target/aarch64-pc-windows-msvc/release\n      - name: Upload installer\n        uses: actions/upload-artifact@v4\n        with:\n          name: windows-installer-arm\n          path: target/wix/volta-windows-arm.msi\n      - name: Upload zip\n        uses: actions/upload-artifact@v4\n        with:\n          name: windows-zip-arm\n          path: target/aarch64-pc-windows-msvc/release/volta-windows-arm.zip\n\n  release:\n    name: Publish release\n    runs-on: ubuntu-latest\n    needs:\n      - linux\n      - linux-arm\n      - macos\n      - windows\n      - windows-arm\n    if: github.event_name == 'push'\n    steps:\n      - name: Check out code\n        uses: actions/checkout@v4\n      - name: Determine release version\n        id: release_info\n        env:\n          TAG: ${{ github.ref }}\n        run: echo \"version=${TAG:11}\" >> $GITHUB_OUTPUT\n      - name: Fetch Linux artifact\n        uses: actions/download-artifact@v4\n        with:\n          name: linux\n          path: release\n      - name: Fetch Linux ARM artifact\n        uses: actions/download-artifact@v4\n        with:\n          name: linux-arm\n          path: release\n      - name: Fetch MacOS artifact\n        uses: actions/download-artifact@v4\n        with:\n          name: macos\n          path: release\n      - name: Fetch Windows installer\n        uses: actions/download-artifact@v4\n        with:\n          name: windows-installer\n          path: release\n      - name: Fetch Windows zip\n        uses: actions/download-artifact@v4\n        with:\n          name: windows-zip\n          path: release\n      - name: Fetch Windows ARM installer\n        uses: actions/download-artifact@v4\n        with:\n          name: windows-installer-arm\n          path: release\n      - name: Fetch Windows ARM zip\n        uses: actions/download-artifact@v4\n        with:\n          name: windows-zip-arm\n          path: release\n      - name: Show release artifacts\n        run: ls -la release\n      - name: Create draft release\n        id: create_release\n        uses: actions/create-release@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          tag_name: ${{ github.ref }}\n          release_name: ${{ github.ref }}\n          draft: true\n      - name: Upload Linux artifact\n        uses: actions/upload-release-asset@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          upload_url: ${{ steps.create_release.outputs.upload_url }}\n          asset_path: ./release/volta-linux.tar.gz\n          asset_name: volta-${{ steps.release_info.outputs.version }}-linux.tar.gz\n          asset_content_type: application/gzip\n      - name: Upload Linux ARM artifact\n        uses: actions/upload-release-asset@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          upload_url: ${{ steps.create_release.outputs.upload_url }}\n          asset_path: ./release/volta-linux-arm.tar.gz\n          asset_name: volta-${{ steps.release_info.outputs.version }}-linux-arm.tar.gz\n          asset_content_type: application/gzip\n      - name: Upload MacOS artifact\n        uses: actions/upload-release-asset@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          upload_url: ${{ steps.create_release.outputs.upload_url }}\n          asset_path: ./release/volta-macos.tar.gz\n          asset_name: volta-${{ steps.release_info.outputs.version }}-macos.tar.gz\n          asset_content_type: application/gzip\n      - name: Upload Windows installer\n        uses: actions/upload-release-asset@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          upload_url: ${{ steps.create_release.outputs.upload_url }}\n          asset_path: ./release/volta-windows.msi\n          asset_name: volta-${{ steps.release_info.outputs.version }}-windows-x86_64.msi\n          asset_content_type: application/x-msi\n      - name: Upload Windows zip\n        uses: actions/upload-release-asset@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          upload_url: ${{ steps.create_release.outputs.upload_url }}\n          asset_path: ./release/volta-windows.zip\n          asset_name: volta-${{ steps.release_info.outputs.version }}-windows.zip\n          asset_content_type: application/zip\n      - name: Upload Windows ARM installer\n        uses: actions/upload-release-asset@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          upload_url: ${{ steps.create_release.outputs.upload_url }}\n          asset_path: ./release/volta-windows-arm.msi\n          asset_name: volta-${{ steps.release_info.outputs.version }}-windows-arm64.msi\n          asset_content_type: application/x-msi\n      - name: Upload Windows ARM zip\n        uses: actions/upload-release-asset@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          upload_url: ${{ steps.create_release.outputs.upload_url }}\n          asset_path: ./release/volta-windows-arm.zip\n          asset_name: volta-${{ steps.release_info.outputs.version }}-windows-arm64.zip\n          asset_content_type: application/zip\n      - name: Upload manifest file\n        uses: actions/upload-release-asset@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          upload_url: ${{ steps.create_release.outputs.upload_url }}\n          asset_path: ./ci/volta.manifest\n          asset_name: volta.manifest\n          asset_content_type: text/plain\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "on:\n  push:\n    branches:\n      - main\n  pull_request:\n    branches:\n      - main\n\nname: Test\n\njobs:\n  tests:\n    strategy:\n      matrix:\n        os:\n          - ubuntu\n          - macos\n          - windows\n    name: Acceptance Tests (${{ matrix.os }})\n    runs-on: ${{ matrix.os }}-latest\n    env:\n      RUST_BACKTRACE: full\n    steps:\n      - name: Check out code\n        uses: actions/checkout@v4\n      - name: Set up cargo\n        uses: actions-rust-lang/setup-rust-toolchain@v1\n      - name: Run tests\n        run: |\n          cargo test --all --features mock-network\n      - name: Lint with clippy\n        run: cargo clippy\n      - name: Lint tests with clippy\n        run: |\n          cargo clippy --tests --features mock-network\n\n  smoke-tests:\n    name: Smoke Tests\n    runs-on: macos-latest\n    env:\n      RUST_BACKTRACE: full\n    steps:\n      - name: Check out code\n        uses: actions/checkout@v4\n      - name: Set up cargo\n        uses: actions-rust-lang/setup-rust-toolchain@v1\n      - name: Run tests\n        run: |\n          cargo test --test smoke --features smoke-tests -- --test-threads 1\n\n  shell-tests:\n    name: Shell Script Tests\n    runs-on: ubuntu-latest\n    steps:\n      - name: Setup BATS\n        run: sudo npm install -g bats\n      - name: Check out code\n        uses: actions/checkout@v4\n      - name: Run tests\n        run: bats dev/unix/tests/\n\n  check-formatting:\n    name: Check code formatting\n    runs-on: ubuntu-latest\n    steps:\n      - name: Check out code\n        uses: actions/checkout@v4\n      - name: Set up cargo\n        uses: actions-rust-lang/setup-rust-toolchain@v1\n      - name: Run check\n        run: |\n          cargo fmt --all --quiet -- --check\n\n  validate-installer-checksum:\n    name: Validate installer checksum\n    runs-on: ubuntu-latest\n    steps:\n      - name: Check out code\n        uses: actions/checkout@v4\n      - name: Run check\n        run: |\n          cd dev/unix\n          sha256sum --check SHASUMS256.txt\n"
  },
  {
    "path": ".gitignore",
    "content": "/target/\n**/*.rs.bk\nNotion.msi\nVolta.msi\ndev/windows/*.log\ndev/windows/Notion.wixobj\ndev/windows/Volta.wixobj\ndev/windows/Notion.wixpdb\ndev/windows/Volta.wixpdb\ndev/unix/install.sh\n/.idea/\n/rls/\n/rls*\n\n\n# Created by https://www.gitignore.io/api/intellij (and then modified heavily)\n\n### Intellij ###\n# Ignore all IDEA files. This means you may have to rebuild the project on your\n# new machines at times, but avoids checking in a bunch of files which are not\n# generally relevant to other developers.\n.idea\n\n# CMake\ncmake-build-*/\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# End of https://www.gitignore.io/api/intellij\n"
  },
  {
    "path": ".vscode/launch.json",
    "content": "{\n  \"version\": \"0.2.0\",\n  \"configurations\": [\n    {\n      \"type\": \"lldb\",\n      \"request\": \"launch\",\n      \"name\": \"Cargo run volta core\",\n      \"cargo\": {\n        \"args\": [\"run\", \"--bin\", \"volta\"],\n        \"filter\": {\n          \"kind\": \"bin\",\n          \"name\": \"volta\"\n        }\n      },\n      \"program\": \"${cargo:program}\",\n      \"args\": [],\n      \"sourceLanguages\": [\"rust\"]\n    },\n    {\n      \"type\": \"lldb\",\n      \"request\": \"launch\",\n      \"name\": \"Cargo test volta core\",\n      \"cargo\": {\n        \"args\": [\n          \"test\",\n          \"--lib\",\n          \"--no-run\",\n          \"--package\",\n          \"volta-core\",\n          \"--\",\n          \"--test-threads\",\n          \"1\"\n        ],\n        \"filter\": {\n          \"kind\": \"lib\",\n          \"name\": \"volta-core\"\n        }\n      },\n      \"program\": \"${cargo:program}\",\n      \"args\": [],\n      \"sourceLanguages\": [\"rust\"]\n    }\n  ]\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"editor.formatOnSave\": true\n}\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to making participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, gender identity and expression, level of experience,\nnationality, personal appearance, race, religion, or sexual identity and\norientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or\n  advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic\n  address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces\nwhen an individual is representing the project or its community. Examples of\nrepresenting a project or community include using an official project e-mail\naddress, posting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event. Representation of a project may be\nfurther defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting the project team at david.herman@gmail.com. All\ncomplaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,\navailable at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html\n\n[homepage]: https://www.contributor-covenant.org\n"
  },
  {
    "path": "COMPATIBILITY.md",
    "content": "# Compatibility\n\nVolta currently tests against the following platforms, and will treat it as a breaking change to drop support for them:\n\n- macOS\n    - x86-64\n    - Apple Silicon\n- Linux x86-64\n- Windows x86-64\n\nWe compile release artifacts compatible with the following, and likewise will treat it as a breaking change to drop support for them:\n\n- macOS v11\n- RHEL and CentOS v7\n- Windows 10\n\nIn general, Volta should build and run against any other modern hardware and operating system supported by stable Rust, and we will make a best effort not to break them. However, we do *not* include them in our SemVer guarantees or test against them.\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "Please see https://docs.volta.sh/contributing/\n"
  },
  {
    "path": "Cargo.toml",
    "content": "[package]\nname = \"volta\"\nversion = \"2.0.2\"\nauthors = [\"David Herman <david.herman@gmail.com>\", \"Charles Pierce <cpierce.grad@gmail.com>\"]\nlicense = \"BSD-2-Clause\"\nrepository = \"https://github.com/volta-cli/volta\"\nedition = \"2021\"\n\n[features]\ncross-platform-docs = [\"volta-core/cross-platform-docs\"]\nmock-network = [\"mockito\", \"volta-core/mock-network\"]\nvolta-dev = []\nsmoke-tests = []\n\n[[bin]]\nname = \"volta-shim\"\npath = \"src/volta-shim.rs\"\n\n[[bin]]\nname = \"volta-migrate\"\npath = \"src/volta-migrate.rs\"\n\n[profile.release]\nlto = \"fat\"\ncodegen-units = 1\n\n[dependencies]\nvolta-core = { path = \"crates/volta-core\" }\nserde = { version = \"1.0\", features = [\"derive\"] }\nserde_json = \"1.0.135\"\nonce_cell = \"1.19.0\"\nlog = { version = \"0.4\", features = [\"std\"] }\nnode-semver = \"2\"\nclap = { version = \"4.5.24\", features = [\"color\", \"derive\", \"wrap_help\"] }\nclap_complete = \"4.5.46\"\nmockito = { version = \"0.31.1\", optional = true }\ntextwrap = \"0.16.1\"\nwhich = \"7.0.1\"\ndirs = \"6.0.0\"\nvolta-migrate = { path = \"crates/volta-migrate\" }\n\n[target.'cfg(windows)'.dependencies]\nwinreg = \"0.53.0\"\n\n[dev-dependencies]\nhamcrest2 = \"0.3.0\"\nenvoy = \"0.1.3\"\nci_info = \"0.14.14\"\nheaders = \"0.4\"\ncfg-if = \"1.0\"\ntest-support = { path = \"crates/test-support\" }\n\n[workspace]\n"
  },
  {
    "path": "LICENSE",
    "content": "BSD 2-CLAUSE LICENSE\n\nCopyright (c) 2017, The Volta Contributors.\nAll rights reserved.\n\nThis product includes:\n\nContributions from LinkedIn Corporation\nCopyright (c) 2017, LinkedIn Corporation.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\n   list of conditions and the following disclaimer.\n2. Redistributions in binary form must reproduce the above copyright notice,\n   this list of conditions and the following disclaimer in the documentation\n   and/or other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND\nON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\nThe views and conclusions contained in the software and documentation are those\nof the authors and should not be interpreted as representing official policies,\neither expressed or implied, of the FreeBSD Project.\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n  <a href=\"https://www.volta.sh/\">\n    <img alt=\"Volta\" src=\"./volta.png?raw=true\" width=\"360\">\n  </a>\n</p>\n\n<p align=\"center\">\n  The Hassle-Free JavaScript Tool Manager\n</p>\n\n<p align=\"center\">\n  <img alt=\"Production Build Status\" src=\"https://github.com/volta-cli/volta/workflows/Production/badge.svg\" />\n  <a href=\"https://github.com/volta-cli/volta/actions?query=workflow%3ATest\">\n    <img alt=\"Test Status\" src=\"https://github.com/volta-cli/volta/workflows/Test/badge.svg\" />\n  </a>\n</p>\n\n---\n\n> [!IMPORTANT]\n> **Volta is unmaintained.** Everything that works today should continue to do so for the foreseeable future, so if it is working for you, there is no particular *urgency* to migrate to another tool, but we will not be able to address breakages from new OS releases or other changes in the ecosystem, so you should put it on your maintenance roadmap at some point. We recommend migrating to [`mise`](https://mise.jdx.dev/). See [issue #2080](https://github.com/volta-cli/volta/issues/2080).\n\n---\n\n\n**Fast:** Install and run any JS tool quickly and seamlessly! Volta is built in Rust and ships as a snappy static binary.\n\n**Reliable:** Ensure everyone in your project has the same tools—without interfering with their workflow.\n\n**Universal:** No matter the package manager, Node runtime, or OS, one command is all you need: `volta install`.\n\n## Features\n\n- Speed 🚀\n- Seamless, per-project version switching\n- Cross-platform support, including Windows and all Unix shells\n- Support for multiple package managers\n- Stable tool installation—no reinstalling on every Node upgrade!\n- Extensibility hooks for site-specific customization\n\n## Installing Volta\n\nRead the [Getting Started Guide](https://docs.volta.sh/guide/getting-started) on our website for detailed instructions on how to install Volta.\n\n## Using Volta\n\nRead the [Understanding Volta Guide](https://docs.volta.sh/guide/understanding) on our website for detailed instructions on how to use Volta.\n\n## Contributing to Volta\n\nContributions are always welcome, no matter how large or small. Substantial feature ideas should be proposed as an [RFC](https://github.com/volta-cli/rfcs). Before contributing, please read the [code of conduct](CODE_OF_CONDUCT.md).\n\nSee the [Contributing Guide](https://docs.volta.sh/contributing/) on our website for detailed instructions on how to contribute to Volta.\n\n## Who is using Volta?\n\n<table>\n  <tbody>\n    <tr>\n      <td align=\"center\">\n        <a href=\"https://github.com/microsoft/TypeScript\" target=\"_blank\">\n          <img src=\"https://raw.githubusercontent.com/microsoft/TypeScript-Website/v2/packages/typescriptlang-org/static/branding/ts-logo-512.svg\" alt=\"TypeScript\" width=\"100\" height=\"100\">\n        </a>\n      </td>\n      <td align=\"center\">\n        <a href=\"https://github.com/getsentry/sentry-javascript\" target=\"_blank\">\n          <img src=\"https://avatars.githubusercontent.com/u/1396951?s=100\" alt=\"Sentry\" width=\"100\" height=\"100\">\n        </a>\n      </td>\n    </tr>\n  </tbody>\n</table>\n\nSee [here](https://sourcegraph.com/search?q=context:global+%22volta%22+file:package.json&patternType=literal) for more Volta users.\n"
  },
  {
    "path": "RELEASES.md",
    "content": "# Version 2.0.2\n\n- Dependency updates\n- Improvements to header handling for HTTP requests (#1822, #1877)\n\n# Version 2.0.1\n\n- Improved accuracy of Node download progress bar on Windows (#1833)\n- You should no longer run into errors about needing the VC++ Runtime on Windows (#1844)\n- The data provided when installing a new Node version is now more relevant and accurate (#1846, #1848)\n- Increased performance to make Volta even more responsive in typical use (#1849)\n- `volta run` will now correctly handle flags in more situations (#1857)\n\n# Version 2.0.0\n\n- 🚨 (BREAKING) 🚨 We upgraded the version of Rust used to build Volta, which drops support for older versions of glibc & Linux kernel. See [the Rust announcement from August 2022](https://blog.rust-lang.org/2022/08/01/Increasing-glibc-kernel-requirements.html) for details about the supported versions. Notably, this means that we no longer support CentOS 6 (#1611)\n- 🚨 (BREAKING) 🚨 Due to costs and changes in the code signing process, we have dropped the code signing for the Windows installer. We now recommend using `winget` to install Volta on Windows (#1650)\n- 🎉 (NEW) 🎉 We now ship a pre-built binary for ARM Linux & ARM Windows (#1696, #1801)\n- Volta no longer requires Developer Mode to be enabled on Windows (#1755)\n- `volta uninstall` now provides better help & error messages to describe its use and limitations (#1628, #1786)\n- Volta will now use a universal binary on Mac, rather than separate Intel- & ARM-specific builds (#1635)\n- Switched to installing profile scripts into `.zshenv` by default, rather than `.zshrc` (#1657)\n- Added a default shim for the `yarnpkg` command, which is an alias of `yarn` (#1670)\n- Added a new `--very-verbose` flag to enable even more logging (note: we haven't yet implemented much additional logging) (#1815)\n- Simplified the fetching process to remove an extra network request and resolve hangs (#1812)\n- Several dependency upgrades and clean-up refactors from @tottoto\n\n# Version 1.1.1\n\n- Experimental support for pnpm (requires `VOLTA_FEATURE_PNPM` environment variable) (#1273)\n- Fix to correctly import native root certificates (#1375)\n- Better detection of executables provided by `yarn` (#1388, #1393)\n\n# Version 1.1.0\n\n- Added support for pinning / installing Yarn 3+ (#1305)\n- Improved portability and installer effectiveness by removing dependency on OpenSSL (#1214)\n\n# Version 1.0.8\n\n- Fix for malformed `bin` entries when installing global packages (#997)\n- Dependency updates\n\n# Version 1.0.7\n\n- Added build for Linux distros with OpenSSL 3.0 (#1211)\n\n# Version 1.0.6\n\n- Fixed panic when `stdout` is closed (#1058)\n- Disabled global package interception when `--prefix` is provided (#1171)\n- Numerous dependency updates\n\n# Version 1.0.5\n\n- Added error when attempting to install Node using `nvm` syntax (#1020)\n- Avoid modifying shell config if the environment is already correct (#990)\n- Prevent trying to read OS-generated files as package configs (#981)\n\n# Version 1.0.4\n\n- Fetch native Apple silicon versions of Node when available (#974)\n\n# Version 1.0.3\n\n- Fix pinning of `npm@bundled` when there is a custom default npm version (#957)\n- Use correct binary name for scoped packages with a string `bin` entry in `package.json` (#969)\n\n# Version 1.0.2\n\n- Fix issues where `volta list` wasn't showing the correct information in all cases (#778, #926)\n- Make detection of tool name case-insensitive on Windows (#941)\n- Fix problem with `npm link` in a scoped package under npm 7 (#945)\n\n# Version 1.0.1\n\n- Create Native build for Apple Silicon machines (#915, #917)\n\n# Version 1.0.0\n\n- Support for `npm link` (#888, #889, #891)\n- Support for `npm update -g` and `yarn global upgrade` (#895)\n- Improvements in the handling of `npm` and `yarn` commands (#886, #887)\n\n# Version 0.9.3\n\n- Various fixes to event plugin logic (#892, #894, #897)\n\n# Version 0.9.2\n\n- Correctly detect Volta binary installation directory (#864)\n\n# Version 0.9.1\n\n- Fix an issue with installing globals using npm 7 (#858)\n\n# Version 0.9.0\n\n- Support Proxies through environment variables (#809, #851)\n- Avoid unnecessary `exists` calls for files (#834)\n- Rework package installs to allow for directly calling package manager (#848, #849)\n- **Breaking Change**: Remove support for `packages` hooks (#817)\n\n# Version 0.8.7\n\n- Support fetching older versions of Yarn (#771)\n- Correctly detect `zsh` environment with `ZDOTDIR` variable (#799)\n- Prevent race conditions when installing tools (#684, #796)\n\n# Version 0.8.6\n\n- Improve parsing of `engines` when installing a package (#791, #792)\n\n# Version 0.8.5\n\n- Improve the stability of installing tools on systems with virus scanning software (#784)\n- Make `volta uninstall` work correctly when the original install had an issue (#787)\n\n# Version 0.8.4\n\n- Add `{{filename}}` and `{{ext}}` (extension) replacements for `template` hooks (#774)\n- Show better error when running `volta install yarn` without a Node version available (#763)\n\n# Version 0.8.3\n\n- Fix bug preventing custom `npm` versions from launching on Windows (#777)\n- Fix for completions in `zsh` for `volta list` (#772)\n\n# Version 0.8.2\n\n- Add support for workspaces through the `extends` key in `package.json` (#755)\n- Improve `volta setup` to make profile scripts more shareable across machines (#756)\n\n# Version 0.8.1\n\n- Fix panic when running `volta completions zsh` (#746)\n- Improve startup latency by reducing binary size (#732, #733, #734, #735)\n\n# Version 0.8.0\n\n- Support for pinning / installing custom versions of `npm` (#691)\n- New command: `volta run` which will let you run one-off commands using custom versions of Node / Yarn / npm (#713)\n- Added default pretty formatter for `volta list` (#697)\n- Improved setup of Volta environment to make it work in more scenarios (#666, #725)\n- Bug fixes and performance improvements (#683, #701, #703, #704, #707, #717)\n\n# Version 0.7.2\n\n- Added `npm.cmd`, `npx.cmd`, and `yarn.cmd` on Windows to support tools that look for CMD files specifically (#663)\n- Updated `volta setup` to also ensure that the shim symlinks are set up correctly (#662)\n\n# Version 0.7.1\n\n- Added warning when attempting to `volta uninstall` a package you don't have installed (#638)\n- Added informational message about pinned project version when running `volta install` (#646)\n- `volta completions` will attempt to create the output directory if it doesn't exist (#647)\n- `volta install` will correctly handle script files that have CRLF as the line ending (#644)\n\n# Version 0.7.0\n\n- Removed deprecated commands `volta activate`, `volta deactivate`, and `volta current` (#620, #559)\n- Simplified installer behavior and added data directory migration support (#619)\n- Removed reliance on UNC paths when executing node scripts (#637)\n\n# Version 0.6.8\n\n- You can now use tagged versions when installing a tool with `volta install` (#604)\n- `volta install <tool>` will now prefer LTS Node when pinning a version (#604)\n\n# Version 0.6.7\n\n- `volta pin` will no longer remove a closing newline from `package.json` (#603)\n- New environment variable `VOLTA_BYPASS` will allow you to temporarily disable Volta shims (#603)\n\n# Version 0.6.6\n\n- Node and Yarn can now both be pinned in the same command `volta pin node yarn` (#593)\n- Windows installer will now work on minimal Windows installs (e.g. Windows Sandbox) (#592)\n\n# Version 0.6.5\n\n- `volta list` Now always outputs to stdout, regardless of how it is called (#581)\n- DEPRECATION: `volta activate` and `volta deactivate` are deprecated and will be removed in a future version (#571)\n\n# Version 0.6.4\n\n- `volta install` now works for installing packages from a private, authenticated registry (#554)\n- `volta install` now has better diagnostic messages when things go wrong (#548)\n\n# Version 0.6.3\n\n- `volta install` will no longer error when installing a scoped binary package (#537)\n\n# Version 0.6.2\n\n- Added `volta list` command for inspecting the available tools and versions (#461)\n\n# Version 0.6.1\n\n- Windows users will see a spinner instead of a � when Volta is loading data (#511)\n- Interrupting a tool with Ctrl+C will correctly wait for the tool to exit (#513)\n\n# Version 0.6.0\n\n- Allow installing 3rd-party binaries from private registries (#469)\n\n# Version 0.5.7\n\n- Prevent corrupting local cache by downloading tools to temp directory (#498)\n\n# Version 0.5.6\n\n- Improve expected behavior with Yarn in projects (#470)\n- Suppress an erroneous \"toolchain\" key warning message (#486)\n\n# Version 0.5.5\n\n- Proper support for relative paths in Bin hooks (#468)\n- Diagnostic messages for shims with `VOLTA_LOGLEVEL=debug` (#466)\n- Preserve user order for multiple tool installs (#479)\n\n# Version 0.5.4\n\n- Show additional diagnostic messages when run with `--verbose` (#455)\n\n# Version 0.5.3\n\n- Prevent unnecessary warning output when not running interactively (#451)\n- Fix a bug in load script for fish shell on Linux (#456)\n- Improve wrapping behavior for warning messages (#453)\n\n# Version 0.5.2\n\n- Improve error messages when running a project-local binary fails (#426)\n- Fix execution of user binaries on Windows (#445)\n\n# Version 0.5.1\n\n- Add per-project hooks configuration in `<PROJECT_ROOT>/.volta/hooks.json` (#411)\n- Support backwards compatibility with `toolchain` key in `package.json` (#434)\n\n# Version 0.5.0\n\n- Rename to Volta: The JavaScript Launcher ⚡️\n- Change `package.json` key to `volta` from `toolchain` (#413)\n- Update `volta completions` behavior to be more usable (#416)\n- Improve `volta which` to correctly find user tools (#419)\n- Remove unneeded lookups of `package.json` files (#420)\n- Cleanup of error messages and extraneous output (#421, #422)\n\n# Version 0.4.1\n\n- Allow tool executions to pass through to the system if no Notion platform exists (#372)\n- Improve installer support for varied Linux distros\n\n# Version 0.4.0\n\n- Update `notion install` to use `tool@version` formatting for specifying a tool (#383, #403)\n- Further error message improvements (#344, #395, #399, #400)\n- Clean up bugs around installing and running packages (#368, #390, #394, #396)\n- Include success messages when running `notion install` and `notion pin` (#397)\n\n# Version 0.3.0\n\n- Support `lts` pseudo-version for Node (#331)\n- Error message improvements\n- Add `notion install` and `notion uninstall` for package binaries\n- Remove autoshimming\n\n# Version 0.2.2\n\n- Add `notion which` command (#293)\n- Show progress when fetching Notion installer (#279)\n- Improved styling for usage information (#283)\n- Support for `fish` shell (#266, #290)\n- Consolidate binaries, for a ~2/3 size reduction of Notion installer (#274)\n\n# Version 0.2.1\n\n- Move preventing globals behind a feature flag (#273)\n\n# Version 0.2.0\n\n- Add support for OpenSSL 1.1.1 (#267)\n- Fix: ensure temp files are on the same volume (#257)\n- Intercept global package installations (#248)\n- Fix: make npx compatible with prelrease versions of npm (#239)\n- Fix: make `notion deactivate` work infallibly, without loading any files (#237)\n- Fix: make `\"npm\"` key optional in `package.json` (#233)\n- Fix: publish latest Notion version via self-hosted endpoint (#230)\n- Fix: eliminate excessive fetching and scanning for exact versions (#227)\n- Rename `notion use` to `notion pin` (#226)\n- Base filesystem isolation on `NOTION_HOME` env var (#224)\n- Fix: robust progress bar logic (#221)\n- Use JSON for internal state files (#220)\n- Support for npm and npx (#205)\n- Changes to directory layout (#181)\n\n# Version 0.1.5\n\n- Autoshimming! (#163)\n- `notion deactivate` also unsets `NOTION_HOME` (#195)\n- Implemented `notion activate` (#201)\n- Fix for Yarn over-fetching bug (#203)\n\n# Version 0.1.4\n\n- Fix for `package.json` parsing bug (#156)\n\n# Version 0.1.3\n\n- Fix for Yarn path bug (#153)\n\n# Version 0.1.2\n\n- Correct logic for computing `latest` version of Node (#144)\n- Don't crash if cache dir was deleted (#138)\n- Improved tests (#135)\n\n# Version 0.1.1\n\n- Support for specifying `latest` as a version specifier (#133)\n- Suppress scary-looking symlink warnings on reinstall (#132)\n- Clearer error message for not-yet-implemented `notion install somebin` (#131)\n- Support optional `v` prefix to version specifiers (#130)\n\n# Version 0.1.0\n\nFirst pre-release, supporting:\n\n- macOS and Linux (bash-only)\n- `notion install` (Node and Yarn only, no package binaries)\n- `notion use`\n- Proof-of-concept plugin API\n"
  },
  {
    "path": "ci/build-linux.sh",
    "content": "#!/bin/bash\n\nset -e\n\n# Activate the upgraded versions of GCC and binutils\n# See https://linux.web.cern.ch/centos7/docs/softwarecollections/#inst\nsource /opt/rh/devtoolset-8/enable\n\necho \"Building Volta\"\n\ncargo build --release\n\necho \"Packaging Binaries\"\n\ncd target/release\ntar -zcvf \"$1.tar.gz\" volta volta-shim volta-migrate\n"
  },
  {
    "path": "ci/build-macos.sh",
    "content": "#!/bin/bash\n\nset -e\n\necho \"Building Volta\"\n\nMACOSX_DEPLOYMENT_TARGET=11.0 cargo build --release --target=aarch64-apple-darwin\nMACOSX_DEPLOYMENT_TARGET=11.0 cargo build --release --target=x86_64-apple-darwin\n\necho \"Packaging Binaries\"\n\nmkdir -p target/universal-apple-darwin/release\n\nfor exe in volta volta-shim volta-migrate\ndo\n    lipo -create -output target/universal-apple-darwin/release/$exe target/x86_64-apple-darwin/release/$exe target/aarch64-apple-darwin/release/$exe\ndone\n\ncd target/universal-apple-darwin/release\n\ntar -zcvf \"$1.tar.gz\" volta volta-shim volta-migrate\n"
  },
  {
    "path": "ci/docker/Dockerfile",
    "content": "FROM cern/cc7-base\n\n# This repo file references a URL that is no longer valid. It also isn't used by the build\n# toolchain, so we can safely remove it entirely\nRUN rm /etc/yum.repos.d/epel.repo\n\n# https://linux.web.cern.ch/centos7/docs/softwarecollections/#inst\n# Tools needed for the build and setup process\nRUN yum -y install wget tar\n# Fetch the repo information for the devtoolset repo\nRUN yum install -y centos-release-scl\n# Install more recent GCC and binutils, to allow us to compile\nRUN yum install -y devtoolset-8\n\nRUN curl https://sh.rustup.rs -sSf | sh -s -- -y\nENV PATH=\"/root/.cargo/bin:${PATH}\"\n"
  },
  {
    "path": "ci/volta.manifest",
    "content": "volta\nvolta-shim\nvolta-migrate\n"
  },
  {
    "path": "crates/archive/Cargo.toml",
    "content": "[package]\nname = \"archive\"\nversion = \"0.1.0\"\nauthors = [\"David Herman <david.herman@gmail.com>\"]\nedition = \"2021\"\n\n[dependencies]\nflate2 = \"1.0\"\ntar = \"0.4.13\"\n# Set features manually to drop usage of `time` crate: we do not rely on that\n# set of capabilities, and it has a vulnerability. We also don't need to use\n# every single compression algorithm feature since we are only downloading\n# Node as a zip file\nzip_rs = { version = \"=2.1.6\", package = \"zip\", default-features = false, features = [\"deflate\", \"bzip2\"] }\ntee = \"0.1.0\"\nfs-utils = { path = \"../fs-utils\" }\nprogress-read = { path = \"../progress-read\" }\nverbatim = \"0.1\"\ncfg-if = \"1.0\"\nheaders = \"0.4\"\nthiserror = \"2.0.0\"\nattohttpc = { version = \"0.28\", default-features = false, features = [\"json\", \"compress\", \"tls-rustls-native-roots\"] }\nlog = { version = \"0.4\", features = [\"std\"] }\n"
  },
  {
    "path": "crates/archive/src/lib.rs",
    "content": "//! This crate provides types for fetching and unpacking compressed\n//! archives in tarball or zip format.\nuse std::fs::File;\nuse std::path::Path;\n\nuse attohttpc::header::HeaderMap;\nuse headers::{ContentLength, Header, HeaderMapExt};\nuse thiserror::Error;\n\nmod tarball;\nmod zip;\n\npub use crate::tarball::Tarball;\npub use crate::zip::Zip;\n\n/// Error type for this crate\n#[derive(Error, Debug)]\npub enum ArchiveError {\n    #[error(\"HTTP failure ({0})\")]\n    HttpError(attohttpc::StatusCode),\n\n    #[error(\"HTTP header '{0}' not found\")]\n    MissingHeaderError(&'static attohttpc::header::HeaderName),\n\n    #[error(\"unexpected content length in HTTP response: {0}\")]\n    UnexpectedContentLengthError(u64),\n\n    #[error(\"{0}\")]\n    IoError(#[from] std::io::Error),\n\n    #[error(\"{0}\")]\n    AttohttpcError(#[from] attohttpc::Error),\n\n    #[error(\"{0}\")]\n    ZipError(#[from] zip_rs::result::ZipError),\n}\n\n/// Metadata describing whether an archive comes from a local or remote origin.\n#[derive(Copy, Clone)]\npub enum Origin {\n    Local,\n    Remote,\n}\n\npub trait Archive {\n    fn compressed_size(&self) -> u64;\n\n    /// Unpacks the zip archive to the specified destination folder.\n    fn unpack(\n        self: Box<Self>,\n        dest: &Path,\n        progress: &mut dyn FnMut(&(), usize),\n    ) -> Result<(), ArchiveError>;\n\n    fn origin(&self) -> Origin;\n}\n\ncfg_if::cfg_if! {\n    if #[cfg(unix)] {\n        /// Load an archive in the native OS-preferred format from the specified file.\n        ///\n        /// On Windows, the preferred format is zip. On Unixes, the preferred format\n        /// is tarball.\n        pub fn load_native(source: File) -> Result<Box<dyn Archive>, ArchiveError> {\n            Tarball::load(source)\n        }\n\n        /// Fetch a remote archive in the native OS-preferred format from the specified\n        /// URL and store its results at the specified file path.\n        ///\n        /// On Windows, the preferred format is zip. On Unixes, the preferred format\n        /// is tarball.\n        pub fn fetch_native(url: &str, cache_file: &Path) -> Result<Box<dyn Archive>, ArchiveError> {\n            Tarball::fetch(url, cache_file)\n        }\n    } else if #[cfg(windows)] {\n        /// Load an archive in the native OS-preferred format from the specified file.\n        ///\n        /// On Windows, the preferred format is zip. On Unixes, the preferred format\n        /// is tarball.\n        pub fn load_native(source: File) -> Result<Box<dyn Archive>, ArchiveError> {\n            Zip::load(source)\n        }\n\n        /// Fetch a remote archive in the native OS-preferred format from the specified\n        /// URL and store its results at the specified file path.\n        ///\n        /// On Windows, the preferred format is zip. On Unixes, the preferred format\n        /// is tarball.\n        pub fn fetch_native(url: &str, cache_file: &Path) -> Result<Box<dyn Archive>, ArchiveError> {\n            Zip::fetch(url, cache_file)\n        }\n    } else {\n        compile_error!(\"Unsupported OS (expected 'unix' or 'windows').\");\n    }\n}\n\n/// Determines the length of an HTTP response's content in bytes, using\n/// the HTTP `\"Content-Length\"` header.\nfn content_length(headers: &HeaderMap) -> Result<u64, ArchiveError> {\n    headers\n        .typed_get()\n        .map(|ContentLength(v)| v)\n        .ok_or_else(|| ArchiveError::MissingHeaderError(ContentLength::name()))\n}\n"
  },
  {
    "path": "crates/archive/src/tarball.rs",
    "content": "//! Provides types and functions for fetching and unpacking a Node installation\n//! tarball in Unix operating systems.\n\nuse std::fs::File;\nuse std::io::Read;\nuse std::path::Path;\n\nuse super::{content_length, Archive, ArchiveError, Origin};\nuse flate2::read::GzDecoder;\nuse fs_utils::ensure_containing_dir_exists;\nuse progress_read::ProgressRead;\nuse tee::TeeReader;\n\n/// A Node installation tarball.\npub struct Tarball {\n    compressed_size: u64,\n    data: Box<dyn Read>,\n    origin: Origin,\n}\n\nimpl Tarball {\n    /// Loads a tarball from the specified file.\n    pub fn load(source: File) -> Result<Box<dyn Archive>, ArchiveError> {\n        let compressed_size = source.metadata()?.len();\n        Ok(Box::new(Tarball {\n            compressed_size,\n            data: Box::new(source),\n            origin: Origin::Local,\n        }))\n    }\n\n    /// Initiate fetching of a tarball from the given URL, returning a\n    /// tarball that can be streamed (and that tees its data to a local\n    /// file as it streams).\n    pub fn fetch(url: &str, cache_file: &Path) -> Result<Box<dyn Archive>, ArchiveError> {\n        let (status, headers, response) = attohttpc::get(url).send()?.split();\n\n        if !status.is_success() {\n            return Err(ArchiveError::HttpError(status));\n        }\n\n        let compressed_size = content_length(&headers)?;\n\n        ensure_containing_dir_exists(&cache_file)?;\n        let file = File::create(cache_file)?;\n        let data = Box::new(TeeReader::new(response, file));\n\n        Ok(Box::new(Tarball {\n            compressed_size,\n            data,\n            origin: Origin::Remote,\n        }))\n    }\n}\n\nimpl Archive for Tarball {\n    fn compressed_size(&self) -> u64 {\n        self.compressed_size\n    }\n    fn unpack(\n        self: Box<Self>,\n        dest: &Path,\n        progress: &mut dyn FnMut(&(), usize),\n    ) -> Result<(), ArchiveError> {\n        let decoded = GzDecoder::new(ProgressRead::new(self.data, (), progress));\n        let mut tarball = tar::Archive::new(decoded);\n        tarball.unpack(dest)?;\n        Ok(())\n    }\n    fn origin(&self) -> Origin {\n        self.origin\n    }\n}\n\n#[cfg(test)]\npub mod tests {\n\n    use crate::tarball::Tarball;\n    use std::fs::File;\n    use std::path::PathBuf;\n\n    fn fixture_path(fixture_dir: &str) -> PathBuf {\n        let mut cargo_manifest_dir = PathBuf::from(env!(\"CARGO_MANIFEST_DIR\"));\n        cargo_manifest_dir.push(\"fixtures\");\n        cargo_manifest_dir.push(fixture_dir);\n        cargo_manifest_dir\n    }\n\n    #[test]\n    fn test_load() {\n        let mut test_file_path = fixture_path(\"tarballs\");\n        test_file_path.push(\"test-file.tar.gz\");\n        let test_file = File::open(test_file_path).expect(\"Couldn't open test file\");\n        let tarball = Tarball::load(test_file).expect(\"Failed to load tarball\");\n\n        assert_eq!(tarball.compressed_size(), 402);\n    }\n}\n"
  },
  {
    "path": "crates/archive/src/zip.rs",
    "content": "//! Provides types and functions for fetching and unpacking a Node installation\n//! zip file in Windows operating systems.\n\nuse std::fs::File;\nuse std::io::Read;\nuse std::path::Path;\n\nuse super::{content_length, ArchiveError};\nuse fs_utils::ensure_containing_dir_exists;\nuse progress_read::ProgressRead;\nuse tee::TeeReader;\nuse verbatim::PathExt;\nuse zip_rs::unstable::stream::ZipStreamReader;\n\nuse super::Archive;\nuse super::Origin;\n\npub struct Zip {\n    compressed_size: u64,\n    data: Box<dyn Read>,\n    origin: Origin,\n}\n\nimpl Zip {\n    /// Loads a cached Node zip archive from the specified file.\n    pub fn load(source: File) -> Result<Box<dyn Archive>, ArchiveError> {\n        let compressed_size = source.metadata()?.len();\n\n        Ok(Box::new(Zip {\n            compressed_size,\n            data: Box::new(source),\n            origin: Origin::Local,\n        }))\n    }\n\n    /// Initiate fetching of a Node zip archive from the given URL, returning\n    /// a `Remote` data source.\n    pub fn fetch(url: &str, cache_file: &Path) -> Result<Box<dyn Archive>, ArchiveError> {\n        let (status, headers, response) = attohttpc::get(url).send()?.split();\n\n        if !status.is_success() {\n            return Err(ArchiveError::HttpError(status));\n        }\n\n        let compressed_size = content_length(&headers)?;\n\n        ensure_containing_dir_exists(&cache_file)?;\n        let file = File::create(cache_file)?;\n        let data = Box::new(TeeReader::new(response, file));\n\n        Ok(Box::new(Zip {\n            compressed_size,\n            data,\n            origin: Origin::Remote,\n        }))\n    }\n}\n\nimpl Archive for Zip {\n    fn compressed_size(&self) -> u64 {\n        self.compressed_size\n    }\n    fn unpack(\n        self: Box<Self>,\n        dest: &Path,\n        progress: &mut dyn FnMut(&(), usize),\n    ) -> Result<(), ArchiveError> {\n        // Use a verbatim path to avoid the legacy Windows 260 byte path limit.\n        let dest: &Path = &dest.to_verbatim();\n        let zip = ZipStreamReader::new(ProgressRead::new(self.data, (), progress));\n        zip.extract(dest)?;\n        Ok(())\n    }\n    fn origin(&self) -> Origin {\n        self.origin\n    }\n}\n\n#[cfg(test)]\npub mod tests {\n\n    use crate::zip::Zip;\n    use std::fs::File;\n    use std::path::PathBuf;\n\n    fn fixture_path(fixture_dir: &str) -> PathBuf {\n        let mut cargo_manifest_dir = PathBuf::from(env!(\"CARGO_MANIFEST_DIR\"));\n        cargo_manifest_dir.push(\"fixtures\");\n        cargo_manifest_dir.push(fixture_dir);\n        cargo_manifest_dir\n    }\n\n    #[test]\n    fn test_load() {\n        let mut test_file_path = fixture_path(\"zips\");\n        test_file_path.push(\"test-file.zip\");\n        let test_file = File::open(test_file_path).expect(\"Couldn't open test file\");\n        let zip = Zip::load(test_file).expect(\"Failed to load zip file\");\n\n        assert_eq!(zip.compressed_size(), 214);\n    }\n}\n"
  },
  {
    "path": "crates/fs-utils/Cargo.toml",
    "content": "[package]\nname = \"fs-utils\"\nversion = \"0.1.0\"\nauthors = [\"Michael Stewart <mikrostew@gmail.com>\"]\nedition = \"2021\"\n\n[dependencies]\n"
  },
  {
    "path": "crates/fs-utils/src/lib.rs",
    "content": "//! This crate provides utilities for operating on the filesystem.\n\nuse std::fs;\nuse std::io;\nuse std::path::Path;\n\n/// This creates the parent directory of the input path, assuming the input path is a file.\npub fn ensure_containing_dir_exists<P: AsRef<Path>>(path: &P) -> io::Result<()> {\n    path.as_ref()\n        .parent()\n        .ok_or_else(|| {\n            io::Error::new(\n                io::ErrorKind::NotFound,\n                format!(\n                    \"Could not determine directory information for {}\",\n                    path.as_ref().display()\n                ),\n            )\n        })\n        .and_then(fs::create_dir_all)\n}\n"
  },
  {
    "path": "crates/progress-read/Cargo.toml",
    "content": "[package]\nname = \"progress-read\"\nversion = \"0.1.0\"\nauthors = [\"David Herman <david.herman@gmail.com>\"]\nedition = \"2021\"\n\n[dependencies]\n"
  },
  {
    "path": "crates/progress-read/src/lib.rs",
    "content": "//! This crate provides an adapter for the `std::io::Read` trait to\n//! allow reporting incremental progress to a callback function.\n\nuse std::io::{self, Read, Seek, SeekFrom};\n\n/// A reader that reports incremental progress while reading.\npub struct ProgressRead<R: Read, T, F: FnMut(&T, usize) -> T> {\n    source: R,\n    accumulator: T,\n    progress: F,\n}\n\nimpl<R: Read, T, F: FnMut(&T, usize) -> T> Read for ProgressRead<R, T, F> {\n    /// Read some bytes from the underlying reader into the specified buffer,\n    /// and report progress to the progress callback. The progress callback is\n    /// passed the current value of the accumulator as its first argument and\n    /// the number of bytes read as its second argument. The result of the\n    /// progress callback is stored as the updated value of the accumulator,\n    /// to be passed to the next invocation of the callback.\n    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {\n        let len = self.source.read(buf)?;\n        let new_accumulator = {\n            let progress = &mut self.progress;\n            progress(&self.accumulator, len)\n        };\n        self.accumulator = new_accumulator;\n        Ok(len)\n    }\n}\n\nimpl<R: Read, T, F: FnMut(&T, usize) -> T> ProgressRead<R, T, F> {\n    /// Construct a new progress reader with the specified underlying reader,\n    /// initial value for an accumulator, and progress callback.\n    pub fn new(source: R, init: T, progress: F) -> ProgressRead<R, T, F> {\n        ProgressRead {\n            source,\n            accumulator: init,\n            progress,\n        }\n    }\n}\n\nimpl<R: Read + Seek, T, F: FnMut(&T, usize) -> T> Seek for ProgressRead<R, T, F> {\n    fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {\n        self.source.seek(pos)\n    }\n}\n"
  },
  {
    "path": "crates/test-support/Cargo.toml",
    "content": "[package]\nname = \"test-support\"\nversion = \"0.1.0\"\nauthors = [\"David Herman <david.herman@gmail.com>\"]\nedition = \"2021\"\n\n[dependencies]\nhamcrest2 = \"0.3.0\"\nserde_json = { version = \"1.0.135\" }\nthiserror = \"2.0.9\"\n"
  },
  {
    "path": "crates/test-support/src/lib.rs",
    "content": "//! Utilities to use with acceptance tests in Volta.\n\n#[macro_export]\nmacro_rules! ok_or_panic {\n    { $e:expr } => {\n        match $e {\n            Ok(x) => x,\n            Err(err) => panic!(\"{} failed with {}\", stringify!($e), err),\n        }\n    };\n}\n\npub mod matchers;\npub mod paths;\npub mod process;\n"
  },
  {
    "path": "crates/test-support/src/matchers.rs",
    "content": "use std::fmt;\nuse std::process::Output;\nuse std::str;\n\nuse crate::process::ProcessBuilder;\n\nuse hamcrest2::core::{MatchResult, Matcher};\nuse serde_json::{self, Value};\n\n#[derive(Clone)]\npub struct Execs {\n    expect_stdout: Option<String>,\n    expect_stderr: Option<String>,\n    expect_exit_code: Option<i32>,\n    expect_stdout_contains: Vec<String>,\n    expect_stderr_contains: Vec<String>,\n    expect_either_contains: Vec<String>,\n    expect_stdout_contains_n: Vec<(String, usize)>,\n    expect_stdout_not_contains: Vec<String>,\n    expect_stderr_not_contains: Vec<String>,\n    expect_stderr_unordered: Vec<String>,\n    expect_neither_contains: Vec<String>,\n    expect_json: Option<Vec<Value>>,\n}\n\nimpl Execs {\n    /// Verify that stdout is equal to the given lines.\n    /// See `lines_match` for supported patterns.\n    pub fn with_stdout<S: ToString>(mut self, expected: S) -> Execs {\n        self.expect_stdout = Some(expected.to_string());\n        self\n    }\n\n    /// Verify that stderr is equal to the given lines.\n    /// See `lines_match` for supported patterns.\n    pub fn with_stderr<S: ToString>(mut self, expected: S) -> Execs {\n        self._with_stderr(&expected);\n        self\n    }\n\n    fn _with_stderr(&mut self, expected: &dyn ToString) {\n        self.expect_stderr = Some(expected.to_string());\n    }\n\n    /// Verify the exit code from the process.\n    pub fn with_status(mut self, expected: i32) -> Execs {\n        self.expect_exit_code = Some(expected);\n        self\n    }\n\n    /// Verify that stdout contains the given contiguous lines somewhere in\n    /// its output.\n    /// See `lines_match` for supported patterns.\n    pub fn with_stdout_contains<S: ToString>(mut self, expected: S) -> Execs {\n        self.expect_stdout_contains.push(expected.to_string());\n        self\n    }\n\n    /// Verify that stderr contains the given contiguous lines somewhere in\n    /// its output.\n    /// See `lines_match` for supported patterns.\n    pub fn with_stderr_contains<S: ToString>(mut self, expected: S) -> Execs {\n        self.expect_stderr_contains.push(expected.to_string());\n        self\n    }\n\n    /// Verify that either stdout or stderr contains the given contiguous\n    /// lines somewhere in its output.\n    /// See `lines_match` for supported patterns.\n    pub fn with_either_contains<S: ToString>(mut self, expected: S) -> Execs {\n        self.expect_either_contains.push(expected.to_string());\n        self\n    }\n\n    /// Verify that stdout contains the given contiguous lines somewhere in\n    /// its output, and should be repeated `number` times.\n    /// See `lines_match` for supported patterns.\n    pub fn with_stdout_contains_n<S: ToString>(mut self, expected: S, number: usize) -> Execs {\n        self.expect_stdout_contains_n\n            .push((expected.to_string(), number));\n        self\n    }\n\n    /// Verify that stdout does not contain the given contiguous lines.\n    /// See `lines_match` for supported patterns.\n    /// See note on `with_stderr_does_not_contain`.\n    pub fn with_stdout_does_not_contain<S: ToString>(mut self, expected: S) -> Execs {\n        self.expect_stdout_not_contains.push(expected.to_string());\n        self\n    }\n\n    /// Verify that stderr does not contain the given contiguous lines.\n    /// See `lines_match` for supported patterns.\n    ///\n    /// Care should be taken when using this method because there is a\n    /// limitless number of possible things that *won't* appear.  A typo means\n    /// your test will pass without verifying the correct behavior. If\n    /// possible, write the test first so that it fails, and then implement\n    /// your fix/feature to make it pass.\n    pub fn with_stderr_does_not_contain<S: ToString>(mut self, expected: S) -> Execs {\n        self.expect_stderr_not_contains.push(expected.to_string());\n        self\n    }\n\n    /// Verify that all of the stderr output is equal to the given lines,\n    /// ignoring the order of the lines.\n    /// See `lines_match` for supported patterns.\n    /// This is useful when checking the output of `cargo build -v` since\n    /// the order of the output is not always deterministic.\n    /// Recommend use `with_stderr_contains` instead unless you really want to\n    /// check *every* line of output.\n    ///\n    /// Be careful when using patterns such as `[..]`, because you may end up\n    /// with multiple lines that might match, and this is not smart enough to\n    /// do anything like longest-match.  For example, avoid something like:\n    ///     [RUNNING] `rustc [..]\n    ///     [RUNNING] `rustc --crate-name foo [..]\n    /// This will randomly fail if the other crate name is `bar`, and the\n    /// order changes.\n    pub fn with_stderr_unordered<S: ToString>(mut self, expected: S) -> Execs {\n        self.expect_stderr_unordered.push(expected.to_string());\n        self\n    }\n\n    /// Verify the JSON output matches the given JSON.\n    /// Typically used when testing cargo commands that emit JSON.\n    /// Each separate JSON object should be separated by a blank line.\n    /// Example:\n    ///     assert_that(\n    ///         p.cargo(\"metadata\"),\n    ///         execs().with_json(r#\"\n    ///             {\"example\": \"abc\"}\n    ///             {\"example\": \"def\"}\n    ///         \"#)\n    ///      );\n    /// Objects should match in the order given.\n    /// The order of arrays is ignored.\n    /// Strings support patterns described in `lines_match`.\n    /// Use `{...}` to match any object.\n    pub fn with_json(mut self, expected: &str) -> Execs {\n        self.expect_json = Some(\n            expected\n                .split(\"\\n\\n\")\n                .map(|obj| obj.parse().unwrap())\n                .collect(),\n        );\n        self\n    }\n\n    fn match_output(&self, actual: &Output) -> MatchResult {\n        self.match_status(actual)\n            .and(self.match_stdout(actual))\n            .and(self.match_stderr(actual))\n    }\n\n    fn match_status(&self, actual: &Output) -> MatchResult {\n        match self.expect_exit_code {\n            None => Ok(()),\n            Some(code) if actual.status.code() == Some(code) => Ok(()),\n            Some(_) => Err(format!(\n                \"exited with {}\\n--- stdout\\n{}\\n--- stderr\\n{}\",\n                actual.status,\n                String::from_utf8_lossy(&actual.stdout),\n                String::from_utf8_lossy(&actual.stderr)\n            )),\n        }\n    }\n\n    fn match_stdout(&self, actual: &Output) -> MatchResult {\n        self.match_std(\n            self.expect_stdout.as_ref(),\n            &actual.stdout,\n            \"stdout\",\n            &actual.stderr,\n            MatchKind::Exact,\n        )?;\n        for expect in self.expect_stdout_contains.iter() {\n            self.match_std(\n                Some(expect),\n                &actual.stdout,\n                \"stdout\",\n                &actual.stderr,\n                MatchKind::Partial,\n            )?;\n        }\n        for expect in self.expect_stderr_contains.iter() {\n            self.match_std(\n                Some(expect),\n                &actual.stderr,\n                \"stderr\",\n                &actual.stdout,\n                MatchKind::Partial,\n            )?;\n        }\n        for &(ref expect, number) in self.expect_stdout_contains_n.iter() {\n            self.match_std(\n                Some(expect),\n                &actual.stdout,\n                \"stdout\",\n                &actual.stderr,\n                MatchKind::PartialN(number),\n            )?;\n        }\n        for expect in self.expect_stdout_not_contains.iter() {\n            self.match_std(\n                Some(expect),\n                &actual.stdout,\n                \"stdout\",\n                &actual.stderr,\n                MatchKind::NotPresent,\n            )?;\n        }\n        for expect in self.expect_stderr_not_contains.iter() {\n            self.match_std(\n                Some(expect),\n                &actual.stderr,\n                \"stderr\",\n                &actual.stdout,\n                MatchKind::NotPresent,\n            )?;\n        }\n        for expect in self.expect_stderr_unordered.iter() {\n            self.match_std(\n                Some(expect),\n                &actual.stderr,\n                \"stderr\",\n                &actual.stdout,\n                MatchKind::Unordered,\n            )?;\n        }\n        for expect in self.expect_neither_contains.iter() {\n            self.match_std(\n                Some(expect),\n                &actual.stdout,\n                \"stdout\",\n                &actual.stdout,\n                MatchKind::NotPresent,\n            )?;\n\n            self.match_std(\n                Some(expect),\n                &actual.stderr,\n                \"stderr\",\n                &actual.stderr,\n                MatchKind::NotPresent,\n            )?;\n        }\n\n        for expect in self.expect_either_contains.iter() {\n            let match_std = self.match_std(\n                Some(expect),\n                &actual.stdout,\n                \"stdout\",\n                &actual.stdout,\n                MatchKind::Partial,\n            );\n            let match_err = self.match_std(\n                Some(expect),\n                &actual.stderr,\n                \"stderr\",\n                &actual.stderr,\n                MatchKind::Partial,\n            );\n\n            if let (Err(_), Err(_)) = (match_std, match_err) {\n                return Err(format!(\n                    \"expected to find:\\n\\\n                     {}\\n\\n\\\n                     did not find in either output.\",\n                    expect\n                ));\n            }\n        }\n\n        if let Some(ref objects) = self.expect_json {\n            let stdout = str::from_utf8(&actual.stdout)\n                .map_err(|_| \"stdout was not utf8 encoded\".to_owned())?;\n            let lines = stdout\n                .lines()\n                .filter(|line| line.starts_with('{'))\n                .collect::<Vec<_>>();\n            if lines.len() != objects.len() {\n                return Err(format!(\n                    \"expected {} json lines, got {}, stdout:\\n{}\",\n                    objects.len(),\n                    lines.len(),\n                    stdout\n                ));\n            }\n            for (obj, line) in objects.iter().zip(lines) {\n                self.match_json(obj, line)?;\n            }\n        }\n        Ok(())\n    }\n\n    fn match_stderr(&self, actual: &Output) -> MatchResult {\n        self.match_std(\n            self.expect_stderr.as_ref(),\n            &actual.stderr,\n            \"stderr\",\n            &actual.stdout,\n            MatchKind::Exact,\n        )\n    }\n\n    fn match_std(\n        &self,\n        expected: Option<&String>,\n        actual: &[u8],\n        description: &str,\n        extra: &[u8],\n        kind: MatchKind,\n    ) -> MatchResult {\n        let out = match expected {\n            Some(out) => out,\n            None => return Ok(()),\n        };\n        let actual = match str::from_utf8(actual) {\n            Err(..) => return Err(format!(\"{} was not utf8 encoded\", description)),\n            Ok(actual) => actual,\n        };\n        // Let's not deal with \\r\\n vs \\n on windows...\n        let actual = actual.replace('\\r', \"\");\n        let actual = actual.replace('\\t', \"<tab>\");\n\n        match kind {\n            MatchKind::Exact => {\n                let a = actual.lines();\n                let e = out.lines();\n\n                let diffs = self.diff_lines(a, e, false);\n                if diffs.is_empty() {\n                    Ok(())\n                } else {\n                    Err(format!(\n                        \"differences:\\n\\\n                         {}\\n\\n\\\n                         other output:\\n\\\n                         `{}`\",\n                        diffs.join(\"\\n\"),\n                        String::from_utf8_lossy(extra)\n                    ))\n                }\n            }\n            MatchKind::Partial => {\n                let mut a = actual.lines();\n                let e = out.lines();\n\n                let mut diffs = self.diff_lines(a.clone(), e.clone(), true);\n                #[allow(clippy::while_let_on_iterator)]\n                while let Some(..) = a.next() {\n                    let a = self.diff_lines(a.clone(), e.clone(), true);\n                    if a.len() < diffs.len() {\n                        diffs = a;\n                    }\n                }\n                if diffs.is_empty() {\n                    Ok(())\n                } else {\n                    Err(format!(\n                        \"expected to find:\\n\\\n                         {}\\n\\n\\\n                         did not find in output:\\n\\\n                         {}\",\n                        out, actual\n                    ))\n                }\n            }\n            MatchKind::PartialN(number) => {\n                let mut a = actual.lines();\n                let e = out.lines();\n\n                let mut matches = 0;\n\n                loop {\n                    if self.diff_lines(a.clone(), e.clone(), true).is_empty() {\n                        matches += 1;\n                    }\n\n                    if a.next().is_none() {\n                        break;\n                    }\n                }\n\n                if matches == number {\n                    Ok(())\n                } else {\n                    Err(format!(\n                        \"expected to find {} occurrences:\\n\\\n                         {}\\n\\n\\\n                         did not find in output:\\n\\\n                         {}\",\n                        number, out, actual\n                    ))\n                }\n            }\n            MatchKind::NotPresent => {\n                let mut a = actual.lines();\n                let e = out.lines();\n\n                let mut diffs = self.diff_lines(a.clone(), e.clone(), true);\n                #[allow(clippy::while_let_on_iterator)]\n                while let Some(..) = a.next() {\n                    let a = self.diff_lines(a.clone(), e.clone(), true);\n                    if a.len() < diffs.len() {\n                        diffs = a;\n                    }\n                }\n                if diffs.is_empty() {\n                    Err(format!(\n                        \"expected not to find:\\n\\\n                         {}\\n\\n\\\n                         but found in output:\\n\\\n                         {}\",\n                        out, actual\n                    ))\n                } else {\n                    Ok(())\n                }\n            }\n            MatchKind::Unordered => {\n                let mut a = actual.lines().collect::<Vec<_>>();\n                let e = out.lines();\n\n                for e_line in e {\n                    match a.iter().position(|a_line| lines_match(e_line, a_line)) {\n                        Some(index) => a.remove(index),\n                        None => {\n                            return Err(format!(\n                                \"Did not find expected line:\\n\\\n                                 {}\\n\\\n                                 Remaining available output:\\n\\\n                                 {}\\n\",\n                                e_line,\n                                a.join(\"\\n\")\n                            ));\n                        }\n                    };\n                }\n                if !a.is_empty() {\n                    Err(format!(\n                        \"Output included extra lines:\\n\\\n                         {}\\n\",\n                        a.join(\"\\n\")\n                    ))\n                } else {\n                    Ok(())\n                }\n            }\n        }\n    }\n\n    fn match_json(&self, expected: &Value, line: &str) -> MatchResult {\n        let actual = match line.parse() {\n            Err(e) => return Err(format!(\"invalid json, {}:\\n`{}`\", e, line)),\n            Ok(actual) => actual,\n        };\n\n        match find_mismatch(expected, &actual) {\n            Some((expected_part, actual_part)) => Err(format!(\n                \"JSON mismatch\\nExpected:\\n{}\\nWas:\\n{}\\nExpected part:\\n{}\\nActual part:\\n{}\\n\",\n                serde_json::to_string_pretty(expected).unwrap(),\n                serde_json::to_string_pretty(&actual).unwrap(),\n                serde_json::to_string_pretty(expected_part).unwrap(),\n                serde_json::to_string_pretty(actual_part).unwrap(),\n            )),\n            None => Ok(()),\n        }\n    }\n\n    fn diff_lines<'a>(\n        &self,\n        actual: str::Lines<'a>,\n        expected: str::Lines<'a>,\n        partial: bool,\n    ) -> Vec<String> {\n        let actual = actual.take(if partial {\n            expected.clone().count()\n        } else {\n            usize::MAX\n        });\n        zip_all(actual, expected)\n            .enumerate()\n            .filter_map(|(i, (a, e))| match (a, e) {\n                (Some(a), Some(e)) => {\n                    if lines_match(e, a) {\n                        None\n                    } else {\n                        Some(format!(\"{:3} - |{}|\\n    + |{}|\\n\", i, e, a))\n                    }\n                }\n                (Some(a), None) => Some(format!(\"{:3} -\\n    + |{}|\\n\", i, a)),\n                (None, Some(e)) => Some(format!(\"{:3} - |{}|\\n    +\\n\", i, e)),\n                (None, None) => panic!(\"Cannot get here\"),\n            })\n            .collect()\n    }\n}\n\n#[derive(Debug, PartialEq, Eq, Clone, Copy)]\nenum MatchKind {\n    Exact,\n    Partial,\n    PartialN(usize),\n    NotPresent,\n    Unordered,\n}\n\n/// Compare a line with an expected pattern.\n/// - Use `[..]` as a wildcard to match 0 or more characters on the same line\n///   (similar to `.*` in a regex).\n/// - Use `[EXE]` to optionally add `.exe` on Windows (empty string on other\n///   platforms).\n/// - There is a wide range of macros (such as `[COMPILING]` or `[WARNING]`)\n///   to match cargo's \"status\" output and allows you to ignore the alignment.\n///   See `substitute_macros` for a complete list of macros.\npub fn lines_match(expected: &str, actual: &str) -> bool {\n    // Let's not deal with / vs \\ (windows...)\n    let expected = expected.replace('\\\\', \"/\");\n    let mut actual: &str = &actual.replace('\\\\', \"/\");\n    let expected = substitute_macros(&expected);\n    for (i, part) in expected.split(\"[..]\").enumerate() {\n        match actual.find(part) {\n            Some(j) => {\n                if i == 0 && j != 0 {\n                    return false;\n                }\n                actual = &actual[j + part.len()..];\n            }\n            None => return false,\n        }\n    }\n    actual.is_empty() || expected.ends_with(\"[..]\")\n}\n\n#[test]\nfn lines_match_works() {\n    assert!(lines_match(\"a b\", \"a b\"));\n    assert!(lines_match(\"a[..]b\", \"a b\"));\n    assert!(lines_match(\"a[..]\", \"a b\"));\n    assert!(lines_match(\"[..]\", \"a b\"));\n    assert!(lines_match(\"[..]b\", \"a b\"));\n\n    assert!(!lines_match(\"[..]b\", \"c\"));\n    assert!(!lines_match(\"b\", \"c\"));\n    assert!(!lines_match(\"b\", \"cb\"));\n}\n\n// Compares JSON object for approximate equality.\n// You can use `[..]` wildcard in strings (useful for OS dependent things such\n// as paths).  You can use a `\"{...}\"` string literal as a wildcard for\n// arbitrary nested JSON (useful for parts of object emitted by other programs\n// (e.g. rustc) rather than Cargo itself).  Arrays are sorted before comparison.\nfn find_mismatch<'a>(expected: &'a Value, actual: &'a Value) -> Option<(&'a Value, &'a Value)> {\n    use serde_json::Value::*;\n    match (expected, actual) {\n        (Number(l), Number(r)) if l == r => None,\n        (Bool(l), Bool(r)) if l == r => None,\n        (String(l), String(r)) if lines_match(l, r) => None,\n        (Array(l), Array(r)) => {\n            if l.len() != r.len() {\n                return Some((expected, actual));\n            }\n\n            let mut l = l.iter().collect::<Vec<_>>();\n            let mut r = r.iter().collect::<Vec<_>>();\n\n            l.retain(\n                |l| match r.iter().position(|r| find_mismatch(l, r).is_none()) {\n                    Some(i) => {\n                        r.remove(i);\n                        false\n                    }\n                    None => true,\n                },\n            );\n\n            if !l.is_empty() {\n                assert!(!r.is_empty());\n                Some((l[0], r[0]))\n            } else {\n                assert_eq!(r.len(), 0);\n                None\n            }\n        }\n        (Object(l), Object(r)) => {\n            let same_keys = l.len() == r.len() && l.keys().all(|k| r.contains_key(k));\n            if !same_keys {\n                return Some((expected, actual));\n            }\n\n            l.values()\n                .zip(r.values())\n                .find_map(|(l, r)| find_mismatch(l, r))\n        }\n        (Null, Null) => None,\n        // magic string literal \"{...}\" acts as wildcard for any sub-JSON\n        (String(l), _) if l == \"{...}\" => None,\n        _ => Some((expected, actual)),\n    }\n}\n\nstruct ZipAll<I1: Iterator, I2: Iterator> {\n    first: I1,\n    second: I2,\n}\n\nimpl<T, I1: Iterator<Item = T>, I2: Iterator<Item = T>> Iterator for ZipAll<I1, I2> {\n    type Item = (Option<T>, Option<T>);\n    fn next(&mut self) -> Option<(Option<T>, Option<T>)> {\n        let first = self.first.next();\n        let second = self.second.next();\n\n        match (first, second) {\n            (None, None) => None,\n            (a, b) => Some((a, b)),\n        }\n    }\n}\n\nfn zip_all<T, I1: Iterator<Item = T>, I2: Iterator<Item = T>>(a: I1, b: I2) -> ZipAll<I1, I2> {\n    ZipAll {\n        first: a,\n        second: b,\n    }\n}\n\nimpl fmt::Display for Execs {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        write!(f, \"execs\")\n    }\n}\n\nimpl fmt::Debug for Execs {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        write!(f, \"execs\")\n    }\n}\n\nimpl Matcher<ProcessBuilder> for Execs {\n    fn matches(&self, mut process: ProcessBuilder) -> MatchResult {\n        self.matches(&mut process)\n    }\n}\n\nimpl<'a> Matcher<&'a mut ProcessBuilder> for Execs {\n    fn matches(&self, process: &'a mut ProcessBuilder) -> MatchResult {\n        println!(\"running {}\", process);\n        let res = process.exec_with_output();\n\n        match res {\n            Ok(out) => self.match_output(&out),\n            Err(err) => {\n                if let Some(out) = &err.output {\n                    return self.match_output(out);\n                }\n                Err(format!(\"could not exec process {}: {}\", process, err))\n            }\n        }\n    }\n}\n\nimpl Matcher<Output> for Execs {\n    fn matches(&self, output: Output) -> MatchResult {\n        self.match_output(&output)\n    }\n}\n\npub fn execs() -> Execs {\n    Execs {\n        expect_stdout: None,\n        expect_stderr: None,\n        expect_exit_code: Some(0),\n        expect_stdout_contains: Vec::new(),\n        expect_stderr_contains: Vec::new(),\n        expect_either_contains: Vec::new(),\n        expect_stdout_contains_n: Vec::new(),\n        expect_stdout_not_contains: Vec::new(),\n        expect_stderr_not_contains: Vec::new(),\n        expect_stderr_unordered: Vec::new(),\n        expect_neither_contains: Vec::new(),\n        expect_json: None,\n    }\n}\n\nfn substitute_macros(input: &str) -> String {\n    let macros = [\n        (\"[RUNNING]\", \"     Running\"),\n        (\"[COMPILING]\", \"   Compiling\"),\n        (\"[CHECKING]\", \"    Checking\"),\n        (\"[CREATED]\", \"     Created\"),\n        (\"[FINISHED]\", \"    Finished\"),\n        (\"[ERROR]\", \"error:\"),\n        (\"[WARNING]\", \"warning:\"),\n        (\"[DOCUMENTING]\", \" Documenting\"),\n        (\"[FRESH]\", \"       Fresh\"),\n        (\"[UPDATING]\", \"    Updating\"),\n        (\"[ADDING]\", \"      Adding\"),\n        (\"[REMOVING]\", \"    Removing\"),\n        (\"[DOCTEST]\", \"   Doc-tests\"),\n        (\"[PACKAGING]\", \"   Packaging\"),\n        (\"[DOWNLOADING]\", \" Downloading\"),\n        (\"[UPLOADING]\", \"   Uploading\"),\n        (\"[VERIFYING]\", \"   Verifying\"),\n        (\"[ARCHIVING]\", \"   Archiving\"),\n        (\"[INSTALLING]\", \"  Installing\"),\n        (\"[REPLACING]\", \"   Replacing\"),\n        (\"[UNPACKING]\", \"   Unpacking\"),\n        (\"[SUMMARY]\", \"     Summary\"),\n        (\"[FIXING]\", \"      Fixing\"),\n        (\"[EXE]\", if cfg!(windows) { \".exe\" } else { \"\" }),\n    ];\n    let mut result = input.to_owned();\n    for &(pat, subst) in &macros {\n        result = result.replace(pat, subst)\n    }\n    result\n}\n"
  },
  {
    "path": "crates/test-support/src/paths.rs",
    "content": "use std::cell::Cell;\nuse std::env;\nuse std::fs;\nuse std::path::{Path, PathBuf};\nuse std::sync::atomic::{AtomicUsize, Ordering};\nuse std::sync::Once;\n\nstatic SMOKE_TEST_DIR: &str = \"smoke_test\";\nstatic NEXT_ID: AtomicUsize = AtomicUsize::new(0);\n\nthread_local!(static TASK_ID: usize = NEXT_ID.fetch_add(1, Ordering::SeqCst));\n\n// creates the root directory for the tests (once), and\n// initializes the root and home directories for the current task\nfn init() {\n    static GLOBAL_INIT: Once = Once::new();\n    thread_local!(static LOCAL_INIT: Cell<bool> = Cell::new(false));\n    GLOBAL_INIT.call_once(|| {\n        global_root().mkdir_p();\n    });\n    LOCAL_INIT.with(|i| {\n        if i.get() {\n            return;\n        }\n        i.set(true);\n        root().rm_rf();\n        home().mkdir_p();\n    })\n}\n\n// the root directory for the smoke tests, in `target/smoke_test`\nfn global_root() -> PathBuf {\n    let mut path = ok_or_panic! { env::current_exe() };\n    path.pop(); // chop off exe name\n    path.pop(); // chop off 'debug'\n\n    // If `cargo test` is run manually then our path looks like\n    // `target/debug/foo`, in which case our `path` is already pointing at\n    // `target`. If, however, `cargo test --target $target` is used then the\n    // output is `target/$target/debug/foo`, so our path is pointing at\n    // `target/$target`. Here we conditionally pop the `$target` name.\n    if path.file_name().and_then(|s| s.to_str()) != Some(\"target\") {\n        path.pop();\n    }\n\n    path.join(SMOKE_TEST_DIR)\n}\n\npub fn root() -> PathBuf {\n    init();\n    global_root().join(TASK_ID.with(|my_id| format!(\"t{}\", my_id)))\n}\n\npub fn home() -> PathBuf {\n    root().join(\"home\")\n}\n\nenum Remove {\n    File,\n    Dir,\n}\nimpl Remove {\n    fn to_str(&self) -> &'static str {\n        match *self {\n            Remove::File => \"remove file\",\n            Remove::Dir => \"remove dir\",\n        }\n    }\n\n    fn at(&self, path: &Path) {\n        if cfg!(windows) {\n            let mut p = ok_or_panic!(path.metadata()).permissions();\n            // This lint rule is not applicable: this is in a `cfg!(windows)` block.\n            #[allow(clippy::permissions_set_readonly_false)]\n            p.set_readonly(false);\n            ok_or_panic! { fs::set_permissions(path, p) };\n        }\n        match *self {\n            Remove::File => fs::remove_file(path),\n            Remove::Dir => fs::remove_dir_all(path), // ensure all dir contents are removed\n        }\n        .unwrap_or_else(|e| {\n            panic!(\"failed to {} {}: {}\", self.to_str(), path.display(), e);\n        })\n    }\n}\n\npub trait PathExt {\n    fn rm(&self);\n    fn rm_rf(&self);\n    fn rm_contents(&self);\n    fn ensure_empty(&self);\n    fn mkdir_p(&self);\n}\n\nimpl PathExt for Path {\n    // delete a file if it exists\n    fn rm(&self) {\n        if !self.exists() {\n            return;\n        }\n        // On windows we can't remove a readonly file, and git will\n        // often clone files as readonly. As a result, we have some\n        // special logic to remove readonly files on windows.\n        Remove::File.at(self);\n    }\n\n    /* Technically there is a potential race condition, but we don't\n     * care all that much for our tests\n     */\n    fn rm_rf(&self) {\n        if !self.exists() {\n            return;\n        }\n        self.rm_contents();\n        Remove::Dir.at(self);\n    }\n\n    // remove directory contents but not the directory itself\n    fn rm_contents(&self) {\n        for file in ok_or_panic! { fs::read_dir(self) } {\n            let file = ok_or_panic! { file };\n            if file.file_type().map(|m| m.is_dir()).unwrap_or(false) {\n                file.path().rm_rf();\n            } else {\n                file.path().rm();\n            }\n        }\n    }\n\n    // ensure the directory is created and empty\n    fn ensure_empty(&self) {\n        self.mkdir_p();\n        self.rm_contents();\n    }\n\n    // create all paths up to the input path\n    fn mkdir_p(&self) {\n        fs::create_dir_all(self)\n            .unwrap_or_else(|e| panic!(\"failed to mkdir_p {}: {}\", self.display(), e))\n    }\n}\n"
  },
  {
    "path": "crates/test-support/src/process.rs",
    "content": "use std::collections::HashMap;\nuse std::env;\nuse std::ffi::{OsStr, OsString};\nuse std::fmt;\nuse std::path::Path;\nuse std::process::{Command, ExitStatus, Output};\nuse std::str;\n\nuse thiserror::Error;\n\n/// A builder object for an external process, similar to `std::process::Command`.\n#[derive(Clone, Debug)]\npub struct ProcessBuilder {\n    /// The program to execute.\n    program: OsString,\n    /// A list of arguments to pass to the program.\n    args: Vec<OsString>,\n    /// Any environment variables that should be set for the program.\n    env: HashMap<String, Option<OsString>>,\n    /// Which directory to run the program from.\n    cwd: Option<OsString>,\n}\n\nimpl fmt::Display for ProcessBuilder {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        write!(f, \"`{}\", self.program.to_string_lossy())?;\n\n        for arg in &self.args {\n            write!(f, \" {}\", arg.to_string_lossy())?;\n        }\n\n        write!(f, \"`\")\n    }\n}\n\nimpl ProcessBuilder {\n    /// (chainable) Set the executable for the process.\n    pub fn program<T: AsRef<OsStr>>(&mut self, program: T) -> &mut ProcessBuilder {\n        self.program = program.as_ref().to_os_string();\n        self\n    }\n\n    /// (chainable) Add an arg to the args list.\n    pub fn arg<T: AsRef<OsStr>>(&mut self, arg: T) -> &mut ProcessBuilder {\n        self.args.push(arg.as_ref().to_os_string());\n        self\n    }\n\n    /// (chainable) Add many args to the args list.\n    pub fn args<T: AsRef<OsStr>>(&mut self, arguments: &[T]) -> &mut ProcessBuilder {\n        self.args\n            .extend(arguments.iter().map(|t| t.as_ref().to_os_string()));\n        self\n    }\n\n    /// (chainable) Replace args with new args list\n    pub fn args_replace<T: AsRef<OsStr>>(&mut self, arguments: &[T]) -> &mut ProcessBuilder {\n        self.args = arguments\n            .iter()\n            .map(|t| t.as_ref().to_os_string())\n            .collect();\n        self\n    }\n\n    /// (chainable) Set the current working directory of the process\n    pub fn cwd<T: AsRef<OsStr>>(&mut self, path: T) -> &mut ProcessBuilder {\n        self.cwd = Some(path.as_ref().to_os_string());\n        self\n    }\n\n    /// (chainable) Set an environment variable for the process.\n    pub fn env<T: AsRef<OsStr>>(&mut self, key: &str, val: T) -> &mut ProcessBuilder {\n        self.env\n            .insert(key.to_string(), Some(val.as_ref().to_os_string()));\n        self\n    }\n\n    /// (chainable) Unset an environment variable for the process.\n    pub fn env_remove(&mut self, key: &str) -> &mut ProcessBuilder {\n        self.env.insert(key.to_string(), None);\n        self\n    }\n\n    /// Get the executable name.\n    pub fn get_program(&self) -> &OsString {\n        &self.program\n    }\n\n    /// Get the program arguments\n    pub fn get_args(&self) -> &[OsString] {\n        &self.args\n    }\n\n    /// Get the current working directory for the process\n    pub fn get_cwd(&self) -> Option<&Path> {\n        self.cwd.as_ref().map(Path::new)\n    }\n\n    /// Get an environment variable as the process will see it (will inherit from environment\n    /// unless explicitally unset).\n    pub fn get_env(&self, var: &str) -> Option<OsString> {\n        self.env\n            .get(var)\n            .cloned()\n            .or_else(|| Some(env::var_os(var)))\n            .and_then(|s| s)\n    }\n\n    /// Get all environment variables explicitly set or unset for the process (not inherited\n    /// vars).\n    pub fn get_envs(&self) -> &HashMap<String, Option<OsString>> {\n        &self.env\n    }\n\n    /// Run the process, waiting for completion, and mapping non-success exit codes to an error.\n    pub fn exec(&self) -> Result<(), ProcessError> {\n        let mut command = self.build_command();\n\n        let exit = match command.status() {\n            Ok(e) => e,\n            Err(_) => {\n                return Err(process_error(\n                    &format!(\"could not execute process {}\", self),\n                    None,\n                    None,\n                ));\n            }\n        };\n\n        if exit.success() {\n            Ok(())\n        } else {\n            Err(process_error(\n                &format!(\"process didn't exit successfully: {}\", self),\n                Some(exit),\n                None,\n            ))\n        }\n    }\n\n    /// Execute the process, returning the stdio output, or an error if non-zero exit status.\n    pub fn exec_with_output(&self) -> Result<Output, ProcessError> {\n        let mut command = self.build_command();\n\n        let output = match command.output() {\n            Ok(o) => o,\n            Err(_) => {\n                return Err(process_error(\n                    &format!(\"could not execute process {}\", self),\n                    None,\n                    None,\n                ));\n            }\n        };\n\n        if output.status.success() {\n            Ok(output)\n        } else {\n            Err(process_error(\n                &format!(\"process didn't exit successfully: {}\", self),\n                Some(output.status),\n                Some(&output),\n            ))\n        }\n    }\n\n    /// Converts ProcessBuilder into a `std::process::Command`\n    pub fn build_command(&self) -> Command {\n        let mut command = Command::new(&self.program);\n        if let Some(cwd) = self.get_cwd() {\n            command.current_dir(cwd);\n        }\n        for arg in &self.args {\n            command.arg(arg);\n        }\n        for (k, v) in &self.env {\n            match *v {\n                Some(ref v) => {\n                    command.env(k, v);\n                }\n                None => {\n                    command.env_remove(k);\n                }\n            }\n        }\n        command\n    }\n}\n\n/// A helper function to create a `ProcessBuilder`.\npub fn process<T: AsRef<OsStr>>(cmd: T) -> ProcessBuilder {\n    ProcessBuilder {\n        program: cmd.as_ref().to_os_string(),\n        args: Vec::new(),\n        cwd: None,\n        env: HashMap::new(),\n    }\n}\n\n#[derive(Debug, Error)]\n#[error(\"{desc}\")]\npub struct ProcessError {\n    pub desc: String,\n    pub exit: Option<ExitStatus>,\n    pub output: Option<Output>,\n}\n\npub fn process_error(\n    msg: &str,\n    status: Option<ExitStatus>,\n    output: Option<&Output>,\n) -> ProcessError {\n    let exit = match status {\n        Some(s) => status_to_string(s),\n        None => \"never executed\".to_string(),\n    };\n    let mut desc = format!(\"{} ({})\", &msg, exit);\n\n    if let Some(out) = output {\n        match str::from_utf8(&out.stdout) {\n            Ok(s) if !s.trim().is_empty() => {\n                desc.push_str(\"\\n--- stdout\\n\");\n                desc.push_str(s);\n            }\n            Ok(..) | Err(..) => {}\n        }\n        match str::from_utf8(&out.stderr) {\n            Ok(s) if !s.trim().is_empty() => {\n                desc.push_str(\"\\n--- stderr\\n\");\n                desc.push_str(s);\n            }\n            Ok(..) | Err(..) => {}\n        }\n    }\n\n    return ProcessError {\n        desc,\n        exit: status,\n        output: output.cloned(),\n    };\n\n    fn status_to_string(status: ExitStatus) -> String {\n        status.to_string()\n    }\n}\n"
  },
  {
    "path": "crates/validate-npm-package-name/Cargo.toml",
    "content": "[package]\nname = \"validate-npm-package-name\"\nversion = \"0.1.0\"\nauthors = [\"Chris Krycho <hello@chriskrycho.com>\"]\nedition = \"2021\"\n\n[lib]\n\n[dependencies]\nonce_cell = \"1.19.0\"\npercent-encoding = \"2.1.0\"\nregex = \"1.1.6\"\n"
  },
  {
    "path": "crates/validate-npm-package-name/src/lib.rs",
    "content": "//! A Rust implementation of the validation rules from the core JS package\n//! [`validate-npm-package-name`](https://github.com/npm/validate-npm-package-name/).\n\nuse once_cell::sync::Lazy;\nuse percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};\nuse regex::Regex;\n\n/// The set of characters to encode, matching the characters encoded by\n/// [`encodeURIComponent`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent#description)\nstatic ENCODE_URI_SET: &AsciiSet = &NON_ALPHANUMERIC\n    .remove(b'-')\n    .remove(b'_')\n    .remove(b'.')\n    .remove(b'!')\n    .remove(b'~')\n    .remove(b'*')\n    .remove(b'\\'')\n    .remove(b'(')\n    .remove(b')');\n\nstatic SCOPED_PACKAGE: Lazy<Regex> =\n    Lazy::new(|| Regex::new(r\"^(?:@([^/]+?)[/])?([^/]+?)$\").expect(\"regex is valid\"));\nstatic SPECIAL_CHARS: Lazy<Regex> = Lazy::new(|| Regex::new(r\"[~'!()*]\").expect(\"regex is valid\"));\nconst BLACKLIST: [&str; 2] = [\"node_modules\", \"favicon.ico\"];\n\n// Borrowed from https://github.com/juliangruber/builtins\nconst BUILTINS: [&str; 39] = [\n    \"assert\",\n    \"buffer\",\n    \"child_process\",\n    \"cluster\",\n    \"console\",\n    \"constants\",\n    \"crypto\",\n    \"dgram\",\n    \"dns\",\n    \"domain\",\n    \"events\",\n    \"fs\",\n    \"http\",\n    \"https\",\n    \"module\",\n    \"net\",\n    \"os\",\n    \"path\",\n    \"punycode\",\n    \"querystring\",\n    \"readline\",\n    \"repl\",\n    \"stream\",\n    \"string_decoder\",\n    \"sys\",\n    \"timers\",\n    \"tls\",\n    \"tty\",\n    \"url\",\n    \"util\",\n    \"vm\",\n    \"zlib\",\n    \"freelist\",\n    // excluded only in some versions\n    \"freelist\",\n    \"v8\",\n    \"process\",\n    \"async_hooks\",\n    \"http2\",\n    \"perf_hooks\",\n];\n\n#[derive(Debug, PartialEq, Eq)]\npub enum Validity {\n    /// Valid for new and old packages\n    Valid,\n\n    /// Valid only for old packages\n    ValidForOldPackages { warnings: Vec<String> },\n\n    /// Not valid for new or old packages\n    Invalid {\n        warnings: Vec<String>,\n        errors: Vec<String>,\n    },\n}\n\nimpl Validity {\n    pub fn valid_for_old_packages(&self) -> bool {\n        matches!(self, Validity::Valid | Validity::ValidForOldPackages { .. })\n    }\n\n    pub fn valid_for_new_packages(&self) -> bool {\n        matches!(self, Validity::Valid)\n    }\n}\n\npub fn validate(name: &str) -> Validity {\n    let mut warnings = Vec::new();\n    let mut errors = Vec::new();\n\n    if name.is_empty() {\n        errors.push(\"name length must be greater than zero\".into());\n    }\n\n    if name.starts_with('.') {\n        errors.push(\"name cannot start with a period\".into());\n    }\n\n    if name.starts_with('_') {\n        errors.push(\"name cannot start with an underscore\".into());\n    }\n\n    if name.trim() != name {\n        errors.push(\"name cannot contain leading or trailing spaces\".into());\n    }\n\n    // No funny business\n    for blacklisted_name in BLACKLIST.iter() {\n        if &name.to_lowercase() == blacklisted_name {\n            errors.push(format!(\"{} is a blacklisted name\", blacklisted_name));\n        }\n    }\n\n    // Generate warnings for stuff that used to be allowed\n\n    for builtin in BUILTINS.iter() {\n        if name.to_lowercase() == *builtin {\n            warnings.push(format!(\"{} is a core module name\", builtin));\n        }\n    }\n\n    // really-long-package-names-------------------------------such--length-----many---wow\n    // the thisisareallyreallylongpackagenameitshouldpublishdowenowhavealimittothelengthofpackagenames-poch.\n    if name.len() > 214 {\n        warnings.push(\"name can no longer contain more than 214 characters\".into());\n    }\n\n    // mIxeD CaSe nAMEs\n    if name.to_lowercase() != name {\n        warnings.push(\"name can no longer contain capital letters\".into());\n    }\n\n    if name\n        .split('/')\n        .last()\n        .map(|final_part| SPECIAL_CHARS.is_match(final_part))\n        .unwrap_or(false)\n    {\n        warnings.push(r#\"name can no longer contain special characters (\"~\\'!()*\")\"#.into());\n    }\n\n    if utf8_percent_encode(name, ENCODE_URI_SET).to_string() != name {\n        // Maybe it's a scoped package name, like @user/package\n        if let Some(captures) = SCOPED_PACKAGE.captures(name) {\n            let valid_scope_name = captures\n                .get(1)\n                .map(|scope| scope.as_str())\n                .map(|scope| utf8_percent_encode(scope, ENCODE_URI_SET).to_string() == scope)\n                .unwrap_or(true);\n\n            let valid_package_name = captures\n                .get(2)\n                .map(|package| package.as_str())\n                .map(|package| utf8_percent_encode(package, ENCODE_URI_SET).to_string() == package)\n                .unwrap_or(true);\n\n            if valid_scope_name && valid_package_name {\n                return done(warnings, errors);\n            }\n        }\n\n        errors.push(\"name can only contain URL-friendly characters\".into());\n    }\n\n    done(warnings, errors)\n}\n\nfn done(warnings: Vec<String>, errors: Vec<String>) -> Validity {\n    match (warnings.len(), errors.len()) {\n        (0, 0) => Validity::Valid,\n        (_, 0) => Validity::ValidForOldPackages { warnings },\n        (_, _) => Validity::Invalid { warnings, errors },\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn traditional() {\n        assert_eq!(validate(\"some-package\"), Validity::Valid);\n        assert_eq!(validate(\"example.com\"), Validity::Valid);\n        assert_eq!(validate(\"under_score\"), Validity::Valid);\n        assert_eq!(validate(\"period.js\"), Validity::Valid);\n        assert_eq!(validate(\"123numeric\"), Validity::Valid);\n        assert_eq!(\n            validate(\"crazy!\"),\n            Validity::ValidForOldPackages {\n                warnings: vec![\n                    r#\"name can no longer contain special characters (\"~\\'!()*\")\"#.into()\n                ]\n            }\n        );\n    }\n\n    #[test]\n    fn scoped() {\n        assert_eq!(validate(\"@npm/thingy\"), Validity::Valid);\n        assert_eq!(\n            validate(\"@npm-zors/money!time.js\"),\n            Validity::ValidForOldPackages {\n                warnings: vec![\n                    r#\"name can no longer contain special characters (\"~\\'!()*\")\"#.into()\n                ]\n            }\n        );\n    }\n\n    #[test]\n    fn invalid() {\n        assert_eq!(\n            validate(\"\"),\n            Validity::Invalid {\n                errors: vec![\"name length must be greater than zero\".into()],\n                warnings: vec![]\n            }\n        );\n\n        assert_eq!(\n            validate(\".start-with-period\"),\n            Validity::Invalid {\n                errors: vec![\"name cannot start with a period\".into()],\n                warnings: vec![]\n            }\n        );\n\n        assert_eq!(\n            validate(\"_start-with-underscore\"),\n            Validity::Invalid {\n                errors: vec![\"name cannot start with an underscore\".into()],\n                warnings: vec![]\n            }\n        );\n\n        assert_eq!(\n            validate(\"contain:colons\"),\n            Validity::Invalid {\n                errors: vec![\"name can only contain URL-friendly characters\".into()],\n                warnings: vec![]\n            }\n        );\n\n        assert_eq!(\n            validate(\" leading-space\"),\n            Validity::Invalid {\n                errors: vec![\n                    \"name cannot contain leading or trailing spaces\".into(),\n                    \"name can only contain URL-friendly characters\".into()\n                ],\n                warnings: vec![]\n            }\n        );\n\n        assert_eq!(\n            validate(\"trailing-space \"),\n            Validity::Invalid {\n                errors: vec![\n                    \"name cannot contain leading or trailing spaces\".into(),\n                    \"name can only contain URL-friendly characters\".into()\n                ],\n                warnings: vec![]\n            }\n        );\n\n        assert_eq!(\n            validate(\"s/l/a/s/h/e/s\"),\n            Validity::Invalid {\n                errors: vec![\"name can only contain URL-friendly characters\".into()],\n                warnings: vec![]\n            }\n        );\n\n        assert_eq!(\n            validate(\"node_modules\"),\n            Validity::Invalid {\n                errors: vec![\"node_modules is a blacklisted name\".into()],\n                warnings: vec![]\n            }\n        );\n\n        assert_eq!(\n            validate(\"favicon.ico\"),\n            Validity::Invalid {\n                errors: vec![\"favicon.ico is a blacklisted name\".into()],\n                warnings: vec![]\n            }\n        );\n    }\n\n    #[test]\n    fn node_io_core() {\n        assert_eq!(\n            validate(\"http\"),\n            Validity::ValidForOldPackages {\n                warnings: vec![\"http is a core module name\".into()]\n            }\n        );\n    }\n\n    #[test]\n    fn long_package_names() {\n        let one_too_long = \"ifyouwanttogetthesumoftwonumberswherethosetwonumbersarechosenbyfindingthelargestoftwooutofthreenumbersandsquaringthemwhichismultiplyingthembyitselfthenyoushouldinputthreenumbersintothisfunctionanditwilldothatforyou-\";\n        let short_enough = \"ifyouwanttogetthesumoftwonumberswherethosetwonumbersarechosenbyfindingthelargestoftwooutofthreenumbersandsquaringthemwhichismultiplyingthembyitselfthenyoushouldinputthreenumbersintothisfunctionanditwilldothatforyou\";\n\n        assert_eq!(\n            validate(one_too_long),\n            Validity::ValidForOldPackages {\n                warnings: vec![\"name can no longer contain more than 214 characters\".into()]\n            }\n        );\n\n        assert_eq!(validate(short_enough), Validity::Valid);\n    }\n\n    #[test]\n    fn legacy_mixed_case() {\n        assert_eq!(\n            validate(\"CAPITAL-LETTERS\"),\n            Validity::ValidForOldPackages {\n                warnings: vec![\"name can no longer contain capital letters\".into()]\n            }\n        );\n    }\n}\n"
  },
  {
    "path": "crates/volta-core/Cargo.toml",
    "content": "[package]\nname = \"volta-core\"\nversion = \"0.1.0\"\nauthors = [\"David Herman <david.herman@gmail.com>\"]\nedition = \"2021\"\n\n[features]\nmock-network = [\"mockito\"]\n# The `cross-platform-docs` feature flag is used for generating API docs for\n# multiple platforms in one build.\n# See ci/publish-docs.yml for an example of how it's enabled.\n# See volta-core::path for an example of where it's used.\ncross-platform-docs = []\n\n[dependencies]\nterminal_size = \"0.4.1\"\nindicatif = \"0.17.9\"\nconsole = \">=0.11.3, <1.0.0\"\nreadext = \"0.1.0\"\nserde_json = { version = \"1.0.135\", features = [\"preserve_order\"] }\nserde = { version = \"1.0.217\", features = [\"derive\"] }\narchive = { path = \"../archive\" }\nnode-semver = \"2\"\ncmdline_words_parser = \"0.2.1\"\nfs-utils = { path = \"../fs-utils\" }\ncfg-if = \"1.0\"\ntempfile = \"3.14.0\"\nos_info = \"3.9.2\"\ndetect-indent = \"0.1\"\nenvoy = \"0.1.3\"\nmockito = { version = \"0.31.1\", optional = true }\nregex = \"1.11.1\"\ndirs = \"6.0.0\"\n# We manually configure the feature list here because 0.4.16 includes the\n# `oldtime` feature by default to avoid a breaking change. Additionally, using\n# the feature list explicitly lets us drop the `wasmbind` feature, which we do\n# not need.\nchrono = { version = \"0.4.39\", default-features = false, features = [\"alloc\", \"std\", \"clock\"] }\nvalidate-npm-package-name = { path = \"../validate-npm-package-name\" }\ntextwrap = \"0.16.1\"\nlog = { version = \"0.4\", features = [\"std\"] }\nctrlc = \"3.4.5\"\nwalkdir = \"2.5.0\"\nvolta-layout = { path = \"../volta-layout\" }\nonce_cell = \"1.19.0\"\ndunce = \"1.0.5\"\nci_info = \"0.14.14\"\nhttpdate = \"1\"\nheaders = \"0.4\"\nattohttpc = { version = \"0.28\", default-features = false, features = [\"json\", \"compress\", \"tls-rustls-native-roots\"] }\nchain-map = \"0.1.0\"\nindexmap = \"2.7.0\"\nretry = \"2\"\nfs2 = \"0.4.3\"\nwhich = \"7.0.1\"\n\n[target.'cfg(windows)'.dependencies]\nwinreg = \"0.55.0\"\njunction = \"1.2.0\"\n"
  },
  {
    "path": "crates/volta-core/fixtures/basic/package.json",
    "content": "{\n  \"name\": \"basic-project\",\n  \"version\": \"0.0.7\",\n  \"description\": \"Testing that manifest pulls things out of this correctly\",\n  \"license\": \"To Kill\",\n  \"dependencies\": {\n    \"@namespace/some-dep\": \"0.2.4\",\n    \"rsvp\": \"^3.5.0\"\n  },\n  \"devDependencies\": {\n    \"@namespaced/something-else\": \"^6.3.7\",\n    \"eslint\": \"~4.8.0\"\n  },\n  \"volta\": {\n    \"node\": \"6.11.1\",\n    \"npm\": \"3.10.10\",\n    \"yarn\": \"1.2.0\"\n  }\n}\n"
  },
  {
    "path": "crates/volta-core/fixtures/basic/subdir/.gitkeep",
    "content": ""
  },
  {
    "path": "crates/volta-core/fixtures/cycle-1/package.json",
    "content": "{\n  \"name\": \"cycle-1-project\",\n  \"version\": \"0.0.1\",\n  \"description\": \"Testing that project correctly detects a cycle between this and volta.json\",\n  \"license\": \"To Kill\",\n  \"volta\": {\n    \"extends\": \"./volta.json\"\n  }\n}\n"
  },
  {
    "path": "crates/volta-core/fixtures/cycle-1/volta.json",
    "content": "{\n  \"volta\": {\n    \"extends\": \"./package.json\"\n  }\n}\n"
  },
  {
    "path": "crates/volta-core/fixtures/cycle-2/package.json",
    "content": "{\n  \"name\": \"cycle-2-project\",\n  \"version\": \"0.0.1\",\n  \"description\": \"Testing that project correctly detects a cycle between workspace-1.json and workspace-2.json\",\n  \"license\": \"To Kill\",\n  \"volta\": {\n    \"extends\": \"./workspace-1.json\"\n  }\n}\n"
  },
  {
    "path": "crates/volta-core/fixtures/cycle-2/workspace-1.json",
    "content": "{\n  \"volta\": {\n    \"extends\": \"./workspace-2.json\"\n  }\n}\n"
  },
  {
    "path": "crates/volta-core/fixtures/cycle-2/workspace-2.json",
    "content": "{\n  \"volta\": {\n    \"extends\": \"./workspace-1.json\"\n  }\n}\n"
  },
  {
    "path": "crates/volta-core/fixtures/hooks/bins.json",
    "content": "{\n  \"node\": {\n    \"distro\": {\n      \"bin\": \"/some/bin/for/node/distro\"\n    },\n    \"latest\": {\n      \"bin\": \"/some/bin/for/node/latest\"\n    },\n    \"index\": {\n      \"bin\": \"/some/bin/for/node/index\"\n    }\n  },\n  \"pnpm\": {\n    \"distro\": {\n      \"bin\": \"/bin/to/pnpm/distro\"\n    },\n    \"latest\": {\n      \"bin\": \"/bin/to/pnpm/latest\"\n    },\n    \"index\": {\n      \"bin\": \"/bin/to/pnpm/index\"\n    }\n  },\n  \"yarn\": {\n    \"distro\": {\n      \"bin\": \"/bin/to/yarn/distro\"\n    },\n    \"latest\": {\n      \"bin\": \"/bin/to/yarn/latest\"\n    },\n    \"index\": {\n      \"bin\": \"/bin/to/yarn/index\"\n    }\n  },\n  \"events\": {\n    \"publish\": {\n      \"bin\": \"/events/bin\"\n    }\n  }\n}\n"
  },
  {
    "path": "crates/volta-core/fixtures/hooks/event_url.json",
    "content": "{\n  \"events\": {\n    \"publish\": {\n      \"url\": \"https://google.com\"\n    }\n  }\n}\n"
  },
  {
    "path": "crates/volta-core/fixtures/hooks/format_github.json",
    "content": "{\n  \"node\": {\n    \"index\": {\n      \"prefix\": \"http://localhost/node/index/\",\n      \"format\": \"github\"\n    }\n  },\n  \"npm\": {\n    \"index\": {\n      \"prefix\": \"http://localhost/npm/index/\",\n      \"format\": \"github\"\n    }\n  },\n  \"pnpm\": {\n    \"index\": {\n      \"prefix\": \"http://localhost/pnpm/index/\",\n      \"format\": \"github\"\n    }\n  },\n  \"yarn\": {\n    \"index\": {\n      \"prefix\": \"http://localhost/yarn/index/\",\n      \"format\": \"github\"\n    }\n  }\n}\n"
  },
  {
    "path": "crates/volta-core/fixtures/hooks/format_npm.json",
    "content": "{\n  \"node\": {\n    \"index\": {\n      \"prefix\": \"http://localhost/node/index/\",\n      \"format\": \"npm\"\n    }\n  },\n  \"npm\": {\n    \"index\": {\n      \"prefix\": \"http://localhost/npm/index/\",\n      \"format\": \"npm\"\n    }\n  },\n  \"pnpm\": {\n    \"index\": {\n      \"prefix\": \"http://localhost/pnpm/index/\",\n      \"format\": \"npm\"\n    }\n  },\n  \"yarn\": {\n    \"index\": {\n      \"prefix\": \"http://localhost/yarn/index/\",\n      \"format\": \"npm\"\n    }\n  }\n}\n"
  },
  {
    "path": "crates/volta-core/fixtures/hooks/prefixes.json",
    "content": "{\n  \"node\": {\n    \"distro\": {\n      \"prefix\": \"http://localhost/node/distro/\"\n    },\n    \"latest\": {\n      \"prefix\": \"http://localhost/node/latest/\"\n    },\n    \"index\": {\n      \"prefix\": \"http://localhost/node/index/\"\n    }\n  },\n  \"pnpm\": {\n    \"distro\": {\n      \"prefix\": \"http://localhost/pnpm/distro/\"\n    },\n    \"latest\": {\n      \"prefix\": \"http://localhost/pnpm/latest/\"\n    },\n    \"index\": {\n      \"prefix\": \"http://localhost/pnpm/index/\"\n    }\n  },\n  \"yarn\": {\n    \"distro\": {\n      \"prefix\": \"http://localhost/yarn/distro/\"\n    },\n    \"latest\": {\n      \"prefix\": \"http://localhost/yarn/latest/\"\n    },\n    \"index\": {\n      \"prefix\": \"http://localhost/yarn/index/\"\n    }\n  }\n}\n"
  },
  {
    "path": "crates/volta-core/fixtures/hooks/project/.volta/hooks.json",
    "content": "{\n  \"node\": {\n    \"distro\": {\n      \"bin\": \"/some/bin/for/node/distro\"\n    },\n    \"latest\": {\n      \"bin\": \"/some/bin/for/node/latest\"\n    },\n    \"index\": {\n      \"bin\": \"/some/bin/for/node/index\"\n    }\n  },\n  \"events\": {\n    \"publish\": {\n      \"bin\": \"/events/bin\"\n    }\n  }\n}\n"
  },
  {
    "path": "crates/volta-core/fixtures/hooks/project/package.json",
    "content": "{\n  \"this file\": \"causes this directory to be recognized as a project\"\n}\n"
  },
  {
    "path": "crates/volta-core/fixtures/hooks/templates.json",
    "content": "{\n  \"node\": {\n    \"distro\": {\n      \"template\": \"http://localhost/node/distro/{{version}}/\"\n    },\n    \"latest\": {\n      \"template\": \"http://localhost/node/latest/{{version}}/\"\n    },\n    \"index\": {\n      \"template\": \"http://localhost/node/index/{{version}}/\"\n    }\n  },\n  \"pnpm\": {\n    \"distro\": {\n      \"template\": \"http://localhost/pnpm/distro/{{version}}/\"\n    },\n    \"latest\": {\n      \"template\": \"http://localhost/pnpm/latest/{{version}}/\"\n    },\n    \"index\": {\n      \"template\": \"http://localhost/pnpm/index/{{version}}/\"\n    }\n  },\n  \"yarn\": {\n    \"distro\": {\n      \"template\": \"http://localhost/yarn/distro/{{version}}/\"\n    },\n    \"latest\": {\n      \"template\": \"http://localhost/yarn/latest/{{version}}/\"\n    },\n    \"index\": {\n      \"template\": \"http://localhost/yarn/index/{{version}}/\"\n    }\n  }\n}\n"
  },
  {
    "path": "crates/volta-core/fixtures/nested/package.json",
    "content": "{\n  \"name\": \"nested-project\",\n  \"version\": \"0.0.1\",\n  \"description\": \"Testing that project correctly detects a nested workspace\",\n  \"license\": \"To Kill\",\n  \"dependencies\": {\n    \"lodash\": \"*\"\n  },\n  \"devDependencies\": {\n    \"eslint\": \"*\"\n  },\n  \"volta\": {\n    \"yarn\": \"1.11.0\",\n    \"npm\": \"6.12.1\",\n    \"node\": \"12.14.0\"\n  }\n}\n"
  },
  {
    "path": "crates/volta-core/fixtures/nested/subproject/inner_project/package.json",
    "content": "{\n  \"name\": \"inner-project\",\n  \"version\": \"0.0.1\",\n  \"description\": \"Testing that project correctly detects a nested workspace\",\n  \"license\": \"To Kill\",\n  \"dependencies\": {\n    \"express\": \"*\"\n  },\n  \"devDependencies\": {\n    \"typescript\": \"*\"\n  },\n  \"volta\": {\n    \"yarn\": \"1.22.4\",\n    \"extends\": \"../package.json\"\n  }\n}\n"
  },
  {
    "path": "crates/volta-core/fixtures/nested/subproject/package.json",
    "content": "{\n  \"name\": \"subproject\",\n  \"version\": \"0.0.1\",\n  \"description\": \"Testing that project correctly detects a nested workspace\",\n  \"license\": \"To Kill\",\n  \"dependencies\": {\n    \"rsvp\": \"*\"\n  },\n  \"devDependencies\": {\n    \"glob\": \"*\"\n  },\n  \"volta\": {\n    \"yarn\": \"1.17.0\",\n    \"npm\": \"6.9.0\",\n    \"extends\": \"../package.json\"\n  }\n}\n"
  },
  {
    "path": "crates/volta-core/fixtures/no_toolchain/package.json",
    "content": "{\n  \"name\": \"basic-project\",\n  \"version\": \"0.0.7\",\n  \"description\": \"Testing that manifest pulls things out of this correctly\",\n  \"license\": \"To Kill\",\n  \"dependencies\": {\n    \"@namespace/some-dep\": \"0.2.4\",\n    \"rsvp\": \"^3.5.0\"\n  },\n  \"devDependencies\": {\n    \"@namespaced/something-else\": \"^6.3.7\",\n    \"eslint\": \"~4.8.0\"\n  }\n}\n"
  },
  {
    "path": "crates/volta-core/fixtures/yarn/pnp-cjs/.pnp.cjs",
    "content": "// plug and play\n"
  },
  {
    "path": "crates/volta-core/fixtures/yarn/pnp-cjs/package.json",
    "content": "{\n  \"name\": \"plug-n-play-cjs\",\n  \"version\": \"2.0.0\",\n  \"description\": \"Testing that Plug-n-Play things work\",\n  \"license\": \"To Ill\",\n  \"volta\": {\n    \"node\": \"6.11.1\",\n    \"npm\": \"3.10.10\",\n    \"yarn\": \"3.2.0\"\n  }\n}\n"
  },
  {
    "path": "crates/volta-core/fixtures/yarn/pnp-js/.pnp.js",
    "content": "// plug and play\n"
  },
  {
    "path": "crates/volta-core/fixtures/yarn/pnp-js/package.json",
    "content": "{\n  \"name\": \"plug-n-play-js\",\n  \"version\": \"2.0.0\",\n  \"description\": \"Testing that Plug-n-Play things work\",\n  \"license\": \"To Ill\",\n  \"volta\": {\n    \"node\": \"6.11.1\",\n    \"npm\": \"3.10.10\",\n    \"yarn\": \"2.4.0\"\n  }\n}\n"
  },
  {
    "path": "crates/volta-core/fixtures/yarn/yarnrc-yml/.yarnrc.yml",
    "content": "yarnPath: .yarn/releases/yarn-3.3.0.cjs"
  },
  {
    "path": "crates/volta-core/fixtures/yarn/yarnrc-yml/package.json",
    "content": "{\n  \"name\": \"yarnrc-yml\",\n  \"version\": \"2.0.0\",\n  \"description\": \"Testing that Yarn berry things work\",\n  \"license\": \"To Ill\",\n  \"volta\": {\n    \"node\": \"6.11.1\",\n    \"npm\": \"3.10.10\",\n    \"yarn\": \"3.3.0\"\n  }\n}\n"
  },
  {
    "path": "crates/volta-core/src/command.rs",
    "content": "use std::ffi::OsStr;\nuse std::process::Command;\n\nuse cfg_if::cfg_if;\n\ncfg_if! {\n    if #[cfg(windows)] {\n        pub fn create_command<E>(exe: E) -> Command\n        where\n            E: AsRef<OsStr>\n        {\n            // Several of the node utilities are implemented as `.bat` or `.cmd` files\n            // When executing those files with `Command`, we need to call them with:\n            //    cmd.exe /C <COMMAND> <ARGUMENTS>\n            // Instead of: <COMMAND> <ARGUMENTS>\n            // See: https://github.com/rust-lang/rust/issues/42791 For a longer discussion\n            let mut command = Command::new(\"cmd.exe\");\n            command.arg(\"/C\");\n            command.arg(exe);\n            command\n        }\n    } else {\n        pub fn create_command<E>(exe: E) -> Command\n        where\n            E: AsRef<OsStr>\n        {\n            Command::new(exe)\n        }\n    }\n}\n"
  },
  {
    "path": "crates/volta-core/src/error/kind.rs",
    "content": "use std::fmt;\nuse std::path::PathBuf;\n\nuse super::ExitCode;\nuse crate::style::{text_width, tool_version};\nuse crate::tool;\nuse crate::tool::package::PackageManager;\nuse textwrap::{fill, indent};\n\nconst REPORT_BUG_CTA: &str =\n    \"Please rerun the command that triggered this error with the environment\nvariable `VOLTA_LOGLEVEL` set to `debug` and open an issue at\nhttps://github.com/volta-cli/volta/issues with the details!\";\n\nconst PERMISSIONS_CTA: &str = \"Please ensure you have correct permissions to the Volta directory.\";\n\n#[derive(Debug)]\n#[cfg_attr(test, derive(PartialEq, Eq))]\npub enum ErrorKind {\n    /// Thrown when package tries to install a binary that is already installed.\n    BinaryAlreadyInstalled {\n        bin_name: String,\n        existing_package: String,\n        new_package: String,\n    },\n\n    /// Thrown when executing an external binary fails\n    BinaryExecError,\n\n    /// Thrown when a binary could not be found in the local inventory\n    BinaryNotFound {\n        name: String,\n    },\n\n    /// Thrown when building the virtual environment path fails\n    BuildPathError,\n\n    /// Thrown when unable to launch a command with VOLTA_BYPASS set\n    BypassError {\n        command: String,\n    },\n\n    /// Thrown when a user tries to `volta fetch` something other than node/yarn/npm.\n    CannotFetchPackage {\n        package: String,\n    },\n\n    /// Thrown when a user tries to `volta pin` something other than node/yarn/npm.\n    CannotPinPackage {\n        package: String,\n    },\n\n    /// Thrown when the Completions out-dir is not a directory\n    CompletionsOutFileError {\n        path: PathBuf,\n    },\n\n    /// Thrown when the containing directory could not be determined\n    ContainingDirError {\n        path: PathBuf,\n    },\n\n    CouldNotDetermineTool,\n\n    /// Thrown when unable to start the migration executable\n    CouldNotStartMigration,\n\n    CreateDirError {\n        dir: PathBuf,\n    },\n\n    /// Thrown when unable to create the layout file\n    CreateLayoutFileError {\n        file: PathBuf,\n    },\n\n    /// Thrown when unable to create a link to the shared global library directory\n    CreateSharedLinkError {\n        name: String,\n    },\n\n    /// Thrown when creating a temporary directory fails\n    CreateTempDirError {\n        in_dir: PathBuf,\n    },\n\n    /// Thrown when creating a temporary file fails\n    CreateTempFileError {\n        in_dir: PathBuf,\n    },\n\n    CurrentDirError,\n\n    /// Thrown when deleting a directory fails\n    DeleteDirectoryError {\n        directory: PathBuf,\n    },\n\n    /// Thrown when deleting a file fails\n    DeleteFileError {\n        file: PathBuf,\n    },\n\n    DeprecatedCommandError {\n        command: String,\n        advice: String,\n    },\n\n    DownloadToolNetworkError {\n        tool: tool::Spec,\n        from_url: String,\n    },\n\n    /// Thrown when unable to execute a hook command\n    ExecuteHookError {\n        command: String,\n    },\n\n    /// Thrown when `volta.extends` keys result in an infinite cycle\n    ExtensionCycleError {\n        paths: Vec<PathBuf>,\n        duplicate: PathBuf,\n    },\n\n    /// Thrown when determining the path to an extension manifest fails\n    ExtensionPathError {\n        path: PathBuf,\n    },\n\n    /// Thrown when a hook command returns a non-zero exit code\n    HookCommandFailed {\n        command: String,\n    },\n\n    /// Thrown when a hook contains multiple fields (prefix, template, or bin)\n    HookMultipleFieldsSpecified,\n\n    /// Thrown when a hook doesn't contain any of the known fields (prefix, template, or bin)\n    HookNoFieldsSpecified,\n\n    /// Thrown when determining the path to a hook fails\n    HookPathError {\n        command: String,\n    },\n\n    /// Thrown when determining the name of a newly-installed package fails\n    InstalledPackageNameError,\n\n    InvalidHookCommand {\n        command: String,\n    },\n\n    /// Thrown when output from a hook command could not be read\n    InvalidHookOutput {\n        command: String,\n    },\n\n    /// Thrown when a user does e.g. `volta install node 12` instead of\n    /// `volta install node@12`.\n    InvalidInvocation {\n        action: String,\n        name: String,\n        version: String,\n    },\n\n    /// Thrown when a user does e.g. `volta install 12` instead of\n    /// `volta install node@12`.\n    InvalidInvocationOfBareVersion {\n        action: String,\n        version: String,\n    },\n\n    /// Thrown when a format other than \"npm\" or \"github\" is given for yarn.index in the hooks\n    InvalidRegistryFormat {\n        format: String,\n    },\n\n    /// Thrown when a tool name is invalid per npm's rules.\n    InvalidToolName {\n        name: String,\n        errors: Vec<String>,\n    },\n\n    /// Thrown when unable to acquire a lock on the Volta directory\n    LockAcquireError,\n\n    /// Thrown when pinning or installing npm@bundled and couldn't detect the bundled version\n    NoBundledNpm {\n        command: String,\n    },\n\n    /// Thrown when pnpm is not set at the command-line\n    NoCommandLinePnpm,\n\n    /// Thrown when Yarn is not set at the command-line\n    NoCommandLineYarn,\n\n    /// Thrown when a user tries to install a Yarn or npm version before installing a Node version.\n    NoDefaultNodeVersion {\n        tool: String,\n    },\n\n    /// Thrown when there is no Node version matching a requested semver specifier.\n    NodeVersionNotFound {\n        matching: String,\n    },\n\n    NoHomeEnvironmentVar,\n\n    /// Thrown when the install dir could not be determined\n    NoInstallDir,\n\n    NoLocalDataDir,\n\n    /// Thrown when a user tries to pin a npm, pnpm, or Yarn version before pinning a Node version.\n    NoPinnedNodeVersion {\n        tool: String,\n    },\n\n    /// Thrown when the platform (Node version) could not be determined\n    NoPlatform,\n\n    /// Thrown when parsing the project manifest and there is a `\"volta\"` key without Node\n    NoProjectNodeInManifest,\n\n    /// Thrown when Yarn is not set in a project\n    NoProjectYarn,\n\n    /// Thrown when pnpm is not set in a project\n    NoProjectPnpm,\n\n    /// Thrown when no shell profiles could be found\n    NoShellProfile {\n        env_profile: String,\n        bin_dir: PathBuf,\n    },\n\n    /// Thrown when the user tries to pin Node or Yarn versions outside of a package.\n    NotInPackage,\n\n    /// Thrown when default Yarn is not set\n    NoDefaultYarn,\n\n    /// Thrown when default pnpm is not set\n    NoDefaultPnpm,\n\n    /// Thrown when `npm link` is called with a package that isn't available\n    NpmLinkMissingPackage {\n        package: String,\n    },\n\n    /// Thrown when `npm link` is called with a package that was not installed / linked with npm\n    NpmLinkWrongManager {\n        package: String,\n    },\n\n    /// Thrown when there is no npm version matching the requested Semver/Tag\n    NpmVersionNotFound {\n        matching: String,\n    },\n\n    NpxNotAvailable {\n        version: String,\n    },\n\n    /// Thrown when the command to install a global package is not successful\n    PackageInstallFailed {\n        package: String,\n    },\n\n    /// Thrown when parsing the package manifest fails\n    PackageManifestParseError {\n        package: String,\n    },\n\n    /// Thrown when reading the package manifest fails\n    PackageManifestReadError {\n        package: String,\n    },\n\n    /// Thrown when a specified package could not be found on the npm registry\n    PackageNotFound {\n        package: String,\n    },\n\n    /// Thrown when parsing a package manifest fails\n    PackageParseError {\n        file: PathBuf,\n    },\n\n    /// Thrown when reading a package manifest fails\n    PackageReadError {\n        file: PathBuf,\n    },\n\n    /// Thrown when a package has been unpacked but is not formed correctly.\n    PackageUnpackError,\n\n    /// Thrown when writing a package manifest fails\n    PackageWriteError {\n        file: PathBuf,\n    },\n\n    /// Thrown when unable to parse a bin config file\n    ParseBinConfigError,\n\n    /// Thrown when unable to parse a hooks.json file\n    ParseHooksError {\n        file: PathBuf,\n    },\n\n    /// Thrown when unable to parse the node index cache\n    ParseNodeIndexCacheError,\n\n    /// Thrown when unable to parse the node index\n    ParseNodeIndexError {\n        from_url: String,\n    },\n\n    /// Thrown when unable to parse the node index cache expiration\n    ParseNodeIndexExpiryError,\n\n    /// Thrown when unable to parse the npm manifest file from a node install\n    ParseNpmManifestError,\n\n    /// Thrown when unable to parse a package configuration\n    ParsePackageConfigError,\n\n    /// Thrown when unable to parse the platform.json file\n    ParsePlatformError,\n\n    /// Thrown when unable to parse a tool spec (`<tool>[@<version>]`)\n    ParseToolSpecError {\n        tool_spec: String,\n    },\n\n    /// Thrown when persisting an archive to the inventory fails\n    PersistInventoryError {\n        tool: String,\n    },\n\n    /// Thrown when there is no pnpm version matching a requested semver specifier.\n    PnpmVersionNotFound {\n        matching: String,\n    },\n\n    /// Thrown when executing a project-local binary fails\n    ProjectLocalBinaryExecError {\n        command: String,\n    },\n\n    /// Thrown when a project-local binary could not be found\n    ProjectLocalBinaryNotFound {\n        command: String,\n    },\n\n    /// Thrown when a publish hook contains both the url and bin fields\n    PublishHookBothUrlAndBin,\n\n    /// Thrown when a publish hook contains neither url nor bin fields\n    PublishHookNeitherUrlNorBin,\n\n    /// Thrown when there was an error reading the user bin directory\n    ReadBinConfigDirError {\n        dir: PathBuf,\n    },\n\n    /// Thrown when there was an error reading the config for a binary\n    ReadBinConfigError {\n        file: PathBuf,\n    },\n\n    /// Thrown when unable to read the default npm version file\n    ReadDefaultNpmError {\n        file: PathBuf,\n    },\n\n    /// Thrown when unable to read the contents of a directory\n    ReadDirError {\n        dir: PathBuf,\n    },\n\n    /// Thrown when there was an error opening a hooks.json file\n    ReadHooksError {\n        file: PathBuf,\n    },\n\n    /// Thrown when there was an error reading the Node Index Cache\n    ReadNodeIndexCacheError {\n        file: PathBuf,\n    },\n\n    /// Thrown when there was an error reading the Node Index Cache Expiration\n    ReadNodeIndexExpiryError {\n        file: PathBuf,\n    },\n\n    /// Thrown when there was an error reading the npm manifest file\n    ReadNpmManifestError,\n\n    /// Thrown when there was an error reading a package configuration file\n    ReadPackageConfigError {\n        file: PathBuf,\n    },\n\n    /// Thrown when there was an error opening the user platform file\n    ReadPlatformError {\n        file: PathBuf,\n    },\n\n    /// Thrown when unable to read the user Path environment variable from the registry\n    #[cfg(windows)]\n    ReadUserPathError,\n\n    /// Thrown when the public registry for Node or Yarn could not be downloaded.\n    RegistryFetchError {\n        tool: String,\n        from_url: String,\n    },\n\n    /// Thrown when the shim binary is called directly, not through a symlink\n    RunShimDirectly,\n\n    /// Thrown when there was an error setting a tool to executable\n    SetToolExecutable {\n        tool: String,\n    },\n\n    /// Thrown when there was an error copying an unpacked tool to the image directory\n    SetupToolImageError {\n        tool: String,\n        version: String,\n        dir: PathBuf,\n    },\n\n    /// Thrown when Volta is unable to create a shim\n    ShimCreateError {\n        name: String,\n    },\n\n    /// Thrown when Volta is unable to remove a shim\n    ShimRemoveError {\n        name: String,\n    },\n\n    /// Thrown when serializing a bin config to JSON fails\n    StringifyBinConfigError,\n\n    /// Thrown when serializing a package config to JSON fails\n    StringifyPackageConfigError,\n\n    /// Thrown when serializing the platform to JSON fails\n    StringifyPlatformError,\n\n    /// Thrown when a given feature has not yet been implemented\n    Unimplemented {\n        feature: String,\n    },\n\n    /// Thrown when unpacking an archive (tarball or zip) fails\n    UnpackArchiveError {\n        tool: String,\n        version: String,\n    },\n\n    /// Thrown when a package to upgrade was not found\n    UpgradePackageNotFound {\n        package: String,\n        manager: PackageManager,\n    },\n\n    /// Thrown when a package to upgrade was installed with a different package manager\n    UpgradePackageWrongManager {\n        package: String,\n        manager: PackageManager,\n    },\n\n    VersionParseError {\n        version: String,\n    },\n\n    /// Thrown when there was an error writing a bin config file\n    WriteBinConfigError {\n        file: PathBuf,\n    },\n\n    /// Thrown when there was an error writing the default npm to file\n    WriteDefaultNpmError {\n        file: PathBuf,\n    },\n\n    /// Thrown when there was an error writing the npm launcher\n    WriteLauncherError {\n        tool: String,\n    },\n\n    /// Thrown when there was an error writing the node index cache\n    WriteNodeIndexCacheError {\n        file: PathBuf,\n    },\n\n    /// Thrown when there was an error writing the node index expiration\n    WriteNodeIndexExpiryError {\n        file: PathBuf,\n    },\n\n    /// Thrown when there was an error writing a package config\n    WritePackageConfigError {\n        file: PathBuf,\n    },\n\n    /// Thrown when writing the platform.json file fails\n    WritePlatformError {\n        file: PathBuf,\n    },\n\n    /// Thrown when unable to write the user PATH environment variable\n    #[cfg(windows)]\n    WriteUserPathError,\n\n    /// Thrown when a user attempts to install a version of Yarn2\n    Yarn2NotSupported,\n\n    /// Thrown when there is an error fetching the latest version of Yarn\n    YarnLatestFetchError {\n        from_url: String,\n    },\n\n    /// Thrown when there is no Yarn version matching a requested semver specifier.\n    YarnVersionNotFound {\n        matching: String,\n    },\n}\n\nimpl fmt::Display for ErrorKind {\n    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {\n        match self {\n            ErrorKind::BinaryAlreadyInstalled {\n                bin_name,\n                existing_package,\n                new_package,\n            } => write!(\n                f,\n                \"Executable '{}' is already installed by {}\n\nPlease remove {} before installing {}\",\n                bin_name, existing_package, existing_package, new_package\n            ),\n            ErrorKind::BinaryExecError => write!(\n                f,\n                \"Could not execute command.\n\nSee `volta help install` and `volta help pin` for info about making tools available.\"\n            ),\n            ErrorKind::BinaryNotFound { name } => write!(\n                f,\n                r#\"Could not find executable \"{}\"\n\nUse `volta install` to add a package to your toolchain (see `volta help install` for more info).\"#,\n                name\n            ),\n            ErrorKind::BuildPathError => write!(\n                f,\n                \"Could not create execution environment.\n\nPlease ensure your PATH is valid.\"\n            ),\n            ErrorKind::BypassError { command } => write!(\n                f,\n                \"Could not execute command '{}'\n\nVOLTA_BYPASS is enabled, please ensure that the command exists on your system or unset VOLTA_BYPASS\",\n                command,\n            ),\n            ErrorKind::CannotFetchPackage { package } => write!(\n                f,\n                \"Fetching packages without installing them is not supported.\n\nUse `volta install {}` to update the default version.\",\n                package\n            ),\n            ErrorKind::CannotPinPackage { package } => write!(\n                f,\n                \"Only node and yarn can be pinned in a project\n\nUse `npm install` or `yarn add` to select a version of {} for this project.\",\n                package\n            ),\n            ErrorKind::CompletionsOutFileError { path } => write!(\n                f,\n                \"Completions file `{}` already exists.\n\nPlease remove the file or pass `-f` or `--force` to override.\",\n                path.display()\n            ),\n            ErrorKind::ContainingDirError { path } => write!(\n                f,\n                \"Could not create the containing directory for {}\n\n{}\",\n                path.display(),\n                PERMISSIONS_CTA\n            ),\n            ErrorKind::CouldNotDetermineTool => write!(\n                f,\n                \"Could not determine tool name\n\n{}\",\n                REPORT_BUG_CTA\n            ),\n            ErrorKind::CouldNotStartMigration => write!(\n                f,\n                \"Could not start migration process to upgrade your Volta directory.\n\nPlease ensure you have 'volta-migrate' on your PATH and run it directly.\"\n            ),\n            ErrorKind::CreateDirError { dir } => write!(\n                f,\n                \"Could not create directory {}\n\nPlease ensure that you have the correct permissions.\",\n                dir.display()\n            ),\n            ErrorKind::CreateLayoutFileError { file } => write!(\n                f,\n                \"Could not create layout file {}\n\n{}\",\n                file.display(), PERMISSIONS_CTA\n            ),\n            ErrorKind::CreateSharedLinkError { name } => write!(\n                f,\n                \"Could not create shared environment for package '{}'\n\n{}\",\n                name, PERMISSIONS_CTA\n            ),\n            ErrorKind::CreateTempDirError { in_dir } => write!(\n                f,\n                \"Could not create temporary directory\nin {}\n\n{}\",\n                in_dir.display(),\n                PERMISSIONS_CTA\n            ),\n            ErrorKind::CreateTempFileError { in_dir } => write!(\n                f,\n                \"Could not create temporary file\nin {}\n\n{}\",\n                in_dir.display(),\n                PERMISSIONS_CTA\n            ),\n            ErrorKind::CurrentDirError => write!(\n                f,\n                \"Could not determine current directory\n\nPlease ensure that you have the correct permissions.\"\n            ),\n            ErrorKind::DeleteDirectoryError { directory } => write!(\n                f,\n                \"Could not remove directory\nat {}\n\n{}\",\n                directory.display(),\n                PERMISSIONS_CTA\n            ),\n            ErrorKind::DeleteFileError { file } => write!(\n                f,\n                \"Could not remove file\nat {}\n\n{}\",\n                file.display(),\n                PERMISSIONS_CTA\n            ),\n            ErrorKind::DeprecatedCommandError { command, advice } => {\n                write!(f, \"The subcommand `{}` is deprecated.\\n{}\", command, advice)\n            }\n            ErrorKind::DownloadToolNetworkError { tool, from_url } => write!(\n                f,\n                \"Could not download {}\nfrom {}\n\nPlease verify your internet connection and ensure the correct version is specified.\",\n                tool, from_url\n            ),\n            ErrorKind::ExecuteHookError { command } => write!(\n                f,\n                \"Could not execute hook command: '{}'\n\nPlease ensure that the correct command is specified.\",\n                command\n            ),\n            ErrorKind::ExtensionCycleError { paths, duplicate } => {\n                // Detected infinite loop in project workspace:\n                //\n                // --> /home/user/workspace/project/package.json\n                //     /home/user/workspace/package.json\n                // --> /home/user/workspace/project/package.json\n                //\n                // Please ensure that project workspaces do not depend on each other.\n                f.write_str(\"Detected infinite loop in project workspace:\\n\\n\")?;\n\n                for path in paths {\n                    if path == duplicate {\n                        f.write_str(\"--> \")?;\n                    } else {\n                        f.write_str(\"    \")?;\n                    }\n\n                    writeln!(f, \"{}\", path.display())?;\n                }\n\n                writeln!(f, \"--> {}\", duplicate.display())?;\n                writeln!(f)?;\n\n                f.write_str(\"Please ensure that project workspaces do not depend on each other.\")\n            }\n            ErrorKind::ExtensionPathError { path } => write!(\n                f,\n                \"Could not determine path to project workspace: '{}'\n\nPlease ensure that the file exists and is accessible.\",\n                path.display(),\n            ),\n            ErrorKind::HookCommandFailed { command } => write!(\n                f,\n                \"Hook command '{}' indicated a failure.\n\nPlease verify the requested tool and version.\",\n                command\n            ),\n            ErrorKind::HookMultipleFieldsSpecified => write!(\n                f,\n                \"Hook configuration includes multiple hook types.\n\nPlease include only one of 'bin', 'prefix', or 'template'\"\n            ),\n            ErrorKind::HookNoFieldsSpecified => write!(\n                f,\n                \"Hook configuration includes no hook types.\n\nPlease include one of 'bin', 'prefix', or 'template'\"\n            ),\n            ErrorKind::HookPathError { command } => write!(\n                f,\n                \"Could not determine path to hook command: '{}'\n\nPlease ensure that the correct command is specified.\",\n                command\n            ),\n            ErrorKind::InstalledPackageNameError => write!(\n                f,\n                \"Could not determine the name of the package that was just installed.\n\n{}\",\n                REPORT_BUG_CTA\n            ),\n            ErrorKind::InvalidHookCommand { command } => write!(\n                f,\n                \"Invalid hook command: '{}'\n\nPlease ensure that the correct command is specified.\",\n                command\n            ),\n            ErrorKind::InvalidHookOutput { command } => write!(\n                f,\n                \"Could not read output from hook command: '{}'\n\nPlease ensure that the command output is valid UTF-8 text.\",\n                command\n            ),\n\n            ErrorKind::InvalidInvocation {\n                action,\n                name,\n                version,\n            } => {\n                let error = format!(\n                    \"`volta {action} {name} {version}` is not supported.\",\n                    action = action,\n                    name = name,\n                    version = version\n                );\n\n                let call_to_action = format!(\n\"To {action} '{name}' version '{version}', please run `volta {action} {formatted}`. \\\nTo {action} the packages '{name}' and '{version}', please {action} them in separate commands, or with explicit versions.\",\n                    action=action,\n                    name=name,\n                    version=version,\n                    formatted=tool_version(name, version)\n                );\n\n                let wrapped_cta = match text_width() {\n                    Some(width) => fill(&call_to_action, width),\n                    None => call_to_action,\n                };\n\n                write!(f, \"{}\\n\\n{}\", error, wrapped_cta)\n            }\n\n            ErrorKind::InvalidInvocationOfBareVersion {\n                action,\n                version,\n            } => {\n                let error = format!(\n                    \"`volta {action} {version}` is not supported.\",\n                    action = action,\n                    version = version\n                );\n\n                let call_to_action = format!(\n\"To {action} node version '{version}', please run `volta {action} {formatted}`. \\\nTo {action} the package '{version}', please use an explicit version such as '{version}@latest'.\",\n                    action=action,\n                    version=version,\n                    formatted=tool_version(\"node\", version)\n                );\n\n                let wrapped_cta = match text_width() {\n                    Some(width) => fill(&call_to_action, width),\n                    None => call_to_action,\n                };\n\n                write!(f, \"{}\\n\\n{}\", error, wrapped_cta)\n            }\n\n            ErrorKind::InvalidRegistryFormat { format } => write!(\n                f,\n                \"Unrecognized index registry format: '{}'\n\nPlease specify either 'npm' or 'github' for the format.\",\nformat\n            ),\n\n            ErrorKind::InvalidToolName { name, errors } => {\n                let indentation = \"    \";\n                let wrapped = match text_width() {\n                    Some(width) => fill(&errors.join(\"\\n\"), width - indentation.len()),\n                    None => errors.join(\"\\n\"),\n                };\n                let formatted_errs = indent(&wrapped, indentation);\n\n                let call_to_action = if errors.len() > 1 {\n                    \"Please fix the following errors:\"\n                } else {\n                    \"Please fix the following error:\"\n                };\n\n                write!(\n                    f,\n                    \"Invalid tool name `{}`\\n\\n{}\\n{}\",\n                    name, call_to_action, formatted_errs\n                )\n            }\n            // Note: No CTA as this error is purely informational and shouldn't be exposed to the user\n            ErrorKind::LockAcquireError => write!(\n                f,\n                \"Unable to acquire lock on Volta directory\"\n            ),\n            ErrorKind::NoBundledNpm { command } => write!(\n                f,\n                \"Could not detect bundled npm version.\n\nPlease ensure you have a Node version selected with `volta {} node` (see `volta help {0}` for more info).\",\n                command\n            ),\n            ErrorKind::NoCommandLinePnpm => write!(\n                f,\n                \"No pnpm version specified.\n\nUse `volta run --pnpm` to select a version (see `volta help run` for more info).\"\n            ),\n            ErrorKind::NoCommandLineYarn => write!(\n                f,\n                \"No Yarn version specified.\n\nUse `volta run --yarn` to select a version (see `volta help run` for more info).\"\n            ),\n            ErrorKind::NoDefaultNodeVersion { tool } => write!(\n                f,\n                \"Cannot install {} because the default Node version is not set.\n\nUse `volta install node` to select a default Node first, then install a {0} version.\",\n                                tool\n            ),\n            ErrorKind::NodeVersionNotFound { matching } => write!(\n                f,\n                r#\"Could not find Node version matching \"{}\" in the version registry.\n\nPlease verify that the version is correct.\"#,\n                matching\n            ),\n            ErrorKind::NoHomeEnvironmentVar => write!(\n                f,\n                \"Could not determine home directory.\n\nPlease ensure the environment variable 'HOME' is set.\"\n            ),\n            ErrorKind::NoInstallDir => write!(\n                f,\n                \"Could not determine Volta install directory.\n\nPlease ensure Volta was installed correctly\"\n            ),\n            ErrorKind::NoLocalDataDir => write!(\n                f,\n                \"Could not determine LocalAppData directory.\n\nPlease ensure the directory is available.\"\n            ),\n            ErrorKind::NoPinnedNodeVersion { tool } => write!(\n                f,\n                \"Cannot pin {} because the Node version is not pinned in this project.\n\nUse `volta pin node` to pin Node first, then pin a {0} version.\",\n                tool\n            ),\n            ErrorKind::NoPlatform => write!(\n                f,\n                \"Node is not available.\n\nTo run any Node command, first set a default version using `volta install node`\"\n            ),\n            ErrorKind::NoProjectNodeInManifest => write!(\n                f,\n                \"No Node version found in this project.\n\nUse `volta pin node` to select a version (see `volta help pin` for more info).\"\n            ),\n            ErrorKind::NoProjectPnpm => write!(\n                f,\n                \"No pnpm version found in this project.\n\nUse `volta pin pnpm` to select a version (see `volta help pin` for more info).\"\n            ),\n            ErrorKind::NoProjectYarn => write!(\n                f,\n                \"No Yarn version found in this project.\n\nUse `volta pin yarn` to select a version (see `volta help pin` for more info).\"\n            ),\n            ErrorKind::NoShellProfile { env_profile, bin_dir } => write!(\n                f,\n                \"Could not locate user profile.\nTried $PROFILE ({}), ~/.bashrc, ~/.bash_profile, ~/.zshenv ~/.zshrc, ~/.profile, and ~/.config/fish/config.fish\n\nPlease create one of these and try again; or you can edit your profile manually to add '{}' to your PATH\",\n                env_profile, bin_dir.display()\n            ),\n            ErrorKind::NotInPackage => write!(\n                f,\n                \"Not in a node package.\n\nUse `volta install` to select a default version of a tool.\"\n            ),\n            ErrorKind::NoDefaultPnpm => write!(\n                f,\n                \"pnpm is not available.\n\nUse `volta install pnpm` to select a default version (see `volta help install` for more info).\"\n            ),\n            ErrorKind::NoDefaultYarn => write!(\n                f,\n                \"Yarn is not available.\n\nUse `volta install yarn` to select a default version (see `volta help install` for more info).\"\n            ),\n            ErrorKind::NpmLinkMissingPackage { package } => write!(\n                f,\n                \"Could not locate the package '{}'\n\nPlease ensure it is available by running `npm link` in its source directory.\",\n                package\n            ),\n            ErrorKind::NpmLinkWrongManager { package } => write!(\n                f,\n                \"The package '{}' was not installed using npm and cannot be linked with `npm link`\n\nPlease ensure it is linked with `npm link` or installed with `npm i -g {0}`.\",\n                package\n            ),\n            ErrorKind::NpmVersionNotFound { matching } => write!(\n                f,\n                r#\"Could not find Node version matching \"{}\" in the version registry.\n\nPlease verify that the version is correct.\"#,\n                matching\n            ),\n            ErrorKind::NpxNotAvailable { version } => write!(\n                f,\n                \"'npx' is only available with npm >= 5.2.0\n\nThis project is configured to use version {} of npm.\",\n                version\n            ),\n            ErrorKind::PackageInstallFailed { package } => write!(\n                f,\n                \"Could not install package '{}'\n\nPlease confirm the package is valid and run with `--verbose` for more diagnostics.\",\n                package\n            ),\n            ErrorKind::PackageManifestParseError { package } => write!(\n                f,\n                \"Could not parse package.json manifest for {}\n\nPlease ensure the package includes a valid manifest file.\",\n                package\n            ),\n            ErrorKind::PackageManifestReadError { package } => write!(\n                f,\n                \"Could not read package.json manifest for {}\n\nPlease ensure the package includes a valid manifest file.\",\n                package\n            ),\n            ErrorKind::PackageNotFound { package } => write!(\n                f,\n                \"Could not find '{}' in the package registry.\n\nPlease verify the requested package is correct.\",\n                package\n            ),\n            ErrorKind::PackageParseError { file } => write!(\n                f,\n                \"Could not parse project manifest\nat {}\n\nPlease ensure that the file is correctly formatted.\",\n                file.display()\n            ),\n            ErrorKind::PackageReadError { file } => write!(\n                f,\n                \"Could not read project manifest\nfrom {}\n\nPlease ensure that the file exists.\",\n                file.display()\n            ),\n            ErrorKind::PackageUnpackError => write!(\n                f,\n                \"Could not determine package directory layout.\n\nPlease ensure the package is correctly formatted.\"\n            ),\n            ErrorKind::PackageWriteError { file } => write!(\n                f,\n                \"Could not write project manifest\nto {}\n\nPlease ensure you have correct permissions.\",\n                file.display()\n            ),\n            ErrorKind::ParseBinConfigError => write!(\n                f,\n                \"Could not parse executable configuration file.\n\n{}\",\n                REPORT_BUG_CTA\n            ),\n            ErrorKind::ParseHooksError { file } => write!(\n                f,\n                \"Could not parse hooks configuration file.\nfrom {}\n\nPlease ensure the file is correctly formatted.\",\n                file.display()\n            ),\n            ErrorKind::ParseNodeIndexCacheError => write!(\n                f,\n                \"Could not parse Node index cache file.\n\n{}\",\n                REPORT_BUG_CTA\n            ),\n            ErrorKind::ParseNodeIndexError { from_url } => write!(\n                f,\n                \"Could not parse Node version index\nfrom {}\n\nPlease verify your internet connection.\",\n                from_url\n            ),\n            ErrorKind::ParseNodeIndexExpiryError => write!(\n                f,\n                \"Could not parse Node index cache expiration file.\n\n{}\",\n                REPORT_BUG_CTA\n            ),\n            ErrorKind::ParseNpmManifestError => write!(\n                f,\n                \"Could not parse package.json file for bundled npm.\n\nPlease ensure the version of Node is correct.\"\n            ),\n            ErrorKind::ParsePackageConfigError => write!(\n                f,\n                \"Could not parse package configuration file.\n\n{}\",\n                REPORT_BUG_CTA\n            ),\n            ErrorKind::ParsePlatformError => write!(\n                f,\n                \"Could not parse platform settings file.\n\n{}\",\n                REPORT_BUG_CTA\n            ),\n            ErrorKind::ParseToolSpecError { tool_spec } => write!(\n                f,\n                \"Could not parse tool spec `{}`\n\nPlease supply a spec in the format `<tool name>[@<version>]`.\",\n                tool_spec\n            ),\n            ErrorKind::PersistInventoryError { tool } => write!(\n                f,\n                \"Could not store {} archive in inventory cache\n\n{}\",\n                tool, PERMISSIONS_CTA\n            ),\n            ErrorKind::PnpmVersionNotFound { matching } => write!(\n                f,\n                r#\"Could not find pnpm version matching \"{}\" in the version registry.\n\nPlease verify that the version is correct.\"#,\n                matching\n            ),\n            ErrorKind::ProjectLocalBinaryExecError { command } => write!(\n                f,\n                \"Could not execute `{}`\n\nPlease ensure you have correct permissions to access the file.\",\n                command\n            ),\n            ErrorKind::ProjectLocalBinaryNotFound { command } => write!(\n                f,\n                \"Could not locate executable `{}` in your project.\n\nPlease ensure that all project dependencies are installed with `npm install` or `yarn install`\",\n                command\n            ),\n            ErrorKind::PublishHookBothUrlAndBin => write!(\n                f,\n                \"Publish hook configuration includes both hook types.\n\nPlease include only one of 'bin' or 'url'\"\n            ),\n            ErrorKind::PublishHookNeitherUrlNorBin => write!(\n                f,\n                \"Publish hook configuration includes no hook types.\n\nPlease include one of 'bin' or 'url'\"\n            ),\n            ErrorKind::ReadBinConfigDirError { dir } => write!(\n                f,\n                \"Could not read executable metadata directory\nat {}\n\n{}\",\n                dir.display(),\n                PERMISSIONS_CTA\n            ),\n            ErrorKind::ReadBinConfigError { file } => write!(\n                f,\n                \"Could not read executable configuration\nfrom {}\n\n{}\",\n                file.display(),\n                PERMISSIONS_CTA\n            ),\n            ErrorKind::ReadDefaultNpmError { file } => write!(\n                f,\n                \"Could not read default npm version\nfrom {}\n\n{}\",\n                file.display(),\n                PERMISSIONS_CTA\n            ),\n            ErrorKind::ReadDirError { dir } => write!(\n                f,\n                \"Could not read contents from directory {}\n\n{}\",\n                dir.display(), PERMISSIONS_CTA\n            ),\n            ErrorKind::ReadHooksError { file } => write!(\n                f,\n                \"Could not read hooks file\nfrom {}\n\n{}\",\n                file.display(),\n                PERMISSIONS_CTA\n            ),\n            ErrorKind::ReadNodeIndexCacheError { file } => write!(\n                f,\n                \"Could not read Node index cache\nfrom {}\n\n{}\",\n                file.display(),\n                PERMISSIONS_CTA\n            ),\n            ErrorKind::ReadNodeIndexExpiryError { file } => write!(\n                f,\n                \"Could not read Node index cache expiration\nfrom {}\n\n{}\",\n                file.display(),\n                PERMISSIONS_CTA\n            ),\n            ErrorKind::ReadNpmManifestError => write!(\n                f,\n                \"Could not read package.json file for bundled npm.\n\nPlease ensure the version of Node is correct.\"\n            ),\n            ErrorKind::ReadPackageConfigError { file } => write!(\n                f,\n                \"Could not read package configuration file\nfrom {}\n\n{}\",\n                file.display(),\n                PERMISSIONS_CTA\n            ),\n            ErrorKind::ReadPlatformError { file } => write!(\n                f,\n                \"Could not read default platform file\nfrom {}\n\n{}\",\n                file.display(),\n                PERMISSIONS_CTA\n            ),\n            #[cfg(windows)]\n            ErrorKind::ReadUserPathError => write!(\n                f,\n                \"Could not read user Path environment variable.\n\nPlease ensure you have access to the your environment variables.\"\n            ),\n            ErrorKind::RegistryFetchError { tool, from_url } => write!(\n                f,\n                \"Could not download {} version registry\nfrom {}\n\nPlease verify your internet connection.\",\n                tool, from_url\n            ),\n            ErrorKind::RunShimDirectly => write!(\n                f,\n                \"'volta-shim' should not be called directly.\n\nPlease use the existing shims provided by Volta (node, yarn, etc.) to run tools.\"\n            ),\n            ErrorKind::SetToolExecutable { tool } => write!(\n                f,\n                r#\"Could not set \"{}\" to executable\n\n{}\"#,\n                tool, PERMISSIONS_CTA\n            ),\n            ErrorKind::SetupToolImageError { tool, version, dir } => write!(\n                f,\n                \"Could not create environment for {} v{}\nat {}\n\n{}\",\n                tool,\n                version,\n                dir.display(),\n                PERMISSIONS_CTA\n            ),\n            ErrorKind::ShimCreateError { name } => write!(\n                f,\n                r#\"Could not create shim for \"{}\"\n\n{}\"#,\n                name, PERMISSIONS_CTA\n            ),\n            ErrorKind::ShimRemoveError { name } => write!(\n                f,\n                r#\"Could not remove shim for \"{}\"\n\n{}\"#,\n                name, PERMISSIONS_CTA\n            ),\n            ErrorKind::StringifyBinConfigError => write!(\n                f,\n                \"Could not serialize executable configuration.\n\n{}\",\n                REPORT_BUG_CTA\n            ),\n            ErrorKind::StringifyPackageConfigError => write!(\n                f,\n                \"Could not serialize package configuration.\n\n{}\",\n                REPORT_BUG_CTA\n            ),\n            ErrorKind::StringifyPlatformError => write!(\n                f,\n                \"Could not serialize platform settings.\n\n{}\",\n                REPORT_BUG_CTA\n            ),\n            ErrorKind::Unimplemented { feature } => {\n                write!(f, \"{} is not supported yet.\", feature)\n            }\n            ErrorKind::UnpackArchiveError { tool, version } => write!(\n                f,\n                \"Could not unpack {} v{}\n\nPlease ensure the correct version is specified.\",\n                tool, version\n            ),\n            ErrorKind::UpgradePackageNotFound { package, manager } => write!(\n                f,\n                r#\"Could not locate the package '{}' to upgrade.\n\nPlease ensure it is installed with `{} {0}`\"#,\n                package,\n                match manager {\n                    PackageManager::Npm => \"npm i -g\",\n                    PackageManager::Pnpm => \"pnpm add -g\",\n                    PackageManager::Yarn => \"yarn global add\",\n                }\n            ),\n            ErrorKind::UpgradePackageWrongManager { package, manager } => {\n                let (name, command) = match manager {\n                    PackageManager::Npm => (\"npm\", \"npm update -g\"),\n                    PackageManager::Pnpm => (\"pnpm\", \"pnpm update -g\"),\n                    PackageManager::Yarn => (\"Yarn\", \"yarn global upgrade\"),\n                };\n                write!(\n                    f,\n                    r#\"The package '{}' was installed using {}.\n\nTo upgrade it, please use the command `{} {0}`\"#,\n                    package, name, command\n                )\n            }\n            ErrorKind::VersionParseError { version } => write!(\n                f,\n                r#\"Could not parse version \"{}\"\n\nPlease verify the intended version.\"#,\n                version\n            ),\n            ErrorKind::WriteBinConfigError { file } => write!(\n                f,\n                \"Could not write executable configuration\nto {}\n\n{}\",\n                file.display(),\n                PERMISSIONS_CTA\n            ),\n            ErrorKind::WriteDefaultNpmError { file } => write!(\n                f,\n                \"Could not write bundled npm version\nto {}\n\n{}\",\n                file.display(),\n                PERMISSIONS_CTA\n            ),\n            ErrorKind::WriteLauncherError { tool } => write!(\n                f,\n                \"Could not set up launcher for {}\n\nThis is most likely an intermittent failure, please try again.\",\n                tool\n            ),\n            ErrorKind::WriteNodeIndexCacheError { file } => write!(\n                f,\n                \"Could not write Node index cache\nto {}\n\n{}\",\n                file.display(),\n                PERMISSIONS_CTA\n            ),\n            ErrorKind::WriteNodeIndexExpiryError { file } => write!(\n                f,\n                \"Could not write Node index cache expiration\nto {}\n\n{}\",\n                file.display(),\n                PERMISSIONS_CTA\n            ),\n            ErrorKind::WritePackageConfigError { file } => write!(\n                f,\n                \"Could not write package configuration\nto {}\n\n{}\",\n                file.display(),\n                PERMISSIONS_CTA\n            ),\n            ErrorKind::WritePlatformError { file } => write!(\n                f,\n                \"Could not save platform settings\nto {}\n\n{}\",\n                file.display(),\n                PERMISSIONS_CTA\n            ),\n            #[cfg(windows)]\n            ErrorKind::WriteUserPathError => write!(\n                f,\n                \"Could not write Path environment variable.\n\nPlease ensure you have permissions to edit your environment variables.\"\n            ),\n            ErrorKind::Yarn2NotSupported => write!(\n                f,\n                \"Yarn version 2 is not recommended for use, and not supported by Volta.\n\nPlease use version 3 or greater instead.\"\n            ),\n            ErrorKind::YarnLatestFetchError { from_url } => write!(\n                f,\n                \"Could not fetch latest version of Yarn\nfrom {}\n\nPlease verify your internet connection.\",\n                from_url\n            ),\n            ErrorKind::YarnVersionNotFound { matching } => write!(\n                f,\n                r#\"Could not find Yarn version matching \"{}\" in the version registry.\n\nPlease verify that the version is correct.\"#,\n                matching\n            ),\n        }\n    }\n}\n\nimpl ErrorKind {\n    pub fn exit_code(&self) -> ExitCode {\n        match self {\n            ErrorKind::BinaryAlreadyInstalled { .. } => ExitCode::FileSystemError,\n            ErrorKind::BinaryExecError => ExitCode::ExecutionFailure,\n            ErrorKind::BinaryNotFound { .. } => ExitCode::ExecutableNotFound,\n            ErrorKind::BuildPathError => ExitCode::EnvironmentError,\n            ErrorKind::BypassError { .. } => ExitCode::ExecutionFailure,\n            ErrorKind::CannotFetchPackage { .. } => ExitCode::InvalidArguments,\n            ErrorKind::CannotPinPackage { .. } => ExitCode::InvalidArguments,\n            ErrorKind::CompletionsOutFileError { .. } => ExitCode::InvalidArguments,\n            ErrorKind::ContainingDirError { .. } => ExitCode::FileSystemError,\n            ErrorKind::CouldNotDetermineTool => ExitCode::UnknownError,\n            ErrorKind::CouldNotStartMigration => ExitCode::EnvironmentError,\n            ErrorKind::CreateDirError { .. } => ExitCode::FileSystemError,\n            ErrorKind::CreateLayoutFileError { .. } => ExitCode::FileSystemError,\n            ErrorKind::CreateSharedLinkError { .. } => ExitCode::FileSystemError,\n            ErrorKind::CreateTempDirError { .. } => ExitCode::FileSystemError,\n            ErrorKind::CreateTempFileError { .. } => ExitCode::FileSystemError,\n            ErrorKind::CurrentDirError => ExitCode::EnvironmentError,\n            ErrorKind::DeleteDirectoryError { .. } => ExitCode::FileSystemError,\n            ErrorKind::DeleteFileError { .. } => ExitCode::FileSystemError,\n            ErrorKind::DeprecatedCommandError { .. } => ExitCode::InvalidArguments,\n            ErrorKind::DownloadToolNetworkError { .. } => ExitCode::NetworkError,\n            ErrorKind::ExecuteHookError { .. } => ExitCode::ExecutionFailure,\n            ErrorKind::ExtensionCycleError { .. } => ExitCode::ConfigurationError,\n            ErrorKind::ExtensionPathError { .. } => ExitCode::FileSystemError,\n            ErrorKind::HookCommandFailed { .. } => ExitCode::ConfigurationError,\n            ErrorKind::HookMultipleFieldsSpecified => ExitCode::ConfigurationError,\n            ErrorKind::HookNoFieldsSpecified => ExitCode::ConfigurationError,\n            ErrorKind::HookPathError { .. } => ExitCode::ConfigurationError,\n            ErrorKind::InstalledPackageNameError => ExitCode::UnknownError,\n            ErrorKind::InvalidHookCommand { .. } => ExitCode::ExecutableNotFound,\n            ErrorKind::InvalidHookOutput { .. } => ExitCode::ExecutionFailure,\n            ErrorKind::InvalidInvocation { .. } => ExitCode::InvalidArguments,\n            ErrorKind::InvalidInvocationOfBareVersion { .. } => ExitCode::InvalidArguments,\n            ErrorKind::InvalidRegistryFormat { .. } => ExitCode::ConfigurationError,\n            ErrorKind::InvalidToolName { .. } => ExitCode::InvalidArguments,\n            ErrorKind::LockAcquireError => ExitCode::FileSystemError,\n            ErrorKind::NoBundledNpm { .. } => ExitCode::ConfigurationError,\n            ErrorKind::NoCommandLinePnpm => ExitCode::ConfigurationError,\n            ErrorKind::NoCommandLineYarn => ExitCode::ConfigurationError,\n            ErrorKind::NoDefaultNodeVersion { .. } => ExitCode::ConfigurationError,\n            ErrorKind::NodeVersionNotFound { .. } => ExitCode::NoVersionMatch,\n            ErrorKind::NoHomeEnvironmentVar => ExitCode::EnvironmentError,\n            ErrorKind::NoInstallDir => ExitCode::EnvironmentError,\n            ErrorKind::NoLocalDataDir => ExitCode::EnvironmentError,\n            ErrorKind::NoPinnedNodeVersion { .. } => ExitCode::ConfigurationError,\n            ErrorKind::NoPlatform => ExitCode::ConfigurationError,\n            ErrorKind::NoProjectNodeInManifest => ExitCode::ConfigurationError,\n            ErrorKind::NoProjectPnpm => ExitCode::ConfigurationError,\n            ErrorKind::NoProjectYarn => ExitCode::ConfigurationError,\n            ErrorKind::NoShellProfile { .. } => ExitCode::EnvironmentError,\n            ErrorKind::NotInPackage => ExitCode::ConfigurationError,\n            ErrorKind::NoDefaultPnpm => ExitCode::ConfigurationError,\n            ErrorKind::NoDefaultYarn => ExitCode::ConfigurationError,\n            ErrorKind::NpmLinkMissingPackage { .. } => ExitCode::ConfigurationError,\n            ErrorKind::NpmLinkWrongManager { .. } => ExitCode::ConfigurationError,\n            ErrorKind::NpmVersionNotFound { .. } => ExitCode::NoVersionMatch,\n            ErrorKind::NpxNotAvailable { .. } => ExitCode::ExecutableNotFound,\n            ErrorKind::PackageInstallFailed { .. } => ExitCode::UnknownError,\n            ErrorKind::PackageManifestParseError { .. } => ExitCode::ConfigurationError,\n            ErrorKind::PackageManifestReadError { .. } => ExitCode::FileSystemError,\n            ErrorKind::PackageNotFound { .. } => ExitCode::InvalidArguments,\n            ErrorKind::PackageParseError { .. } => ExitCode::ConfigurationError,\n            ErrorKind::PackageReadError { .. } => ExitCode::FileSystemError,\n            ErrorKind::PackageUnpackError => ExitCode::ConfigurationError,\n            ErrorKind::PackageWriteError { .. } => ExitCode::FileSystemError,\n            ErrorKind::ParseBinConfigError => ExitCode::UnknownError,\n            ErrorKind::ParseHooksError { .. } => ExitCode::ConfigurationError,\n            ErrorKind::ParseToolSpecError { .. } => ExitCode::InvalidArguments,\n            ErrorKind::ParseNodeIndexCacheError => ExitCode::UnknownError,\n            ErrorKind::ParseNodeIndexError { .. } => ExitCode::NetworkError,\n            ErrorKind::ParseNodeIndexExpiryError => ExitCode::UnknownError,\n            ErrorKind::ParseNpmManifestError => ExitCode::UnknownError,\n            ErrorKind::ParsePackageConfigError => ExitCode::UnknownError,\n            ErrorKind::ParsePlatformError => ExitCode::ConfigurationError,\n            ErrorKind::PersistInventoryError { .. } => ExitCode::FileSystemError,\n            ErrorKind::PnpmVersionNotFound { .. } => ExitCode::NoVersionMatch,\n            ErrorKind::ProjectLocalBinaryExecError { .. } => ExitCode::ExecutionFailure,\n            ErrorKind::ProjectLocalBinaryNotFound { .. } => ExitCode::FileSystemError,\n            ErrorKind::PublishHookBothUrlAndBin => ExitCode::ConfigurationError,\n            ErrorKind::PublishHookNeitherUrlNorBin => ExitCode::ConfigurationError,\n            ErrorKind::ReadBinConfigDirError { .. } => ExitCode::FileSystemError,\n            ErrorKind::ReadBinConfigError { .. } => ExitCode::FileSystemError,\n            ErrorKind::ReadDefaultNpmError { .. } => ExitCode::FileSystemError,\n            ErrorKind::ReadDirError { .. } => ExitCode::FileSystemError,\n            ErrorKind::ReadHooksError { .. } => ExitCode::FileSystemError,\n            ErrorKind::ReadNodeIndexCacheError { .. } => ExitCode::FileSystemError,\n            ErrorKind::ReadNodeIndexExpiryError { .. } => ExitCode::FileSystemError,\n            ErrorKind::ReadNpmManifestError => ExitCode::UnknownError,\n            ErrorKind::ReadPackageConfigError { .. } => ExitCode::FileSystemError,\n            ErrorKind::ReadPlatformError { .. } => ExitCode::FileSystemError,\n            #[cfg(windows)]\n            ErrorKind::ReadUserPathError => ExitCode::EnvironmentError,\n            ErrorKind::RegistryFetchError { .. } => ExitCode::NetworkError,\n            ErrorKind::RunShimDirectly => ExitCode::InvalidArguments,\n            ErrorKind::SetupToolImageError { .. } => ExitCode::FileSystemError,\n            ErrorKind::SetToolExecutable { .. } => ExitCode::FileSystemError,\n            ErrorKind::ShimCreateError { .. } => ExitCode::FileSystemError,\n            ErrorKind::ShimRemoveError { .. } => ExitCode::FileSystemError,\n            ErrorKind::StringifyBinConfigError => ExitCode::UnknownError,\n            ErrorKind::StringifyPackageConfigError => ExitCode::UnknownError,\n            ErrorKind::StringifyPlatformError => ExitCode::UnknownError,\n            ErrorKind::Unimplemented { .. } => ExitCode::UnknownError,\n            ErrorKind::UnpackArchiveError { .. } => ExitCode::UnknownError,\n            ErrorKind::UpgradePackageNotFound { .. } => ExitCode::ConfigurationError,\n            ErrorKind::UpgradePackageWrongManager { .. } => ExitCode::ConfigurationError,\n            ErrorKind::VersionParseError { .. } => ExitCode::NoVersionMatch,\n            ErrorKind::WriteBinConfigError { .. } => ExitCode::FileSystemError,\n            ErrorKind::WriteDefaultNpmError { .. } => ExitCode::FileSystemError,\n            ErrorKind::WriteLauncherError { .. } => ExitCode::FileSystemError,\n            ErrorKind::WriteNodeIndexCacheError { .. } => ExitCode::FileSystemError,\n            ErrorKind::WriteNodeIndexExpiryError { .. } => ExitCode::FileSystemError,\n            ErrorKind::WritePackageConfigError { .. } => ExitCode::FileSystemError,\n            ErrorKind::WritePlatformError { .. } => ExitCode::FileSystemError,\n            #[cfg(windows)]\n            ErrorKind::WriteUserPathError => ExitCode::EnvironmentError,\n            ErrorKind::Yarn2NotSupported => ExitCode::NoVersionMatch,\n            ErrorKind::YarnLatestFetchError { .. } => ExitCode::NetworkError,\n            ErrorKind::YarnVersionNotFound { .. } => ExitCode::NoVersionMatch,\n        }\n    }\n}\n"
  },
  {
    "path": "crates/volta-core/src/error/mod.rs",
    "content": "use std::error::Error;\nuse std::fmt;\nuse std::process::exit;\n\nmod kind;\nmod reporter;\n\npub use kind::ErrorKind;\npub use reporter::report_error;\n\npub type Fallible<T> = Result<T, VoltaError>;\n\n/// Error type for Volta\n#[derive(Debug)]\npub struct VoltaError {\n    inner: Box<Inner>,\n}\n\n#[derive(Debug)]\nstruct Inner {\n    kind: ErrorKind,\n    source: Option<Box<dyn Error>>,\n}\n\nimpl VoltaError {\n    /// The exit code Volta should use when this error stops execution\n    pub fn exit_code(&self) -> ExitCode {\n        self.inner.kind.exit_code()\n    }\n\n    /// Create a new VoltaError instance including a source error\n    pub fn from_source<E>(source: E, kind: ErrorKind) -> Self\n    where\n        E: Into<Box<dyn Error>>,\n    {\n        VoltaError {\n            inner: Box::new(Inner {\n                kind,\n                source: Some(source.into()),\n            }),\n        }\n    }\n\n    /// Get a reference to the ErrorKind for this error\n    pub fn kind(&self) -> &ErrorKind {\n        &self.inner.kind\n    }\n}\n\nimpl fmt::Display for VoltaError {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        self.inner.kind.fmt(f)\n    }\n}\n\nimpl Error for VoltaError {\n    fn source(&self) -> Option<&(dyn Error + 'static)> {\n        self.inner.source.as_ref().map(|b| b.as_ref())\n    }\n}\n\nimpl From<ErrorKind> for VoltaError {\n    fn from(kind: ErrorKind) -> Self {\n        VoltaError {\n            inner: Box::new(Inner { kind, source: None }),\n        }\n    }\n}\n\n/// Trait providing the with_context method to easily convert any Result error into a VoltaError\npub trait Context<T> {\n    fn with_context<F>(self, f: F) -> Fallible<T>\n    where\n        F: FnOnce() -> ErrorKind;\n}\n\nimpl<T, E> Context<T> for Result<T, E>\nwhere\n    E: Error + 'static,\n{\n    fn with_context<F>(self, f: F) -> Fallible<T>\n    where\n        F: FnOnce() -> ErrorKind,\n    {\n        self.map_err(|e| VoltaError::from_source(e, f()))\n    }\n}\n\n/// Exit codes supported by Volta Errors\n#[derive(Copy, Clone, Debug)]\npub enum ExitCode {\n    /// No error occurred.\n    Success = 0,\n\n    /// An unknown error occurred.\n    UnknownError = 1,\n\n    /// An invalid combination of command-line arguments was supplied.\n    InvalidArguments = 3,\n\n    /// No match could be found for the requested version string.\n    NoVersionMatch = 4,\n\n    /// A network error occurred.\n    NetworkError = 5,\n\n    /// A required environment variable was unset or invalid.\n    EnvironmentError = 6,\n\n    /// A file could not be read or written.\n    FileSystemError = 7,\n\n    /// Package configuration is missing or incorrect.\n    ConfigurationError = 8,\n\n    /// The command or feature is not yet implemented.\n    NotYetImplemented = 9,\n\n    /// The requested executable could not be run.\n    ExecutionFailure = 126,\n\n    /// The requested executable is not available.\n    ExecutableNotFound = 127,\n}\n\nimpl ExitCode {\n    pub fn exit(self) -> ! {\n        exit(self as i32);\n    }\n}\n"
  },
  {
    "path": "crates/volta-core/src/error/reporter.rs",
    "content": "use std::env::args_os;\nuse std::error::Error;\nuse std::fs::File;\nuse std::io::Write;\nuse std::path::PathBuf;\n\nuse super::VoltaError;\nuse crate::layout::volta_home;\nuse crate::style::format_error_cause;\nuse chrono::Local;\nuse ci_info::is_ci;\nuse console::strip_ansi_codes;\nuse fs_utils::ensure_containing_dir_exists;\nuse log::{debug, error};\n\n/// Report an error, both to the console and to error logs\npub fn report_error(volta_version: &str, err: &VoltaError) {\n    let message = err.to_string();\n    error!(\"{}\", message);\n\n    if let Some(details) = compose_error_details(err) {\n        if is_ci() {\n            // In CI, we write the error details to the log so that they are available in the CI logs\n            // A log file may not even exist by the time the user is reviewing a failure\n            error!(\"{}\", details);\n        } else {\n            // Outside of CI, we write the error details as Debug (Verbose) information\n            // And we write an actual error log that the user can review\n            debug!(\"{}\", details);\n\n            // Note: Writing the error log info directly to stderr as it is a message for the user\n            // Any custom logs will have all of the details already, so showing a message about writing\n            // the error log would be redundant\n            match write_error_log(volta_version, message, details) {\n                Ok(log_file) => {\n                    eprintln!(\"Error details written to {}\", log_file.to_string_lossy());\n                }\n                Err(_) => {\n                    eprintln!(\"Unable to write error log!\");\n                }\n            }\n        }\n    }\n}\n\n/// Write an error log with all details about the error\nfn write_error_log(\n    volta_version: &str,\n    message: String,\n    details: String,\n) -> Result<PathBuf, Box<dyn Error>> {\n    let file_name = Local::now()\n        .format(\"volta-error-%Y-%m-%d_%H_%M_%S%.3f.log\")\n        .to_string();\n    let log_file_path = volta_home()?.log_dir().join(file_name);\n\n    ensure_containing_dir_exists(&log_file_path)?;\n    let mut log_file = File::create(&log_file_path)?;\n\n    writeln!(log_file, \"{}\", collect_arguments())?;\n    writeln!(log_file, \"Volta v{}\", volta_version)?;\n    writeln!(log_file)?;\n    writeln!(log_file, \"{}\", strip_ansi_codes(&message))?;\n    writeln!(log_file)?;\n    writeln!(log_file, \"{}\", strip_ansi_codes(&details))?;\n\n    Ok(log_file_path)\n}\n\nfn compose_error_details(err: &VoltaError) -> Option<String> {\n    // Only compose details if there is an underlying cause for the error\n    let mut current = err.source()?;\n    let mut details = String::new();\n\n    // Walk up the tree of causes and include all of them\n    loop {\n        details.push_str(&format_error_cause(current));\n\n        match current.source() {\n            Some(cause) => {\n                details.push_str(\"\\n\\n\");\n                current = cause;\n            }\n            None => {\n                break;\n            }\n        };\n    }\n\n    Some(details)\n}\n\n/// Combines all the arguments into a single String\nfn collect_arguments() -> String {\n    // The Debug formatter for OsString properly quotes and escapes each value\n    args_os()\n        .map(|arg| format!(\"{:?}\", arg))\n        .collect::<Vec<String>>()\n        .join(\" \")\n}\n"
  },
  {
    "path": "crates/volta-core/src/event.rs",
    "content": "//! Events for the sessions in executables and shims and everything\n\nuse std::env;\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nuse serde::{Deserialize, Serialize};\n\nuse crate::error::{ExitCode, VoltaError};\nuse crate::hook::Publish;\nuse crate::monitor::send_events;\nuse crate::session::ActivityKind;\n\n// the Event data that is serialized to JSON and sent the plugin\n#[derive(Deserialize, Serialize)]\npub struct Event {\n    timestamp: u64,\n    pub name: String,\n    pub event: EventKind,\n}\n\n#[derive(Deserialize, Serialize, PartialEq, Eq, Debug)]\npub struct ErrorEnv {\n    argv: String,\n    exec_path: String,\n    path: String,\n    platform: String,\n    platform_version: String,\n}\n\n#[derive(Deserialize, Serialize, PartialEq, Eq, Debug)]\n#[serde(rename_all = \"lowercase\")]\npub enum EventKind {\n    Start,\n    End {\n        exit_code: i32,\n    },\n    Error {\n        exit_code: i32,\n        error: String,\n        env: ErrorEnv,\n    },\n    ToolEnd {\n        exit_code: i32,\n    },\n    Args {\n        argv: String,\n    },\n}\n\nimpl EventKind {\n    pub fn into_event(self, activity_kind: ActivityKind) -> Event {\n        Event {\n            timestamp: unix_timestamp(),\n            name: activity_kind.to_string(),\n            event: self,\n        }\n    }\n}\n\n// returns the current number of milliseconds since the epoch\nfn unix_timestamp() -> u64 {\n    let start = SystemTime::now();\n    let duration = start\n        .duration_since(UNIX_EPOCH)\n        .expect(\"Time went backwards\");\n    let nanosecs_since_epoch = duration.as_secs() * 1_000_000_000 + duration.subsec_nanos() as u64;\n    nanosecs_since_epoch / 1_000_000\n}\n\nfn get_error_env() -> ErrorEnv {\n    let path = match env::var(\"PATH\") {\n        Ok(p) => p,\n        Err(_e) => \"error: Unable to get path from environment\".to_string(),\n    };\n    let argv = env::args().collect::<Vec<String>>().join(\" \");\n    let exec_path = match env::current_exe() {\n        Ok(ep) => ep.display().to_string(),\n        Err(_e) => \"error: Unable to get executable path from environment\".to_string(),\n    };\n\n    let info = os_info::get();\n    let platform = info.os_type().to_string();\n    let platform_version = info.version().to_string();\n\n    ErrorEnv {\n        argv,\n        exec_path,\n        path,\n        platform,\n        platform_version,\n    }\n}\n\npub struct EventLog {\n    events: Vec<Event>,\n}\n\nimpl EventLog {\n    /// Constructs a new 'EventLog'\n    pub fn init() -> Self {\n        EventLog { events: Vec::new() }\n    }\n\n    pub fn add_event_start(&mut self, activity_kind: ActivityKind) {\n        self.add_event(EventKind::Start, activity_kind)\n    }\n    pub fn add_event_end(&mut self, activity_kind: ActivityKind, exit_code: ExitCode) {\n        self.add_event(\n            EventKind::End {\n                exit_code: exit_code as i32,\n            },\n            activity_kind,\n        )\n    }\n    pub fn add_event_tool_end(&mut self, activity_kind: ActivityKind, exit_code: i32) {\n        self.add_event(EventKind::ToolEnd { exit_code }, activity_kind)\n    }\n    pub fn add_event_error(&mut self, activity_kind: ActivityKind, error: &VoltaError) {\n        self.add_event(\n            EventKind::Error {\n                exit_code: error.exit_code() as i32,\n                error: error.to_string(),\n                env: get_error_env(),\n            },\n            activity_kind,\n        )\n    }\n    pub fn add_event_args(&mut self) {\n        let argv = env::args_os()\n            .enumerate()\n            .fold(String::new(), |mut result, (i, arg)| {\n                if i > 0 {\n                    result.push(' ');\n                }\n                result.push_str(&arg.to_string_lossy());\n                result\n            });\n        self.add_event(EventKind::Args { argv }, ActivityKind::Args)\n    }\n\n    fn add_event(&mut self, event_kind: EventKind, activity_kind: ActivityKind) {\n        let event = event_kind.into_event(activity_kind);\n        self.events.push(event);\n    }\n\n    pub fn publish(&self, plugin: Option<&Publish>) {\n        match plugin {\n            // Note: This call to unimplemented is left in, as it's not a Fallible operation that can use ErrorKind::Unimplemented\n            Some(Publish::Url(_)) => unimplemented!(),\n            Some(Publish::Bin(command)) => {\n                send_events(command, &self.events);\n            }\n            None => {}\n        }\n    }\n}\n\n#[cfg(test)]\npub mod tests {\n\n    use super::{EventKind, EventLog};\n    use crate::error::{ErrorKind, ExitCode};\n    use crate::session::ActivityKind;\n    use regex::Regex;\n\n    #[test]\n    fn test_adding_events() {\n        let mut event_log = EventLog::init();\n        assert_eq!(event_log.events.len(), 0);\n\n        event_log.add_event_start(ActivityKind::Current);\n        assert_eq!(event_log.events.len(), 1);\n        assert_eq!(event_log.events[0].name, \"current\");\n        assert_eq!(event_log.events[0].event, EventKind::Start);\n\n        event_log.add_event_end(ActivityKind::Pin, ExitCode::NetworkError);\n        assert_eq!(event_log.events.len(), 2);\n        assert_eq!(event_log.events[1].name, \"pin\");\n        assert_eq!(event_log.events[1].event, EventKind::End { exit_code: 5 });\n\n        event_log.add_event_tool_end(ActivityKind::Version, 12);\n        assert_eq!(event_log.events.len(), 3);\n        assert_eq!(event_log.events[2].name, \"version\");\n        assert_eq!(\n            event_log.events[2].event,\n            EventKind::ToolEnd { exit_code: 12 }\n        );\n\n        let error = ErrorKind::BinaryExecError.into();\n        event_log.add_event_error(ActivityKind::Install, &error);\n        assert_eq!(event_log.events.len(), 4);\n        assert_eq!(event_log.events[3].name, \"install\");\n        // not checking the error because it has too much machine-specific info\n\n        event_log.add_event_args();\n        assert_eq!(event_log.events.len(), 5);\n        assert_eq!(event_log.events[4].name, \"args\");\n        match event_log.events[4].event {\n            EventKind::Args { ref argv } => {\n                let re = Regex::new(\"volta_core\").unwrap();\n                assert!(re.is_match(argv));\n            }\n            _ => {\n                panic!(\n                    \"Expected EventKind::Args {{ argv }}, Got: {:?}\",\n                    event_log.events[4].event\n                );\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "crates/volta-core/src/fs.rs",
    "content": "//! Provides utilities for operating on the filesystem.\n\nuse std::fs::{self, create_dir_all, read_dir, DirEntry, File, Metadata};\nuse std::io;\n#[cfg(unix)]\nuse std::os::unix::fs::PermissionsExt;\nuse std::path::Path;\n\nuse crate::error::{Context, ErrorKind, Fallible};\nuse crate::layout::volta_home;\nuse retry::delay::Fibonacci;\nuse retry::{retry, OperationResult};\nuse tempfile::{tempdir_in, NamedTempFile, TempDir};\n\n/// Opens a file, creating it if it doesn't exist\npub fn touch(path: &Path) -> io::Result<File> {\n    if !path.is_file() {\n        if let Some(basedir) = path.parent() {\n            create_dir_all(basedir)?;\n        }\n        File::create(path)?;\n    }\n    File::open(path)\n}\n\n/// Removes the target directory, if it exists. If the directory doesn't exist, that is treated as\n/// success.\npub fn remove_dir_if_exists<P: AsRef<Path>>(path: P) -> Fallible<()> {\n    fs::remove_dir_all(&path)\n        .or_else(ok_if_not_found)\n        .with_context(|| ErrorKind::DeleteDirectoryError {\n            directory: path.as_ref().to_owned(),\n        })\n}\n\n/// Removes the target file, if it exists. If the file doesn't exist, that is treated as success.\npub fn remove_file_if_exists<P: AsRef<Path>>(path: P) -> Fallible<()> {\n    fs::remove_file(&path)\n        .or_else(ok_if_not_found)\n        .with_context(|| ErrorKind::DeleteFileError {\n            file: path.as_ref().to_owned(),\n        })\n}\n\n/// Converts a failure because of file not found into a success.\n///\n/// Handling the error is preferred over checking if a file exists before removing it, since\n/// that avoids a potential race condition between the check and the removal.\npub fn ok_if_not_found<T: Default>(err: io::Error) -> io::Result<T> {\n    match err.kind() {\n        io::ErrorKind::NotFound => Ok(T::default()),\n        _ => Err(err),\n    }\n}\n\n/// Reads a file, if it exists.\npub fn read_file<P: AsRef<Path>>(path: P) -> io::Result<Option<String>> {\n    let result: io::Result<String> = fs::read_to_string(path);\n\n    match result {\n        Ok(string) => Ok(Some(string)),\n        Err(error) => match error.kind() {\n            io::ErrorKind::NotFound => Ok(None),\n            _ => Err(error),\n        },\n    }\n}\n\n/// Reads the full contents of a directory, eagerly extracting each directory entry\n/// and its metadata and returning an iterator over them. Returns `Error` if any of\n/// these steps fails.\n///\n/// This function makes it easier to write high level logic for manipulating the\n/// contents of directories (map, filter, etc).\n///\n/// Note that this function allocates an intermediate vector of directory entries to\n/// construct the iterator from, so if a directory is expected to be very large, it\n/// will allocate temporary data proportional to the number of entries.\npub fn read_dir_eager(dir: &Path) -> io::Result<impl Iterator<Item = (DirEntry, Metadata)>> {\n    let entries = read_dir(dir)?;\n    let vec = entries\n        .map(|entry| {\n            let entry = entry?;\n            let metadata = entry.metadata()?;\n            Ok((entry, metadata))\n        })\n        .collect::<io::Result<Vec<(DirEntry, Metadata)>>>()?;\n\n    Ok(vec.into_iter())\n}\n\n/// Reads the contents of a directory and returns a Vec of the matched results\n/// from the input function\npub fn dir_entry_match<T, F>(dir: &Path, mut f: F) -> io::Result<Vec<T>>\nwhere\n    F: FnMut(&DirEntry) -> Option<T>,\n{\n    let entries = read_dir_eager(dir)?;\n    Ok(entries\n        .filter(|(_, metadata)| metadata.is_file())\n        .filter_map(|(entry, _)| f(&entry))\n        .collect::<Vec<T>>())\n}\n\n/// Creates a NamedTempFile in the Volta tmp directory\npub fn create_staging_file() -> Fallible<NamedTempFile> {\n    let tmp_dir = volta_home()?.tmp_dir();\n    NamedTempFile::new_in(tmp_dir).with_context(|| ErrorKind::CreateTempFileError {\n        in_dir: tmp_dir.to_owned(),\n    })\n}\n\n/// Creates a staging directory in the Volta tmp directory\npub fn create_staging_dir() -> Fallible<TempDir> {\n    let tmp_root = volta_home()?.tmp_dir();\n    tempdir_in(tmp_root).with_context(|| ErrorKind::CreateTempDirError {\n        in_dir: tmp_root.to_owned(),\n    })\n}\n\n/// Create a file symlink. The `dst` path will be a symbolic link pointing to the `src` path.\npub fn symlink_file<S, D>(src: S, dest: D) -> io::Result<()>\nwhere\n    S: AsRef<Path>,\n    D: AsRef<Path>,\n{\n    #[cfg(windows)]\n    return std::os::windows::fs::symlink_file(src, dest);\n\n    #[cfg(unix)]\n    return std::os::unix::fs::symlink(src, dest);\n}\n\n/// Create a directory symlink. The `dst` path will be a symbolic link pointing to the `src` path\npub fn symlink_dir<S, D>(src: S, dest: D) -> io::Result<()>\nwhere\n    S: AsRef<Path>,\n    D: AsRef<Path>,\n{\n    #[cfg(windows)]\n    return junction::create(src, dest);\n\n    #[cfg(unix)]\n    return std::os::unix::fs::symlink(src, dest);\n}\n\n/// Ensure that a given file has 'executable' permissions, otherwise we won't be able to call it\n#[cfg(unix)]\npub fn set_executable(bin: &Path) -> io::Result<()> {\n    let mut permissions = fs::metadata(bin)?.permissions();\n    let mode = permissions.mode();\n\n    if mode & 0o111 != 0o111 {\n        permissions.set_mode(mode | 0o111);\n        fs::set_permissions(bin, permissions)\n    } else {\n        Ok(())\n    }\n}\n\n/// Ensure that a given file has 'executable' permissions, otherwise we won't be able to call it\n///\n/// Note: This is a no-op on Windows, which has no concept of 'executable' permissions\n#[cfg(windows)]\npub fn set_executable(_bin: &Path) -> io::Result<()> {\n    Ok(())\n}\n\n/// Rename a file or directory to a new name, retrying if the operation fails because of permissions\n///\n/// Will retry for ~30 seconds with longer and longer delays between each, to allow for virus scan\n/// and other automated operations to complete.\npub fn rename<F, T>(from: F, to: T) -> io::Result<()>\nwhere\n    F: AsRef<Path>,\n    T: AsRef<Path>,\n{\n    // 21 Fibonacci steps starting at 1 ms is ~28 seconds total\n    // See https://github.com/rust-lang/rustup/pull/1873 where this was used by Rustup to work around\n    // virus scanning file locks\n    let from = from.as_ref();\n    let to = to.as_ref();\n\n    retry(Fibonacci::from_millis(1).take(21), || {\n        match fs::rename(from, to) {\n            Ok(_) => OperationResult::Ok(()),\n            Err(e) => match e.kind() {\n                io::ErrorKind::PermissionDenied => OperationResult::Retry(e),\n                _ => OperationResult::Err(e),\n            },\n        }\n    })\n    .map_err(|e| e.error)\n}\n"
  },
  {
    "path": "crates/volta-core/src/hook/mod.rs",
    "content": "//! Provides types for working with Volta hooks.\n\nuse std::borrow::Cow;\nuse std::fs::File;\nuse std::iter::once;\nuse std::marker::PhantomData;\nuse std::path::Path;\n\nuse crate::error::{Context, ErrorKind, Fallible};\nuse crate::layout::volta_home;\nuse crate::project::Project;\nuse crate::tool::{Node, Npm, Pnpm, Tool};\nuse log::debug;\nuse once_cell::unsync::OnceCell;\n\npub(crate) mod serial;\npub mod tool;\n\n/// A hook for publishing Volta events.\n#[derive(PartialEq, Eq, Debug)]\npub enum Publish {\n    /// Reports an event by sending a POST request to a URL.\n    Url(String),\n\n    /// Reports an event by forking a process and sending the event by IPC.\n    Bin(String),\n}\n\n/// Lazily loaded Volta hook configuration\npub struct LazyHookConfig {\n    settings: OnceCell<HookConfig>,\n}\n\nimpl LazyHookConfig {\n    /// Constructs a new `LazyHookConfig`\n    pub fn init() -> LazyHookConfig {\n        LazyHookConfig {\n            settings: OnceCell::new(),\n        }\n    }\n\n    /// Forces the loading of the hook configuration from both project-local and user-default hooks\n    pub fn get(&self, project: Option<&Project>) -> Fallible<&HookConfig> {\n        self.settings\n            .get_or_try_init(|| HookConfig::current(project))\n    }\n}\n\n/// Volta hook configuration\npub struct HookConfig {\n    node: Option<ToolHooks<Node>>,\n    npm: Option<ToolHooks<Npm>>,\n    pnpm: Option<ToolHooks<Pnpm>>,\n    yarn: Option<YarnHooks>,\n    events: Option<EventHooks>,\n}\n\n/// Volta hooks for an individual tool\npub struct ToolHooks<T: Tool> {\n    /// The hook for resolving the URL for a distro version\n    pub distro: Option<tool::DistroHook>,\n    /// The hook for resolving the URL for the latest version\n    pub latest: Option<tool::MetadataHook>,\n    /// The hook for resolving the Tool Index URL\n    pub index: Option<tool::MetadataHook>,\n\n    phantom: PhantomData<T>,\n}\n\n/// Volta hooks for Yarn\npub struct YarnHooks {\n    /// The hook for resolving the URL for a distro version\n    pub distro: Option<tool::DistroHook>,\n    /// The hook for resolving the URL for the latest version\n    pub latest: Option<tool::MetadataHook>,\n    /// The hook for resolving the Tool Index URL\n    pub index: Option<tool::YarnIndexHook>,\n}\n\nimpl<T: Tool> ToolHooks<T> {\n    /// Extends this ToolHooks with another, giving precendence to the current instance\n    fn merge(self, other: Self) -> Self {\n        Self {\n            distro: self.distro.or(other.distro),\n            latest: self.latest.or(other.latest),\n            index: self.index.or(other.index),\n            phantom: PhantomData,\n        }\n    }\n}\n\nimpl YarnHooks {\n    /// Extends this YarnHooks with another, giving precendence to the current instance\n    fn merge(self, other: Self) -> Self {\n        Self {\n            distro: self.distro.or(other.distro),\n            latest: self.latest.or(other.latest),\n            index: self.index.or(other.index),\n        }\n    }\n}\n\nmacro_rules! merge_hooks {\n    ($self:ident, $other:ident, $field:ident) => {\n        match ($self.$field, $other.$field) {\n            (Some(current), Some(other)) => Some(current.merge(other)),\n            (Some(single), None) | (None, Some(single)) => Some(single),\n            (None, None) => None,\n        }\n    };\n}\n\nimpl HookConfig {\n    pub fn node(&self) -> Option<&ToolHooks<Node>> {\n        self.node.as_ref()\n    }\n\n    pub fn npm(&self) -> Option<&ToolHooks<Npm>> {\n        self.npm.as_ref()\n    }\n\n    pub fn pnpm(&self) -> Option<&ToolHooks<Pnpm>> {\n        self.pnpm.as_ref()\n    }\n\n    pub fn yarn(&self) -> Option<&YarnHooks> {\n        self.yarn.as_ref()\n    }\n\n    pub fn events(&self) -> Option<&EventHooks> {\n        self.events.as_ref()\n    }\n\n    /// Returns the current hooks, which are a merge between the user hooks and\n    /// the project hooks (if any).\n    fn current(project: Option<&Project>) -> Fallible<Self> {\n        let default_hooks_file = volta_home()?.default_hooks_file();\n\n        // Since `from_paths` expects the paths to be sorted in descending precedence order, we\n        // include all project hooks first (workspace_roots is already sorted in descending\n        // precedence order)\n        // See the per-project configuration RFC for more details on the configuration precedence:\n        // https://github.com/volta-cli/rfcs/blob/main/text/0033-per-project-config.md#configuration-precedence\n        let paths = project\n            .into_iter()\n            .flat_map(Project::workspace_roots)\n            .map(|root| {\n                let mut path = root.join(\".volta\");\n                path.push(\"hooks.json\");\n                Cow::Owned(path)\n            })\n            .chain(once(Cow::Borrowed(default_hooks_file)));\n\n        Self::from_paths(paths)\n    }\n\n    /// Returns the merged hooks loaded from an iterator of potential hook files\n    ///\n    /// `paths` should be sorted in order of descending precedence.\n    fn from_paths<P, I>(paths: I) -> Fallible<Self>\n    where\n        P: AsRef<Path>,\n        I: IntoIterator<Item = P>,\n    {\n        paths\n            .into_iter()\n            .try_fold(None, |acc: Option<Self>, hooks_file| {\n                // Try to load the hooks and merge with any already loaded hooks\n                match Self::from_file(hooks_file.as_ref())? {\n                    Some(hooks) => {\n                        debug!(\n                            \"Loaded custom hooks file: {}\",\n                            hooks_file.as_ref().display()\n                        );\n                        Ok(Some(match acc {\n                            Some(loaded) => loaded.merge(hooks),\n                            None => hooks,\n                        }))\n                    }\n                    None => Ok(acc),\n                }\n            })\n            // If there were no hooks loaded at all, provide a default empty HookConfig\n            .map(|maybe_config| {\n                maybe_config.unwrap_or_else(|| {\n                    debug!(\"No custom hooks found\");\n                    Self {\n                        node: None,\n                        npm: None,\n                        pnpm: None,\n                        yarn: None,\n                        events: None,\n                    }\n                })\n            })\n    }\n\n    fn from_file(file_path: &Path) -> Fallible<Option<Self>> {\n        if !file_path.is_file() {\n            return Ok(None);\n        }\n\n        let file = File::open(file_path).with_context(|| ErrorKind::ReadHooksError {\n            file: file_path.to_path_buf(),\n        })?;\n\n        let raw: serial::RawHookConfig =\n            serde_json::de::from_reader(file).with_context(|| ErrorKind::ParseHooksError {\n                file: file_path.to_path_buf(),\n            })?;\n\n        // Invariant: Since we successfully loaded it, we know we have a valid file path\n        let hooks_path = file_path.parent().expect(\"File paths always have a parent\");\n\n        raw.into_hook_config(hooks_path).map(Some)\n    }\n\n    /// Merges this HookConfig with another, giving precedence to the current instance\n    fn merge(self, other: Self) -> Self {\n        Self {\n            node: merge_hooks!(self, other, node),\n            npm: merge_hooks!(self, other, npm),\n            pnpm: merge_hooks!(self, other, pnpm),\n            yarn: merge_hooks!(self, other, yarn),\n            events: merge_hooks!(self, other, events),\n        }\n    }\n}\n\n/// Format of the registry used for Yarn (Npm or Github)\n#[derive(PartialEq, Eq, Debug)]\npub enum RegistryFormat {\n    Npm,\n    Github,\n}\n\nimpl RegistryFormat {\n    pub fn from_str(raw_format: &str) -> Fallible<RegistryFormat> {\n        match raw_format {\n            \"npm\" => Ok(RegistryFormat::Npm),\n            \"github\" => Ok(RegistryFormat::Github),\n            other => Err(ErrorKind::InvalidRegistryFormat {\n                format: String::from(other),\n            }\n            .into()),\n        }\n    }\n}\n\n/// Volta hooks related to events.\npub struct EventHooks {\n    /// The hook for publishing events, if any.\n    pub publish: Option<Publish>,\n}\n\nimpl EventHooks {\n    /// Merges this EventHooks with another, giving precedence to the current instance\n    fn merge(self, other: Self) -> Self {\n        Self {\n            publish: self.publish.or(other.publish),\n        }\n    }\n}\n\n#[cfg(test)]\npub mod tests {\n\n    use super::{tool, HookConfig, Publish, RegistryFormat};\n    use std::path::PathBuf;\n\n    fn fixture_path(fixture_dir: &str) -> PathBuf {\n        let mut cargo_manifest_dir = PathBuf::from(env!(\"CARGO_MANIFEST_DIR\"));\n        cargo_manifest_dir.push(\"fixtures\");\n        cargo_manifest_dir.push(fixture_dir);\n        cargo_manifest_dir\n    }\n\n    #[test]\n    fn test_from_str_event_url() {\n        let fixture_dir = fixture_path(\"hooks\");\n        let url_file = fixture_dir.join(\"event_url.json\");\n        let hooks = HookConfig::from_file(&url_file).unwrap().unwrap();\n\n        assert_eq!(\n            hooks.events.unwrap().publish,\n            Some(Publish::Url(\"https://google.com\".to_string()))\n        );\n    }\n\n    #[test]\n    fn test_from_str_bins() {\n        let fixture_dir = fixture_path(\"hooks\");\n        let bin_file = fixture_dir.join(\"bins.json\");\n        let hooks = HookConfig::from_file(&bin_file).unwrap().unwrap();\n        let node = hooks.node.unwrap();\n        let pnpm = hooks.pnpm.unwrap();\n        let yarn = hooks.yarn.unwrap();\n\n        assert_eq!(\n            node.distro,\n            Some(tool::DistroHook::Bin {\n                bin: \"/some/bin/for/node/distro\".to_string(),\n                base_path: fixture_dir.clone(),\n            })\n        );\n        assert_eq!(\n            node.latest,\n            Some(tool::MetadataHook::Bin {\n                bin: \"/some/bin/for/node/latest\".to_string(),\n                base_path: fixture_dir.clone(),\n            })\n        );\n        assert_eq!(\n            node.index,\n            Some(tool::MetadataHook::Bin {\n                bin: \"/some/bin/for/node/index\".to_string(),\n                base_path: fixture_dir.clone(),\n            })\n        );\n        // pnpm\n        assert_eq!(\n            pnpm.distro,\n            Some(tool::DistroHook::Bin {\n                bin: \"/bin/to/pnpm/distro\".to_string(),\n                base_path: fixture_dir.clone(),\n            })\n        );\n        assert_eq!(\n            pnpm.latest,\n            Some(tool::MetadataHook::Bin {\n                bin: \"/bin/to/pnpm/latest\".to_string(),\n                base_path: fixture_dir.clone(),\n            })\n        );\n        assert_eq!(\n            pnpm.index,\n            Some(tool::MetadataHook::Bin {\n                bin: \"/bin/to/pnpm/index\".to_string(),\n                base_path: fixture_dir.clone(),\n            })\n        );\n        // Yarn\n        assert_eq!(\n            yarn.distro,\n            Some(tool::DistroHook::Bin {\n                bin: \"/bin/to/yarn/distro\".to_string(),\n                base_path: fixture_dir.clone(),\n            })\n        );\n        assert_eq!(\n            yarn.latest,\n            Some(tool::MetadataHook::Bin {\n                bin: \"/bin/to/yarn/latest\".to_string(),\n                base_path: fixture_dir.clone(),\n            })\n        );\n        assert_eq!(\n            yarn.index,\n            Some(tool::YarnIndexHook {\n                format: RegistryFormat::Github,\n                metadata: tool::MetadataHook::Bin {\n                    bin: \"/bin/to/yarn/index\".to_string(),\n                    base_path: fixture_dir,\n                },\n            })\n        );\n        assert_eq!(\n            hooks.events.unwrap().publish,\n            Some(Publish::Bin(\"/events/bin\".to_string()))\n        );\n    }\n\n    #[test]\n    fn test_from_str_prefixes() {\n        let fixture_dir = fixture_path(\"hooks\");\n        let prefix_file = fixture_dir.join(\"prefixes.json\");\n        let hooks = HookConfig::from_file(&prefix_file).unwrap().unwrap();\n        let node = hooks.node.unwrap();\n        let pnpm = hooks.pnpm.unwrap();\n        let yarn = hooks.yarn.unwrap();\n\n        assert_eq!(\n            node.distro,\n            Some(tool::DistroHook::Prefix(\n                \"http://localhost/node/distro/\".to_string()\n            ))\n        );\n        assert_eq!(\n            node.latest,\n            Some(tool::MetadataHook::Prefix(\n                \"http://localhost/node/latest/\".to_string()\n            ))\n        );\n        assert_eq!(\n            node.index,\n            Some(tool::MetadataHook::Prefix(\n                \"http://localhost/node/index/\".to_string()\n            ))\n        );\n        // pnpm\n        assert_eq!(\n            pnpm.distro,\n            Some(tool::DistroHook::Prefix(\n                \"http://localhost/pnpm/distro/\".to_string()\n            ))\n        );\n        assert_eq!(\n            pnpm.latest,\n            Some(tool::MetadataHook::Prefix(\n                \"http://localhost/pnpm/latest/\".to_string()\n            ))\n        );\n        assert_eq!(\n            pnpm.index,\n            Some(tool::MetadataHook::Prefix(\n                \"http://localhost/pnpm/index/\".to_string()\n            ))\n        );\n        // Yarn\n        assert_eq!(\n            yarn.distro,\n            Some(tool::DistroHook::Prefix(\n                \"http://localhost/yarn/distro/\".to_string()\n            ))\n        );\n        assert_eq!(\n            yarn.latest,\n            Some(tool::MetadataHook::Prefix(\n                \"http://localhost/yarn/latest/\".to_string()\n            ))\n        );\n        assert_eq!(\n            yarn.index,\n            Some(tool::YarnIndexHook {\n                format: RegistryFormat::Github,\n                metadata: tool::MetadataHook::Prefix(\"http://localhost/yarn/index/\".to_string())\n            })\n        );\n    }\n\n    #[test]\n    fn test_from_str_templates() {\n        let fixture_dir = fixture_path(\"hooks\");\n        let template_file = fixture_dir.join(\"templates.json\");\n        let hooks = HookConfig::from_file(&template_file).unwrap().unwrap();\n        let node = hooks.node.unwrap();\n        let pnpm = hooks.pnpm.unwrap();\n        let yarn = hooks.yarn.unwrap();\n        assert_eq!(\n            node.distro,\n            Some(tool::DistroHook::Template(\n                \"http://localhost/node/distro/{{version}}/\".to_string()\n            ))\n        );\n        assert_eq!(\n            node.latest,\n            Some(tool::MetadataHook::Template(\n                \"http://localhost/node/latest/{{version}}/\".to_string()\n            ))\n        );\n        assert_eq!(\n            node.index,\n            Some(tool::MetadataHook::Template(\n                \"http://localhost/node/index/{{version}}/\".to_string()\n            ))\n        );\n        // pnpm\n        assert_eq!(\n            pnpm.distro,\n            Some(tool::DistroHook::Template(\n                \"http://localhost/pnpm/distro/{{version}}/\".to_string()\n            ))\n        );\n        assert_eq!(\n            pnpm.latest,\n            Some(tool::MetadataHook::Template(\n                \"http://localhost/pnpm/latest/{{version}}/\".to_string()\n            ))\n        );\n        assert_eq!(\n            pnpm.index,\n            Some(tool::MetadataHook::Template(\n                \"http://localhost/pnpm/index/{{version}}/\".to_string()\n            ))\n        );\n        // Yarn\n        assert_eq!(\n            yarn.distro,\n            Some(tool::DistroHook::Template(\n                \"http://localhost/yarn/distro/{{version}}/\".to_string()\n            ))\n        );\n        assert_eq!(\n            yarn.latest,\n            Some(tool::MetadataHook::Template(\n                \"http://localhost/yarn/latest/{{version}}/\".to_string()\n            ))\n        );\n        assert_eq!(\n            yarn.index,\n            Some(tool::YarnIndexHook {\n                format: RegistryFormat::Github,\n                metadata: tool::MetadataHook::Template(\n                    \"http://localhost/yarn/index/{{version}}/\".to_string()\n                )\n            })\n        );\n    }\n\n    #[test]\n    fn test_from_str_format_npm() {\n        let fixture_dir = fixture_path(\"hooks\");\n        let format_npm_file = fixture_dir.join(\"format_npm.json\");\n        let hooks = HookConfig::from_file(&format_npm_file).unwrap().unwrap();\n        let yarn = hooks.yarn.unwrap();\n        let node = hooks.node.unwrap();\n        let npm = hooks.npm.unwrap();\n        let pnpm = hooks.pnpm.unwrap();\n        assert_eq!(\n            yarn.index,\n            Some(tool::YarnIndexHook {\n                format: RegistryFormat::Npm,\n                metadata: tool::MetadataHook::Prefix(\"http://localhost/yarn/index/\".to_string())\n            })\n        );\n        // node and npm don't have format\n        assert_eq!(\n            node.index,\n            Some(tool::MetadataHook::Prefix(\n                \"http://localhost/node/index/\".to_string()\n            ))\n        );\n        assert_eq!(\n            npm.index,\n            Some(tool::MetadataHook::Prefix(\n                \"http://localhost/npm/index/\".to_string()\n            ))\n        );\n        // pnpm also doesn't have format\n        assert_eq!(\n            pnpm.index,\n            Some(tool::MetadataHook::Prefix(\n                \"http://localhost/pnpm/index/\".to_string()\n            ))\n        );\n    }\n\n    #[test]\n    fn test_from_str_format_github() {\n        let fixture_dir = fixture_path(\"hooks\");\n        let format_github_file = fixture_dir.join(\"format_github.json\");\n        let hooks = HookConfig::from_file(&format_github_file).unwrap().unwrap();\n        let yarn = hooks.yarn.unwrap();\n        let node = hooks.node.unwrap();\n        let npm = hooks.npm.unwrap();\n        let pnpm = hooks.pnpm.unwrap();\n        assert_eq!(\n            yarn.index,\n            Some(tool::YarnIndexHook {\n                format: RegistryFormat::Github,\n                metadata: tool::MetadataHook::Prefix(\"http://localhost/yarn/index/\".to_string())\n            })\n        );\n        // node and npm don't have format\n        assert_eq!(\n            node.index,\n            Some(tool::MetadataHook::Prefix(\n                \"http://localhost/node/index/\".to_string()\n            ))\n        );\n        assert_eq!(\n            npm.index,\n            Some(tool::MetadataHook::Prefix(\n                \"http://localhost/npm/index/\".to_string()\n            ))\n        );\n        // pnpm also doesn't have format\n        assert_eq!(\n            pnpm.index,\n            Some(tool::MetadataHook::Prefix(\n                \"http://localhost/pnpm/index/\".to_string()\n            ))\n        );\n    }\n\n    #[test]\n    fn test_merge() {\n        let fixture_dir = fixture_path(\"hooks\");\n        let default_hooks = HookConfig::from_file(&fixture_dir.join(\"templates.json\"))\n            .unwrap()\n            .unwrap();\n        let project_hooks_dir = fixture_path(\"hooks/project/.volta\");\n        let project_hooks_file = project_hooks_dir.join(\"hooks.json\");\n        let project_hooks = HookConfig::from_file(&project_hooks_file)\n            .expect(\"Could not read project hooks.json\")\n            .expect(\"Could not find project hooks.json\");\n        let merged_hooks = project_hooks.merge(default_hooks);\n        let node = merged_hooks.node.expect(\"No node config found\");\n        let pnpm = merged_hooks.pnpm.expect(\"No pnpm config found\");\n        let yarn = merged_hooks.yarn.expect(\"No yarn config found\");\n\n        assert_eq!(\n            node.distro,\n            Some(tool::DistroHook::Bin {\n                bin: \"/some/bin/for/node/distro\".to_string(),\n                base_path: project_hooks_dir.clone(),\n            })\n        );\n        assert_eq!(\n            node.latest,\n            Some(tool::MetadataHook::Bin {\n                bin: \"/some/bin/for/node/latest\".to_string(),\n                base_path: project_hooks_dir.clone(),\n            })\n        );\n        assert_eq!(\n            node.index,\n            Some(tool::MetadataHook::Bin {\n                bin: \"/some/bin/for/node/index\".to_string(),\n                base_path: project_hooks_dir,\n            })\n        );\n        // pnpm\n        assert_eq!(\n            pnpm.distro,\n            Some(tool::DistroHook::Template(\n                \"http://localhost/pnpm/distro/{{version}}/\".to_string()\n            ))\n        );\n        assert_eq!(\n            pnpm.latest,\n            Some(tool::MetadataHook::Template(\n                \"http://localhost/pnpm/latest/{{version}}/\".to_string()\n            ))\n        );\n        assert_eq!(\n            pnpm.index,\n            Some(tool::MetadataHook::Template(\n                \"http://localhost/pnpm/index/{{version}}/\".to_string()\n            ))\n        );\n        // Yarn\n        assert_eq!(\n            yarn.distro,\n            Some(tool::DistroHook::Template(\n                \"http://localhost/yarn/distro/{{version}}/\".to_string()\n            ))\n        );\n        assert_eq!(\n            yarn.latest,\n            Some(tool::MetadataHook::Template(\n                \"http://localhost/yarn/latest/{{version}}/\".to_string()\n            ))\n        );\n        assert_eq!(\n            yarn.index,\n            Some(tool::YarnIndexHook {\n                format: RegistryFormat::Github,\n                metadata: tool::MetadataHook::Template(\n                    \"http://localhost/yarn/index/{{version}}/\".to_string()\n                )\n            })\n        );\n        assert_eq!(\n            merged_hooks.events.expect(\"No events config found\").publish,\n            Some(Publish::Bin(\"/events/bin\".to_string()))\n        );\n    }\n\n    #[test]\n    fn test_from_paths() {\n        let project_hooks_dir = fixture_path(\"hooks/project/.volta\");\n        let project_hooks_file = project_hooks_dir.join(\"hooks.json\");\n        let default_hooks_file = fixture_path(\"hooks/templates.json\");\n\n        let merged_hooks =\n            HookConfig::from_paths([project_hooks_file, default_hooks_file]).unwrap();\n        let node = merged_hooks.node.expect(\"No node config found\");\n        let pnpm = merged_hooks.pnpm.expect(\"No pnpm config found\");\n        let yarn = merged_hooks.yarn.expect(\"No yarn config found\");\n\n        assert_eq!(\n            node.distro,\n            Some(tool::DistroHook::Bin {\n                bin: \"/some/bin/for/node/distro\".to_string(),\n                base_path: project_hooks_dir.clone(),\n            })\n        );\n        assert_eq!(\n            node.latest,\n            Some(tool::MetadataHook::Bin {\n                bin: \"/some/bin/for/node/latest\".to_string(),\n                base_path: project_hooks_dir.clone(),\n            })\n        );\n        assert_eq!(\n            node.index,\n            Some(tool::MetadataHook::Bin {\n                bin: \"/some/bin/for/node/index\".to_string(),\n                base_path: project_hooks_dir,\n            })\n        );\n        // pnpm\n        assert_eq!(\n            pnpm.distro,\n            Some(tool::DistroHook::Template(\n                \"http://localhost/pnpm/distro/{{version}}/\".to_string()\n            ))\n        );\n        assert_eq!(\n            pnpm.latest,\n            Some(tool::MetadataHook::Template(\n                \"http://localhost/pnpm/latest/{{version}}/\".to_string()\n            ))\n        );\n        assert_eq!(\n            pnpm.index,\n            Some(tool::MetadataHook::Template(\n                \"http://localhost/pnpm/index/{{version}}/\".to_string()\n            ))\n        );\n        // Yarn\n        assert_eq!(\n            yarn.distro,\n            Some(tool::DistroHook::Template(\n                \"http://localhost/yarn/distro/{{version}}/\".to_string()\n            ))\n        );\n        assert_eq!(\n            yarn.latest,\n            Some(tool::MetadataHook::Template(\n                \"http://localhost/yarn/latest/{{version}}/\".to_string()\n            ))\n        );\n        assert_eq!(\n            yarn.index,\n            Some(tool::YarnIndexHook {\n                format: RegistryFormat::Github,\n                metadata: tool::MetadataHook::Template(\n                    \"http://localhost/yarn/index/{{version}}/\".to_string()\n                )\n            })\n        );\n        assert_eq!(\n            merged_hooks.events.expect(\"No events config found\").publish,\n            Some(Publish::Bin(\"/events/bin\".to_string()))\n        );\n    }\n}\n"
  },
  {
    "path": "crates/volta-core/src/hook/serial.rs",
    "content": "use std::marker::PhantomData;\nuse std::path::Path;\n\nuse super::tool;\nuse super::RegistryFormat;\nuse crate::error::{ErrorKind, Fallible, VoltaError};\nuse crate::tool::{Node, Npm, Pnpm, Tool};\nuse serde::{Deserialize, Serialize};\n\n#[derive(Serialize, Deserialize)]\npub struct RawResolveHook {\n    prefix: Option<String>,\n    template: Option<String>,\n    bin: Option<String>,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct RawIndexHook {\n    prefix: Option<String>,\n    template: Option<String>,\n    bin: Option<String>,\n    format: Option<String>,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct RawPublishHook {\n    url: Option<String>,\n    bin: Option<String>,\n}\n\nimpl RawResolveHook {\n    fn into_hook<H, P, T, B>(self, to_prefix: P, to_template: T, to_bin: B) -> Fallible<H>\n    where\n        P: FnOnce(String) -> H,\n        T: FnOnce(String) -> H,\n        B: FnOnce(String) -> H,\n    {\n        match self {\n            RawResolveHook {\n                prefix: Some(prefix),\n                template: None,\n                bin: None,\n            } => Ok(to_prefix(prefix)),\n            RawResolveHook {\n                prefix: None,\n                template: Some(template),\n                bin: None,\n            } => Ok(to_template(template)),\n            RawResolveHook {\n                prefix: None,\n                template: None,\n                bin: Some(bin),\n            } => Ok(to_bin(bin)),\n            RawResolveHook {\n                prefix: None,\n                template: None,\n                bin: None,\n            } => Err(ErrorKind::HookNoFieldsSpecified.into()),\n            _ => Err(ErrorKind::HookMultipleFieldsSpecified.into()),\n        }\n    }\n\n    pub fn into_distro_hook(self, base_dir: &Path) -> Fallible<tool::DistroHook> {\n        self.into_hook(\n            tool::DistroHook::Prefix,\n            tool::DistroHook::Template,\n            |bin| tool::DistroHook::Bin {\n                bin,\n                base_path: base_dir.to_owned(),\n            },\n        )\n    }\n\n    pub fn into_metadata_hook(self, base_dir: &Path) -> Fallible<tool::MetadataHook> {\n        self.into_hook(\n            tool::MetadataHook::Prefix,\n            tool::MetadataHook::Template,\n            |bin| tool::MetadataHook::Bin {\n                bin,\n                base_path: base_dir.to_owned(),\n            },\n        )\n    }\n}\n\nimpl RawIndexHook {\n    pub fn into_index_hook(self, base_dir: &Path) -> Fallible<tool::YarnIndexHook> {\n        // use user-specified format, or default to Github (legacy)\n        let format = match self.format {\n            Some(format_str) => RegistryFormat::from_str(&format_str)?,\n            None => RegistryFormat::Github,\n        };\n        Ok(tool::YarnIndexHook {\n            format,\n            metadata: RawResolveHook {\n                prefix: self.prefix,\n                template: self.template,\n                bin: self.bin,\n            }\n            .into_metadata_hook(base_dir)?,\n        })\n    }\n}\n\nimpl TryFrom<RawPublishHook> for super::Publish {\n    type Error = VoltaError;\n\n    fn try_from(raw: RawPublishHook) -> Fallible<super::Publish> {\n        match raw {\n            RawPublishHook {\n                url: Some(url),\n                bin: None,\n            } => Ok(super::Publish::Url(url)),\n            RawPublishHook {\n                url: None,\n                bin: Some(bin),\n            } => Ok(super::Publish::Bin(bin)),\n            RawPublishHook {\n                url: None,\n                bin: None,\n            } => Err(ErrorKind::PublishHookNeitherUrlNorBin.into()),\n            _ => Err(ErrorKind::PublishHookBothUrlAndBin.into()),\n        }\n    }\n}\n\n#[derive(Serialize, Deserialize)]\npub struct RawHookConfig {\n    pub node: Option<RawToolHooks<Node>>,\n    pub npm: Option<RawToolHooks<Npm>>,\n    pub pnpm: Option<RawToolHooks<Pnpm>>,\n    pub yarn: Option<RawYarnHooks>,\n    pub events: Option<RawEventHooks>,\n}\n\n#[derive(Serialize, Deserialize)]\n#[serde(rename = \"events\")]\npub struct RawEventHooks {\n    pub publish: Option<RawPublishHook>,\n}\n\nimpl TryFrom<RawEventHooks> for super::EventHooks {\n    type Error = VoltaError;\n\n    fn try_from(raw: RawEventHooks) -> Fallible<super::EventHooks> {\n        let publish = raw.publish.map(|p| p.try_into()).transpose()?;\n\n        Ok(super::EventHooks { publish })\n    }\n}\n\n#[derive(Serialize, Deserialize)]\n#[serde(rename = \"tool\")]\npub struct RawToolHooks<T: Tool> {\n    pub distro: Option<RawResolveHook>,\n    pub latest: Option<RawResolveHook>,\n    pub index: Option<RawResolveHook>,\n\n    #[serde(skip)]\n    phantom: PhantomData<T>,\n}\n\n#[derive(Serialize, Deserialize)]\n#[serde(rename = \"yarn\")]\npub struct RawYarnHooks {\n    pub distro: Option<RawResolveHook>,\n    pub latest: Option<RawResolveHook>,\n    pub index: Option<RawIndexHook>,\n}\n\nimpl RawHookConfig {\n    pub fn into_hook_config(self, base_dir: &Path) -> Fallible<super::HookConfig> {\n        let node = self.node.map(|n| n.into_tool_hooks(base_dir)).transpose()?;\n        let npm = self.npm.map(|n| n.into_tool_hooks(base_dir)).transpose()?;\n        let pnpm = self.pnpm.map(|p| p.into_tool_hooks(base_dir)).transpose()?;\n        let yarn = self.yarn.map(|y| y.into_yarn_hooks(base_dir)).transpose()?;\n        let events = self.events.map(|e| e.try_into()).transpose()?;\n        Ok(super::HookConfig {\n            node,\n            npm,\n            pnpm,\n            yarn,\n            events,\n        })\n    }\n}\n\nimpl<T: Tool> RawToolHooks<T> {\n    pub fn into_tool_hooks(self, base_dir: &Path) -> Fallible<super::ToolHooks<T>> {\n        let distro = self\n            .distro\n            .map(|d| d.into_distro_hook(base_dir))\n            .transpose()?;\n        let latest = self\n            .latest\n            .map(|d| d.into_metadata_hook(base_dir))\n            .transpose()?;\n        let index = self\n            .index\n            .map(|d| d.into_metadata_hook(base_dir))\n            .transpose()?;\n\n        Ok(super::ToolHooks {\n            distro,\n            latest,\n            index,\n            phantom: PhantomData,\n        })\n    }\n}\n\nimpl RawYarnHooks {\n    pub fn into_yarn_hooks(self, base_dir: &Path) -> Fallible<super::YarnHooks> {\n        let distro = self\n            .distro\n            .map(|d| d.into_distro_hook(base_dir))\n            .transpose()?;\n        let latest = self\n            .latest\n            .map(|d| d.into_metadata_hook(base_dir))\n            .transpose()?;\n        let index = self\n            .index\n            .map(|d| d.into_index_hook(base_dir))\n            .transpose()?;\n\n        Ok(super::YarnHooks {\n            distro,\n            latest,\n            index,\n        })\n    }\n}\n"
  },
  {
    "path": "crates/volta-core/src/hook/tool.rs",
    "content": "//! Types representing Volta Tool Hooks.\n\nuse std::ffi::OsString;\nuse std::path::{Path, PathBuf};\nuse std::process::Stdio;\n\nuse crate::command::create_command;\nuse crate::error::{Context, ErrorKind, Fallible};\nuse crate::hook::RegistryFormat;\nuse crate::tool::{NODE_DISTRO_ARCH, NODE_DISTRO_OS};\nuse cmdline_words_parser::parse_posix;\nuse dunce::canonicalize;\nuse log::debug;\nuse node_semver::Version;\nuse once_cell::sync::Lazy;\n\nconst ARCH_TEMPLATE: &str = \"{{arch}}\";\nconst OS_TEMPLATE: &str = \"{{os}}\";\nconst VERSION_TEMPLATE: &str = \"{{version}}\";\nconst EXTENSION_TEMPLATE: &str = \"{{ext}}\";\nconst FILENAME_TEMPLATE: &str = \"{{filename}}\";\n\nstatic REL_PATH: Lazy<String> = Lazy::new(|| format!(\".{}\", std::path::MAIN_SEPARATOR));\nstatic REL_PATH_PARENT: Lazy<String> = Lazy::new(|| format!(\"..{}\", std::path::MAIN_SEPARATOR));\n\n/// A hook for resolving the distro URL for a given tool version\n#[derive(PartialEq, Eq, Debug)]\npub enum DistroHook {\n    Prefix(String),\n    Template(String),\n    Bin { bin: String, base_path: PathBuf },\n}\n\nimpl DistroHook {\n    /// Performs resolution of the distro URL based on the given version and file name\n    pub fn resolve(&self, version: &Version, filename: &str) -> Fallible<String> {\n        let extension = calculate_extension(filename).unwrap_or(\"\");\n\n        match &self {\n            DistroHook::Prefix(prefix) => Ok(format!(\"{}{}\", prefix, filename)),\n            DistroHook::Template(template) => Ok(template\n                .replace(ARCH_TEMPLATE, NODE_DISTRO_ARCH)\n                .replace(OS_TEMPLATE, NODE_DISTRO_OS)\n                .replace(EXTENSION_TEMPLATE, extension)\n                .replace(FILENAME_TEMPLATE, filename)\n                .replace(VERSION_TEMPLATE, &version.to_string())),\n            DistroHook::Bin { bin, base_path } => {\n                execute_binary(bin, base_path, Some(version.to_string()))\n            }\n        }\n    }\n}\n\n/// Use the expected filename to determine the extension for this hook\n///\n/// This will include the multi-part `tar.gz` extension if it is present, otherwise it will use\n/// the standard extension.\nfn calculate_extension(filename: &str) -> Option<&str> {\n    let mut parts = filename.rsplit('.');\n    match (parts.next(), parts.next(), parts.next()) {\n        (Some(ext), Some(\"tar\"), Some(_)) => {\n            // .tar.gz style extension, return both parts\n            //                          tar  .   gz\n            let index = filename.len() - 3 - 1 - ext.len();\n            filename.get(index..)\n        }\n        (Some(_), Some(\"\"), None) => {\n            // Dotfile, e.g. `.npmrc`, where the `.` character is at the beginning - No extension\n            None\n        }\n        (Some(ext), Some(_), _) => {\n            // Standard File Extension\n            Some(ext)\n        }\n        _ => None,\n    }\n}\n\n/// A hook for resolving the URL for metadata about a tool\n#[derive(PartialEq, Eq, Debug)]\npub enum MetadataHook {\n    Prefix(String),\n    Template(String),\n    Bin { bin: String, base_path: PathBuf },\n}\n\nimpl MetadataHook {\n    /// Performs resolution of the metadata URL based on the given default file name\n    pub fn resolve(&self, filename: &str) -> Fallible<String> {\n        match &self {\n            MetadataHook::Prefix(prefix) => Ok(format!(\"{}{}\", prefix, filename)),\n            MetadataHook::Template(template) => Ok(template\n                .replace(ARCH_TEMPLATE, NODE_DISTRO_ARCH)\n                .replace(OS_TEMPLATE, NODE_DISTRO_OS)\n                .replace(FILENAME_TEMPLATE, filename)),\n            MetadataHook::Bin { bin, base_path } => execute_binary(bin, base_path, None),\n        }\n    }\n}\n\n/// A hook for resolving the URL for the Yarn index\n#[derive(PartialEq, Eq, Debug)]\npub struct YarnIndexHook {\n    pub format: RegistryFormat,\n    pub metadata: MetadataHook,\n}\n\nimpl YarnIndexHook {\n    /// Performs resolution of the metadata URL based on the given default file name\n    pub fn resolve(&self, filename: &str) -> Fallible<String> {\n        match &self.metadata {\n            MetadataHook::Prefix(prefix) => Ok(format!(\"{}{}\", prefix, filename)),\n            MetadataHook::Template(template) => Ok(template\n                .replace(ARCH_TEMPLATE, NODE_DISTRO_ARCH)\n                .replace(OS_TEMPLATE, NODE_DISTRO_OS)\n                .replace(FILENAME_TEMPLATE, filename)),\n            MetadataHook::Bin { bin, base_path } => execute_binary(bin, base_path, None),\n        }\n    }\n}\n\n/// Execute a shell command and return the trimmed stdout from that command\nfn execute_binary(bin: &str, base_path: &Path, extra_arg: Option<String>) -> Fallible<String> {\n    let mut trimmed = bin.trim().to_string();\n    let mut words = parse_posix(&mut trimmed);\n    let cmd = match words.next() {\n        Some(word) => {\n            // Treat any path that starts with a './' or '../' as a relative path (using OS separator)\n            if word.starts_with(REL_PATH.as_str()) || word.starts_with(REL_PATH_PARENT.as_str()) {\n                canonicalize(base_path.join(word)).with_context(|| ErrorKind::HookPathError {\n                    command: String::from(word),\n                })?\n            } else {\n                PathBuf::from(word)\n            }\n        }\n        None => {\n            return Err(ErrorKind::InvalidHookCommand {\n                command: String::from(bin.trim()),\n            }\n            .into())\n        }\n    };\n\n    let mut args: Vec<OsString> = words.map(OsString::from).collect();\n    if let Some(arg) = extra_arg {\n        args.push(OsString::from(arg));\n    }\n\n    let mut command = create_command(cmd);\n    command\n        .args(&args)\n        .current_dir(base_path)\n        .stdin(Stdio::null())\n        .stdout(Stdio::piped())\n        .stderr(Stdio::inherit());\n\n    debug!(\"Running hook command: {:?}\", command);\n    let output = command\n        .output()\n        .with_context(|| ErrorKind::ExecuteHookError {\n            command: String::from(bin.trim()),\n        })?;\n\n    if !output.status.success() {\n        return Err(ErrorKind::HookCommandFailed {\n            command: bin.trim().into(),\n        }\n        .into());\n    }\n\n    let url = String::from_utf8(output.stdout).with_context(|| ErrorKind::InvalidHookOutput {\n        command: String::from(bin.trim()),\n    })?;\n\n    Ok(url.trim().to_string())\n}\n\n#[cfg(test)]\npub mod tests {\n    use super::{calculate_extension, DistroHook, MetadataHook};\n    use crate::tool::{NODE_DISTRO_ARCH, NODE_DISTRO_OS};\n    use node_semver::Version;\n\n    #[test]\n    fn test_distro_prefix_resolve() {\n        let prefix = \"http://localhost/node/distro/\";\n        let filename = \"node.tar.gz\";\n        let hook = DistroHook::Prefix(prefix.to_string());\n        let version = Version::parse(\"1.0.0\").unwrap();\n\n        assert_eq!(\n            hook.resolve(&version, filename)\n                .expect(\"Could not resolve URL\"),\n            format!(\"{}{}\", prefix, filename)\n        );\n    }\n\n    #[test]\n    fn test_distro_template_resolve() {\n        let hook = DistroHook::Template(\n            \"http://localhost/node/{{os}}/{{arch}}/{{version}}/{{ext}}/{{filename}}\".to_string(),\n        );\n        let version = Version::parse(\"1.0.0\").unwrap();\n\n        // tar.gz format has extra handling, to support a multi-part extension\n        let expected = format!(\n            \"http://localhost/node/{}/{}/{}/tar.gz/node-v1.0.0.tar.gz\",\n            NODE_DISTRO_OS, NODE_DISTRO_ARCH, version\n        );\n        assert_eq!(\n            hook.resolve(&version, \"node-v1.0.0.tar.gz\")\n                .expect(\"Could not resolve URL\"),\n            expected\n        );\n\n        // zip is a standard extension\n        let expected = format!(\n            \"http://localhost/node/{}/{}/{}/zip/node-v1.0.0.zip\",\n            NODE_DISTRO_OS, NODE_DISTRO_ARCH, version\n        );\n        assert_eq!(\n            hook.resolve(&version, \"node-v1.0.0.zip\")\n                .expect(\"Could not resolve URL\"),\n            expected\n        );\n    }\n\n    #[test]\n    fn test_metadata_prefix_resolve() {\n        let prefix = \"http://localhost/node/index/\";\n        let filename = \"index.json\";\n        let hook = MetadataHook::Prefix(prefix.to_string());\n\n        assert_eq!(\n            hook.resolve(filename).expect(\"Could not resolve URL\"),\n            format!(\"{}{}\", prefix, filename)\n        );\n    }\n\n    #[test]\n    fn test_metadata_template_resolve() {\n        let hook = MetadataHook::Template(\n            \"http://localhost/node/{{os}}/{{arch}}/{{filename}}\".to_string(),\n        );\n        let expected = format!(\n            \"http://localhost/node/{}/{}/index.json\",\n            NODE_DISTRO_OS, NODE_DISTRO_ARCH\n        );\n\n        assert_eq!(\n            hook.resolve(\"index.json\").expect(\"Could not resolve URL\"),\n            expected\n        );\n    }\n\n    #[test]\n    fn test_calculate_extension() {\n        // Handles .tar.* files\n        assert_eq!(calculate_extension(\"file.tar.gz\"), Some(\"tar.gz\"));\n        assert_eq!(calculate_extension(\"file.tar.xz\"), Some(\"tar.xz\"));\n        assert_eq!(calculate_extension(\"file.tar.xyz\"), Some(\"tar.xyz\"));\n\n        // Handles dotfiles\n        assert_eq!(calculate_extension(\".filerc\"), None);\n\n        // Handles standard extensions\n        assert_eq!(calculate_extension(\"tar.gz\"), Some(\"gz\"));\n        assert_eq!(calculate_extension(\"file.zip\"), Some(\"zip\"));\n\n        // Handles files with no extension at all\n        assert_eq!(calculate_extension(\"bare_file\"), None);\n    }\n}\n"
  },
  {
    "path": "crates/volta-core/src/inventory.rs",
    "content": "//! Provides types for working with Volta's _inventory_, the local repository\n//! of available tool versions.\n\nuse std::collections::BTreeSet;\nuse std::ffi::OsStr;\nuse std::path::Path;\n\nuse crate::error::{Context, ErrorKind, Fallible};\nuse crate::fs::read_dir_eager;\nuse crate::layout::volta_home;\nuse crate::tool::PackageConfig;\nuse crate::version::parse_version;\nuse log::debug;\nuse node_semver::Version;\nuse walkdir::WalkDir;\n\n/// Checks if a given Node version image is available on the local machine\npub fn node_available(version: &Version) -> Fallible<bool> {\n    volta_home().map(|home| {\n        home.node_image_root_dir()\n            .join(version.to_string())\n            .exists()\n    })\n}\n\n/// Collects a set of all Node versions fetched on the local machine\npub fn node_versions() -> Fallible<BTreeSet<Version>> {\n    volta_home().and_then(|home| read_versions(home.node_image_root_dir()))\n}\n\n/// Checks if a given npm version image is available on the local machine\npub fn npm_available(version: &Version) -> Fallible<bool> {\n    volta_home().map(|home| home.npm_image_dir(&version.to_string()).exists())\n}\n\n/// Collects a set of all npm versions fetched on the local machine\npub fn npm_versions() -> Fallible<BTreeSet<Version>> {\n    volta_home().and_then(|home| read_versions(home.npm_image_root_dir()))\n}\n\n/// Checks if a given pnpm version image is available on the local machine\npub fn pnpm_available(version: &Version) -> Fallible<bool> {\n    volta_home().map(|home| home.pnpm_image_dir(&version.to_string()).exists())\n}\n\n/// Collects a set of all pnpm versions fetched on the local machine\npub fn pnpm_versions() -> Fallible<BTreeSet<Version>> {\n    volta_home().and_then(|home| read_versions(home.pnpm_image_root_dir()))\n}\n\n/// Checks if a given Yarn version image is available on the local machine\npub fn yarn_available(version: &Version) -> Fallible<bool> {\n    volta_home().map(|home| home.yarn_image_dir(&version.to_string()).exists())\n}\n\n/// Collects a set of all Yarn versions fetched on the local machine\npub fn yarn_versions() -> Fallible<BTreeSet<Version>> {\n    volta_home().and_then(|home| read_versions(home.yarn_image_root_dir()))\n}\n\n/// Collects a set of all Package Configs on the local machine\npub fn package_configs() -> Fallible<BTreeSet<PackageConfig>> {\n    let package_dir = volta_home()?.default_package_dir();\n\n    WalkDir::new(package_dir)\n        .max_depth(2)\n        .into_iter()\n        // Ignore any items which didn't resolve as `DirEntry` correctly.\n        // There is no point trying to do anything with those, and no error\n        // we can report to the user in any case. Log the failure in the\n        // debug output, though\n        .filter_map(|entry| match entry {\n            Ok(dir_entry) => {\n                // Ignore directory entries and any files that don't have a .json extension.\n                // This will prevent us from trying to parse OS-generated files as package\n                // configs (e.g. `.DS_Store` on macOS)\n                let extension = dir_entry.path().extension().and_then(OsStr::to_str);\n                match (dir_entry.file_type().is_file(), extension) {\n                    (true, Some(ext)) if ext.eq_ignore_ascii_case(\"json\") => {\n                        Some(dir_entry.into_path())\n                    }\n                    _ => None,\n                }\n            }\n            Err(e) => {\n                debug!(\"{}\", e);\n                None\n            }\n        })\n        .map(PackageConfig::from_file)\n        .collect()\n}\n\n/// Reads the contents of a directory and returns the set of all versions found\n/// in the directory's listing by parsing the directory names as semantic versions\nfn read_versions(dir: &Path) -> Fallible<BTreeSet<Version>> {\n    let contents = read_dir_eager(dir).with_context(|| ErrorKind::ReadDirError {\n        dir: dir.to_owned(),\n    })?;\n\n    Ok(contents\n        .filter(|(_, metadata)| metadata.is_dir())\n        .filter_map(|(entry, _)| parse_version(entry.file_name().to_string_lossy()).ok())\n        .collect())\n}\n"
  },
  {
    "path": "crates/volta-core/src/layout/mod.rs",
    "content": "use std::env;\nuse std::path::PathBuf;\n\nuse crate::error::{Context, ErrorKind, Fallible};\nuse cfg_if::cfg_if;\nuse dunce::canonicalize;\nuse once_cell::sync::OnceCell;\nuse volta_layout::v4::{VoltaHome, VoltaInstall};\n\ncfg_if! {\n    if #[cfg(unix)] {\n        mod unix;\n        pub use unix::*;\n    } else if #[cfg(windows)] {\n        mod windows;\n        pub use windows::*;\n    }\n}\n\nstatic VOLTA_HOME: OnceCell<VoltaHome> = OnceCell::new();\nstatic VOLTA_INSTALL: OnceCell<VoltaInstall> = OnceCell::new();\n\npub fn volta_home<'a>() -> Fallible<&'a VoltaHome> {\n    VOLTA_HOME.get_or_try_init(|| {\n        let home_dir = match env::var_os(\"VOLTA_HOME\") {\n            Some(home) => PathBuf::from(home),\n            None => default_home_dir()?,\n        };\n\n        Ok(VoltaHome::new(home_dir))\n    })\n}\n\npub fn volta_install<'a>() -> Fallible<&'a VoltaInstall> {\n    VOLTA_INSTALL.get_or_try_init(|| {\n        let install_dir = match env::var_os(\"VOLTA_INSTALL_DIR\") {\n            Some(install) => PathBuf::from(install),\n            None => default_install_dir()?,\n        };\n\n        Ok(VoltaInstall::new(install_dir))\n    })\n}\n\n/// Determine the binary install directory from the currently running executable\n///\n/// The volta-shim and volta binaries will be installed in the same location, so we can use the\n/// currently running executable to find the binary install directory. Note that we need to\n/// canonicalize the path we get from current_exe to make sure we resolve symlinks and find the\n/// actual binary files\nfn default_install_dir() -> Fallible<PathBuf> {\n    env::current_exe()\n        .and_then(canonicalize)\n        .map(|mut path| {\n            path.pop(); // Remove the executable name from the path\n            path\n        })\n        .with_context(|| ErrorKind::NoInstallDir)\n}\n"
  },
  {
    "path": "crates/volta-core/src/layout/unix.rs",
    "content": "use std::path::PathBuf;\n\nuse super::volta_home;\nuse crate::error::{ErrorKind, Fallible};\n\npub(super) fn default_home_dir() -> Fallible<PathBuf> {\n    let mut home = dirs::home_dir().ok_or(ErrorKind::NoHomeEnvironmentVar)?;\n    home.push(\".volta\");\n    Ok(home)\n}\n\npub fn env_paths() -> Fallible<Vec<PathBuf>> {\n    let home = volta_home()?;\n    Ok(vec![home.shim_dir().to_owned()])\n}\n"
  },
  {
    "path": "crates/volta-core/src/layout/windows.rs",
    "content": "use std::path::PathBuf;\n\nuse super::{volta_home, volta_install};\nuse crate::error::{ErrorKind, Fallible};\n\npub(super) fn default_home_dir() -> Fallible<PathBuf> {\n    let mut home = dirs::data_local_dir().ok_or(ErrorKind::NoLocalDataDir)?;\n    home.push(\"Volta\");\n    Ok(home)\n}\n\npub fn env_paths() -> Fallible<Vec<PathBuf>> {\n    let home = volta_home()?;\n    let install = volta_install()?;\n\n    Ok(vec![home.shim_dir().to_owned(), install.root().to_owned()])\n}\n"
  },
  {
    "path": "crates/volta-core/src/lib.rs",
    "content": "//! The main implementation crate for the core of Volta.\n\nmod command;\npub mod error;\npub mod event;\npub mod fs;\nmod hook;\npub mod inventory;\npub mod layout;\npub mod log;\npub mod monitor;\npub mod platform;\npub mod project;\npub mod run;\npub mod session;\npub mod shim;\npub mod signal;\npub mod style;\npub mod sync;\npub mod tool;\npub mod toolchain;\npub mod version;\n\nconst VOLTA_FEATURE_PNPM: &str = \"VOLTA_FEATURE_PNPM\";\n"
  },
  {
    "path": "crates/volta-core/src/log.rs",
    "content": "//! This module provides a custom Logger implementation for use with the `log` crate\nuse console::style;\nuse log::{trace, Level, LevelFilter, Log, Metadata, Record, SetLoggerError};\nuse std::env;\nuse std::fmt::Display;\nuse std::io::IsTerminal;\nuse textwrap::{fill, Options, WordSplitter};\n\nuse crate::style::text_width;\n\nconst ERROR_PREFIX: &str = \"error:\";\nconst WARNING_PREFIX: &str = \"warning:\";\nconst SHIM_ERROR_PREFIX: &str = \"Volta error:\";\nconst SHIM_WARNING_PREFIX: &str = \"Volta warning:\";\nconst MIGRATION_ERROR_PREFIX: &str = \"Volta update error:\";\nconst MIGRATION_WARNING_PREFIX: &str = \"Volta update warning:\";\nconst VOLTA_LOGLEVEL: &str = \"VOLTA_LOGLEVEL\";\nconst ALLOWED_PREFIXES: [&str; 5] = [\n    \"volta\",\n    \"archive\",\n    \"fs-utils\",\n    \"progress-read\",\n    \"validate-npm-package-name\",\n];\nconst WRAP_INDENT: &str = \"    \";\n\n/// Represents the context from which the logger was created\npub enum LogContext {\n    /// Log messages from the `volta` executable\n    Volta,\n\n    /// Log messages from one of the shims\n    Shim,\n\n    /// Log messages from the migration\n    Migration,\n}\n\n/// Represents the level of verbosity that was requested by the user\n#[derive(Debug, Copy, Clone)]\npub enum LogVerbosity {\n    Quiet,\n    Default,\n    Verbose,\n    VeryVerbose,\n}\n\npub struct Logger {\n    context: LogContext,\n    level: LevelFilter,\n}\n\nimpl Log for Logger {\n    fn enabled(&self, metadata: &Metadata) -> bool {\n        metadata.level() <= self.level\n    }\n\n    fn log(&self, record: &Record) {\n        let level_allowed = self.enabled(record.metadata());\n\n        let is_valid_target = ALLOWED_PREFIXES\n            .iter()\n            .any(|prefix| record.target().starts_with(prefix));\n\n        if level_allowed && is_valid_target {\n            match record.level() {\n                Level::Error => self.log_error(record.args()),\n                Level::Warn => self.log_warning(record.args()),\n                // all info-level messages go to stdout\n                Level::Info => println!(\"{}\", record.args()),\n                // all debug- and trace-level messages go to stderr\n                Level::Debug => eprintln!(\"[verbose] {}\", record.args()),\n                Level::Trace => eprintln!(\"[trace] {}\", record.args()),\n            }\n        }\n    }\n\n    fn flush(&self) {}\n}\n\nimpl Logger {\n    /// Initialize the global logger with a Logger instance\n    /// Will use the requested level of Verbosity\n    /// If set to Default, will use the environment to determine the level of verbosity\n    pub fn init(context: LogContext, verbosity: LogVerbosity) -> Result<(), SetLoggerError> {\n        let logger = Logger::new(context, verbosity);\n        log::set_max_level(logger.level);\n        log::set_boxed_logger(Box::new(logger))?;\n        Ok(())\n    }\n\n    fn new(context: LogContext, verbosity: LogVerbosity) -> Self {\n        let level = match verbosity {\n            LogVerbosity::Quiet => LevelFilter::Error,\n            LogVerbosity::Default => level_from_env(),\n            LogVerbosity::Verbose => LevelFilter::Debug,\n            LogVerbosity::VeryVerbose => LevelFilter::Trace,\n        };\n\n        Logger { context, level }\n    }\n\n    fn log_error<D>(&self, message: &D)\n    where\n        D: Display,\n    {\n        let prefix = match &self.context {\n            LogContext::Volta => ERROR_PREFIX,\n            LogContext::Shim => SHIM_ERROR_PREFIX,\n            LogContext::Migration => MIGRATION_ERROR_PREFIX,\n        };\n\n        eprintln!(\"{} {}\", style(prefix).red().bold(), message);\n    }\n\n    fn log_warning<D>(&self, message: &D)\n    where\n        D: Display,\n    {\n        let prefix = match &self.context {\n            LogContext::Volta => WARNING_PREFIX,\n            LogContext::Shim => SHIM_WARNING_PREFIX,\n            LogContext::Migration => MIGRATION_WARNING_PREFIX,\n        };\n\n        eprintln!(\n            \"{} {}\",\n            style(prefix).yellow().bold(),\n            wrap_content(prefix, message)\n        );\n    }\n}\n\n/// Wraps the supplied content to the terminal width, if we are in a terminal.\n/// If not, returns the content as a String\n///\n/// Note: Uses the supplied prefix to calculate the terminal width, but then removes\n/// it so that it can be styled (style characters are counted against the wrapped width)\nfn wrap_content<D>(prefix: &str, content: &D) -> String\nwhere\n    D: Display,\n{\n    match text_width() {\n        Some(width) => {\n            let options = Options::new(width)\n                .word_splitter(WordSplitter::NoHyphenation)\n                .subsequent_indent(WRAP_INDENT)\n                .break_words(false);\n\n            fill(&format!(\"{} {}\", prefix, content), options).replace(prefix, \"\")\n        }\n        None => format!(\" {}\", content),\n    }\n}\n\n/// Determines the correct logging level based on the environment\n/// If VOLTA_LOGLEVEL is set to a valid level, we use that\n/// If not, we check the current stdout to determine whether it is a TTY or not\n///     If it is a TTY, we use Info\n///     If it is NOT a TTY, we use Error as we don't want to show warnings when running as a script\nfn level_from_env() -> LevelFilter {\n    env::var(VOLTA_LOGLEVEL)\n        .ok()\n        .and_then(|level| level.to_uppercase().parse().ok())\n        .unwrap_or_else(|| {\n            if std::io::stdout().is_terminal() {\n                trace!(\"using fallback log level (info)\");\n                LevelFilter::Info\n            } else {\n                LevelFilter::Error\n            }\n        })\n}\n\n#[cfg(test)]\nmod tests {}\n"
  },
  {
    "path": "crates/volta-core/src/monitor.rs",
    "content": "use std::env;\nuse std::io::Write;\nuse std::path::PathBuf;\nuse std::process::{Child, Stdio};\n\nuse log::debug;\nuse tempfile::NamedTempFile;\n\nuse crate::command::create_command;\nuse crate::event::Event;\n\n/// Send event to the spawned command process\n// if hook command is not configured, this is not called\npub fn send_events(command: &str, events: &[Event]) {\n    match serde_json::to_string_pretty(&events) {\n        Ok(events_json) => {\n            let tempfile_path = env::var_os(\"VOLTA_WRITE_EVENTS_FILE\")\n                .and_then(|_| write_events_file(events_json.clone()));\n            if let Some(ref mut child_process) = spawn_process(command, tempfile_path) {\n                if let Some(ref mut p_stdin) = child_process.stdin.as_mut() {\n                    if let Err(error) = writeln!(p_stdin, \"{}\", events_json) {\n                        debug!(\"Could not write events to executable stdin: {:?}\", error);\n                    }\n                }\n            }\n        }\n        Err(error) => {\n            debug!(\"Could not serialize events data to JSON: {:?}\", error);\n        }\n    }\n}\n\n// Write the events JSON to a file in the temporary directory\nfn write_events_file(events_json: String) -> Option<PathBuf> {\n    match NamedTempFile::new() {\n        Ok(mut events_file) => {\n            match events_file.write_all(events_json.as_bytes()) {\n                Ok(()) => {\n                    let path = events_file.into_temp_path();\n                    // if it's not persisted, the temp file will be automatically deleted\n                    // (and the executable won't be able to read it)\n                    match path.keep() {\n                        Ok(tempfile_path) => Some(tempfile_path),\n                        Err(error) => {\n                            debug!(\"Failed to persist temp file for events data: {:?}\", error);\n                            None\n                        }\n                    }\n                }\n                Err(error) => {\n                    debug!(\"Failed to write events to the temp file: {:?}\", error);\n                    None\n                }\n            }\n        }\n        Err(error) => {\n            debug!(\"Failed to create a temp file for events data: {:?}\", error);\n            None\n        }\n    }\n}\n\n// Spawn a child process to receive the events data, setting the path to the events file as an env var\nfn spawn_process(command: &str, tempfile_path: Option<PathBuf>) -> Option<Child> {\n    command.split(' ').take(1).next().and_then(|executable| {\n        let mut child = create_command(executable);\n        child.args(command.split(' ').skip(1));\n        child.stdin(Stdio::piped());\n        if let Some(events_file) = tempfile_path {\n            child.env(\"EVENTS_FILE\", events_file);\n        }\n\n        #[cfg(not(debug_assertions))]\n        // Hide stdout and stderr of spawned process in release mode\n        child.stdout(Stdio::null()).stderr(Stdio::null());\n\n        match child.spawn() {\n            Err(err) => {\n                debug!(\"Unable to run executable command: '{}'\\n{}\", command, err);\n                None\n            }\n            Ok(c) => Some(c),\n        }\n    })\n}\n"
  },
  {
    "path": "crates/volta-core/src/platform/image.rs",
    "content": "use std::ffi::OsString;\nuse std::path::PathBuf;\n\nuse super::{build_path_error, Sourced};\nuse crate::error::{Context, Fallible};\nuse crate::layout::volta_home;\nuse crate::tool::load_default_npm_version;\nuse node_semver::Version;\n\n/// A platform image.\npub struct Image {\n    /// The pinned version of Node.\n    pub node: Sourced<Version>,\n    /// The custom version of npm, if any. `None` represents using the npm that is bundled with Node\n    pub npm: Option<Sourced<Version>>,\n    /// The pinned version of pnpm, if any.\n    pub pnpm: Option<Sourced<Version>>,\n    /// The pinned version of Yarn, if any.\n    pub yarn: Option<Sourced<Version>>,\n}\n\nimpl Image {\n    fn bins(&self) -> Fallible<Vec<PathBuf>> {\n        let home = volta_home()?;\n        let mut bins = Vec::with_capacity(3);\n\n        if let Some(npm) = &self.npm {\n            let npm_str = npm.value.to_string();\n            bins.push(home.npm_image_bin_dir(&npm_str));\n        }\n\n        if let Some(pnpm) = &self.pnpm {\n            let pnpm_str = pnpm.value.to_string();\n            bins.push(home.pnpm_image_bin_dir(&pnpm_str));\n        }\n\n        if let Some(yarn) = &self.yarn {\n            let yarn_str = yarn.value.to_string();\n            bins.push(home.yarn_image_bin_dir(&yarn_str));\n        }\n\n        // Add Node path to the bins last, so that any custom version of npm will be earlier in the PATH\n        let node_str = self.node.value.to_string();\n        bins.push(home.node_image_bin_dir(&node_str));\n        Ok(bins)\n    }\n\n    /// Produces a modified version of the current `PATH` environment variable that\n    /// will find toolchain executables (Node, npm, pnpm, Yarn) in the installation directories\n    /// for the given versions instead of in the Volta shim directory.\n    pub fn path(&self) -> Fallible<OsString> {\n        let old_path = envoy::path().unwrap_or_else(|| envoy::Var::from(\"\"));\n\n        old_path\n            .split()\n            .prefix(self.bins()?)\n            .join()\n            .with_context(build_path_error)\n    }\n\n    /// Determines the sourced version of npm that will be available, resolving the version bundled with Node, if needed\n    pub fn resolve_npm(&self) -> Fallible<Sourced<Version>> {\n        match &self.npm {\n            Some(npm) => Ok(npm.clone()),\n            None => load_default_npm_version(&self.node.value).map(|npm| Sourced {\n                value: npm,\n                source: self.node.source,\n            }),\n        }\n    }\n}\n"
  },
  {
    "path": "crates/volta-core/src/platform/mod.rs",
    "content": "use std::env;\nuse std::fmt;\n\nuse crate::error::{ErrorKind, Fallible};\nuse crate::session::Session;\nuse crate::tool::{Node, Npm, Pnpm, Yarn};\nuse crate::VOLTA_FEATURE_PNPM;\nuse node_semver::Version;\n\nmod image;\nmod system;\n// Note: The tests get their own module because we need them to run as a single unit to prevent\n// clobbering environment variable changes\n#[cfg(test)]\nmod tests;\n\npub use image::Image;\npub use system::System;\n\n/// The source with which a version is associated\n#[derive(Clone, Copy)]\n#[cfg_attr(test, derive(Eq, PartialEq, Debug))]\npub enum Source {\n    /// Represents a version from the user default platform\n    Default,\n\n    /// Represents a version from a project manifest\n    Project,\n\n    /// Represents a version from a pinned Binary platform\n    Binary,\n\n    /// Represents a version from the command line (via `volta run`)\n    CommandLine,\n}\n\nimpl fmt::Display for Source {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match self {\n            Source::Default => write!(f, \"default\"),\n            Source::Project => write!(f, \"project\"),\n            Source::Binary => write!(f, \"binary\"),\n            Source::CommandLine => write!(f, \"command-line\"),\n        }\n    }\n}\n\npub struct Sourced<T> {\n    pub value: T,\n    pub source: Source,\n}\n\nimpl<T> Sourced<T> {\n    pub fn with_default(value: T) -> Self {\n        Sourced {\n            value,\n            source: Source::Default,\n        }\n    }\n\n    pub fn with_project(value: T) -> Self {\n        Sourced {\n            value,\n            source: Source::Project,\n        }\n    }\n\n    pub fn with_binary(value: T) -> Self {\n        Sourced {\n            value,\n            source: Source::Binary,\n        }\n    }\n\n    pub fn with_command_line(value: T) -> Self {\n        Sourced {\n            value,\n            source: Source::CommandLine,\n        }\n    }\n}\n\nimpl<T> Sourced<T> {\n    pub fn as_ref(&self) -> Sourced<&T> {\n        Sourced {\n            value: &self.value,\n            source: self.source,\n        }\n    }\n}\n\nimpl<T> Sourced<&T>\nwhere\n    T: Clone,\n{\n    pub fn cloned(self) -> Sourced<T> {\n        Sourced {\n            value: self.value.clone(),\n            source: self.source,\n        }\n    }\n}\n\nimpl<T> Clone for Sourced<T>\nwhere\n    T: Clone,\n{\n    fn clone(&self) -> Sourced<T> {\n        Sourced {\n            value: self.value.clone(),\n            source: self.source,\n        }\n    }\n}\n\n/// Represents 3 possible states: Having a value, not having a value, and inheriting a value\n#[cfg_attr(test, derive(Eq, PartialEq, Debug))]\n#[derive(Clone, Default)]\npub enum InheritOption<T> {\n    Some(T),\n    None,\n    #[default]\n    Inherit,\n}\n\nimpl<T> InheritOption<T> {\n    /// Applies a function to the contained value (if any)\n    pub fn map<U, F>(self, f: F) -> InheritOption<U>\n    where\n        F: FnOnce(T) -> U,\n    {\n        match self {\n            InheritOption::Some(value) => InheritOption::Some(f(value)),\n            InheritOption::None => InheritOption::None,\n            InheritOption::Inherit => InheritOption::Inherit,\n        }\n    }\n\n    /// Converts the `InheritOption` into a regular `Option` by inheriting from the provided value if needed\n    pub fn inherit(self, other: Option<T>) -> Option<T> {\n        match self {\n            InheritOption::Some(value) => Some(value),\n            InheritOption::None => None,\n            InheritOption::Inherit => other,\n        }\n    }\n}\n\nimpl<T> From<InheritOption<T>> for Option<T> {\n    fn from(base: InheritOption<T>) -> Option<T> {\n        base.inherit(None)\n    }\n}\n\n#[derive(Clone, PartialOrd, Ord, PartialEq, Eq)]\n#[cfg_attr(test, derive(Debug))]\n/// Represents the specification of a single Platform, regardless of the source\npub struct PlatformSpec {\n    pub node: Version,\n    pub npm: Option<Version>,\n    pub pnpm: Option<Version>,\n    pub yarn: Option<Version>,\n}\n\nimpl PlatformSpec {\n    /// Convert this PlatformSpec into a Platform with all sources set to `Default`\n    pub fn as_default(&self) -> Platform {\n        Platform {\n            node: Sourced::with_default(self.node.clone()),\n            npm: self.npm.clone().map(Sourced::with_default),\n            pnpm: self.pnpm.clone().map(Sourced::with_default),\n            yarn: self.yarn.clone().map(Sourced::with_default),\n        }\n    }\n\n    /// Convert this PlatformSpec into a Platform with all sources set to `Project`\n    pub fn as_project(&self) -> Platform {\n        Platform {\n            node: Sourced::with_project(self.node.clone()),\n            npm: self.npm.clone().map(Sourced::with_project),\n            pnpm: self.pnpm.clone().map(Sourced::with_project),\n            yarn: self.yarn.clone().map(Sourced::with_project),\n        }\n    }\n\n    /// Convert this PlatformSpec into a Platform with all sources set to `Binary`\n    pub fn as_binary(&self) -> Platform {\n        Platform {\n            node: Sourced::with_binary(self.node.clone()),\n            npm: self.npm.clone().map(Sourced::with_binary),\n            pnpm: self.pnpm.clone().map(Sourced::with_binary),\n            yarn: self.yarn.clone().map(Sourced::with_binary),\n        }\n    }\n}\n\n/// Represents a (maybe) platform with values from the command line\n#[derive(Clone)]\npub struct CliPlatform {\n    pub node: Option<Version>,\n    pub npm: InheritOption<Version>,\n    pub pnpm: InheritOption<Version>,\n    pub yarn: InheritOption<Version>,\n}\n\nimpl CliPlatform {\n    /// Merges the `CliPlatform` with a `Platform`, inheriting from the base where needed\n    pub fn merge(self, base: Platform) -> Platform {\n        Platform {\n            node: self.node.map_or(base.node, Sourced::with_command_line),\n            npm: self.npm.map(Sourced::with_command_line).inherit(base.npm),\n            pnpm: self.pnpm.map(Sourced::with_command_line).inherit(base.pnpm),\n            yarn: self.yarn.map(Sourced::with_command_line).inherit(base.yarn),\n        }\n    }\n}\n\nimpl From<CliPlatform> for Option<Platform> {\n    /// Converts the `CliPlatform` into a possible Platform without a base from which to inherit\n    fn from(base: CliPlatform) -> Option<Platform> {\n        match base.node {\n            None => None,\n            Some(node) => Some(Platform {\n                node: Sourced::with_command_line(node),\n                npm: base.npm.map(Sourced::with_command_line).into(),\n                pnpm: base.pnpm.map(Sourced::with_command_line).into(),\n                yarn: base.yarn.map(Sourced::with_command_line).into(),\n            }),\n        }\n    }\n}\n\n/// Represents a real Platform, with Versions pulled from one or more `PlatformSpec`s\n#[derive(Clone)]\npub struct Platform {\n    pub node: Sourced<Version>,\n    pub npm: Option<Sourced<Version>>,\n    pub pnpm: Option<Sourced<Version>>,\n    pub yarn: Option<Sourced<Version>>,\n}\n\nimpl Platform {\n    /// Returns the user's currently active platform, if any\n    ///\n    /// Active platform is determined by first looking at the Project Platform\n    ///\n    /// - If there is a project platform then we use it\n    ///   - If there is no pnpm/Yarn version in the project platform, we pull\n    ///     pnpm/Yarn from the default platform if available, and merge the two\n    ///     platforms into a final one\n    /// - If there is no Project platform, then we use the user Default Platform\n    pub fn current(session: &mut Session) -> Fallible<Option<Self>> {\n        if let Some(mut platform) = session.project_platform()?.map(PlatformSpec::as_project) {\n            if platform.pnpm.is_none() {\n                platform.pnpm = session\n                    .default_platform()?\n                    .and_then(|default_platform| default_platform.pnpm.clone())\n                    .map(Sourced::with_default);\n            }\n\n            if platform.yarn.is_none() {\n                platform.yarn = session\n                    .default_platform()?\n                    .and_then(|default_platform| default_platform.yarn.clone())\n                    .map(Sourced::with_default);\n            }\n\n            Ok(Some(platform))\n        } else {\n            Ok(session.default_platform()?.map(PlatformSpec::as_default))\n        }\n    }\n\n    /// Check out a `Platform` into a fully-realized `Image`\n    ///\n    /// This will ensure that all necessary tools are fetched and available for execution\n    pub fn checkout(self, session: &mut Session) -> Fallible<Image> {\n        Node::new(self.node.value.clone()).ensure_fetched(session)?;\n\n        if let Some(Sourced { value: version, .. }) = &self.npm {\n            Npm::new(version.clone()).ensure_fetched(session)?;\n        }\n\n        // Only force download of the pnpm version if the pnpm feature flag is set. If it isn't,\n        // then we won't be using the `Pnpm` tool to execute (we will be relying on the global\n        // package logic), so fetching the Pnpm version would only be redundant work.\n        if env::var_os(VOLTA_FEATURE_PNPM).is_some() {\n            if let Some(Sourced { value: version, .. }) = &self.pnpm {\n                Pnpm::new(version.clone()).ensure_fetched(session)?;\n            }\n        }\n\n        if let Some(Sourced { value: version, .. }) = &self.yarn {\n            Yarn::new(version.clone()).ensure_fetched(session)?;\n        }\n\n        Ok(Image {\n            node: self.node,\n            npm: self.npm,\n            pnpm: self.pnpm,\n            yarn: self.yarn,\n        })\n    }\n}\n\nfn build_path_error() -> ErrorKind {\n    ErrorKind::BuildPathError\n}\n"
  },
  {
    "path": "crates/volta-core/src/platform/system.rs",
    "content": "use std::ffi::OsString;\n\nuse super::build_path_error;\nuse crate::error::{Context, Fallible};\nuse crate::layout::env_paths;\n\n/// A lightweight namespace type representing the system environment, i.e. the environment\n/// with Volta removed.\npub struct System;\n\nimpl System {\n    /// Produces a modified version of the current `PATH` environment variable that\n    /// removes the Volta shims and binaries, to use for running system node and\n    /// executables.\n    pub fn path() -> Fallible<OsString> {\n        let old_path = envoy::path().unwrap_or_else(|| envoy::Var::from(\"\"));\n        let mut new_path = old_path.split();\n\n        for remove_path in env_paths()? {\n            new_path = new_path.remove(remove_path);\n        }\n\n        new_path.join().with_context(build_path_error)\n    }\n}\n"
  },
  {
    "path": "crates/volta-core/src/platform/tests.rs",
    "content": "use super::*;\nuse crate::layout::volta_home;\n#[cfg(windows)]\nuse crate::layout::volta_install;\nuse node_semver::Version;\n#[cfg(windows)]\nuse std::path::PathBuf;\n\n// Since unit tests are run in parallel, tests that modify the PATH environment variable are subject to race conditions\n// To prevent that, ensure that all tests that rely on PATH are run in serial by adding them to this meta-test\n#[test]\nfn test_paths() {\n    test_image_path();\n    test_system_path();\n}\n\n#[cfg(unix)]\nfn build_test_path() -> String {\n    format!(\n        \"{}:/usr/bin:/bin\",\n        volta_home().unwrap().shim_dir().to_string_lossy()\n    )\n}\n\n#[cfg(windows)]\nfn build_test_path() -> String {\n    let pathbufs = vec![\n        volta_home().unwrap().shim_dir().to_owned(),\n        PathBuf::from(\"C:\\\\\\\\somebin\"),\n        volta_install().unwrap().root().to_owned(),\n        PathBuf::from(\"D:\\\\\\\\ProbramFlies\"),\n    ];\n    std::env::join_paths(pathbufs.iter())\n        .unwrap()\n        .into_string()\n        .expect(\"Could not create path containing shim dir\")\n}\n\nfn test_image_path() {\n    #[cfg(unix)]\n    let path_delimiter = \":\";\n    #[cfg(windows)]\n    let path_delimiter = \";\";\n    let path = build_test_path();\n    std::env::set_var(\"PATH\", &path);\n\n    let node_bin = volta_home().unwrap().node_image_bin_dir(\"1.2.3\");\n    let expected_node_bin = node_bin.to_str().unwrap();\n\n    let npm_bin = volta_home().unwrap().npm_image_bin_dir(\"6.4.3\");\n    let expected_npm_bin = npm_bin.to_str().unwrap();\n\n    let pnpm_bin = volta_home().unwrap().pnpm_image_bin_dir(\"7.7.1\");\n    let expected_pnpm_bin = pnpm_bin.to_str().unwrap();\n\n    let yarn_bin = volta_home().unwrap().yarn_image_bin_dir(\"4.5.7\");\n    let expected_yarn_bin = yarn_bin.to_str().unwrap();\n\n    let v123 = Version::parse(\"1.2.3\").unwrap();\n    let v457 = Version::parse(\"4.5.7\").unwrap();\n    let v643 = Version::parse(\"6.4.3\").unwrap();\n    let v771 = Version::parse(\"7.7.1\").unwrap();\n\n    let only_node = Image {\n        node: Sourced::with_default(v123.clone()),\n        npm: None,\n        pnpm: None,\n        yarn: None,\n    };\n\n    assert_eq!(\n        only_node.path().unwrap().into_string().unwrap(),\n        [expected_node_bin, &path].join(path_delimiter)\n    );\n\n    let node_npm = Image {\n        node: Sourced::with_default(v123.clone()),\n        npm: Some(Sourced::with_default(v643.clone())),\n        pnpm: None,\n        yarn: None,\n    };\n\n    assert_eq!(\n        node_npm.path().unwrap().into_string().unwrap(),\n        [expected_npm_bin, expected_node_bin, &path].join(path_delimiter)\n    );\n\n    let node_pnpm = Image {\n        node: Sourced::with_default(v123.clone()),\n        npm: None,\n        pnpm: Some(Sourced::with_default(v771.clone())),\n        yarn: None,\n    };\n\n    assert_eq!(\n        node_pnpm.path().unwrap().into_string().unwrap(),\n        [expected_pnpm_bin, expected_node_bin, &path].join(path_delimiter)\n    );\n\n    let node_yarn = Image {\n        node: Sourced::with_default(v123.clone()),\n        npm: None,\n        pnpm: None,\n        yarn: Some(Sourced::with_default(v457.clone())),\n    };\n\n    assert_eq!(\n        node_yarn.path().unwrap().into_string().unwrap(),\n        [expected_yarn_bin, expected_node_bin, &path].join(path_delimiter)\n    );\n\n    let node_npm_pnpm = Image {\n        node: Sourced::with_default(v123.clone()),\n        npm: Some(Sourced::with_default(v643.clone())),\n        pnpm: Some(Sourced::with_default(v771)),\n        yarn: None,\n    };\n\n    assert_eq!(\n        node_npm_pnpm.path().unwrap().into_string().unwrap(),\n        [\n            expected_npm_bin,\n            expected_pnpm_bin,\n            expected_node_bin,\n            &path\n        ]\n        .join(path_delimiter)\n    );\n\n    let node_npm_yarn = Image {\n        node: Sourced::with_default(v123),\n        npm: Some(Sourced::with_default(v643)),\n        pnpm: None,\n        yarn: Some(Sourced::with_default(v457)),\n    };\n\n    assert_eq!(\n        node_npm_yarn.path().unwrap().into_string().unwrap(),\n        [\n            expected_npm_bin,\n            expected_yarn_bin,\n            expected_node_bin,\n            &path\n        ]\n        .join(path_delimiter)\n    );\n}\n\nfn test_system_path() {\n    let path = build_test_path();\n    std::env::set_var(\"PATH\", path);\n\n    #[cfg(unix)]\n    let expected_path = String::from(\"/usr/bin:/bin\");\n    #[cfg(windows)]\n    let expected_path = String::from(\"C:\\\\\\\\somebin;D:\\\\\\\\ProbramFlies\");\n\n    assert_eq!(\n        System::path().unwrap().into_string().unwrap(),\n        expected_path\n    );\n}\n\nmod inherit_option {\n    mod map {\n        use super::super::super::*;\n\n        #[test]\n        fn converts_some_value() {\n            let opt = InheritOption::Some(1);\n\n            assert_eq!(opt.map(|n| n + 1), InheritOption::Some(2));\n        }\n\n        #[test]\n        fn leaves_none() {\n            let opt: InheritOption<i32> = InheritOption::None;\n\n            assert_eq!(opt.map(|n| n + 1), InheritOption::None);\n        }\n\n        #[test]\n        fn leaves_inherit() {\n            let opt: InheritOption<i32> = InheritOption::Inherit;\n\n            assert_eq!(opt.map(|n| n + 1), InheritOption::Inherit);\n        }\n    }\n\n    mod inherit {\n        use super::super::super::*;\n\n        #[test]\n        fn keeps_some_value() {\n            let opt = InheritOption::Some(1);\n\n            assert_eq!(opt.inherit(Some(2)), Some(1));\n        }\n\n        #[test]\n        fn leaves_none() {\n            let opt = InheritOption::None;\n\n            assert_eq!(opt.inherit(Some(2)), None);\n        }\n\n        #[test]\n        fn inherits_from_base() {\n            let opt = InheritOption::Inherit;\n\n            assert_eq!(opt.inherit(Some(2)), Some(2));\n        }\n    }\n}\n\nmod cli_platform {\n    use node_semver::Version;\n\n    const NODE_VERSION: Version = Version {\n        major: 12,\n        minor: 14,\n        patch: 1,\n        build: Vec::new(),\n        pre_release: Vec::new(),\n    };\n    const NPM_VERSION: Version = Version {\n        major: 6,\n        minor: 13,\n        patch: 2,\n        build: Vec::new(),\n        pre_release: Vec::new(),\n    };\n    const YARN_VERSION: Version = Version {\n        major: 1,\n        minor: 17,\n        patch: 0,\n        build: Vec::new(),\n        pre_release: Vec::new(),\n    };\n\n    mod merge {\n        use super::super::super::*;\n        use super::*;\n\n        #[test]\n        fn uses_node() {\n            let test = CliPlatform {\n                node: Some(NODE_VERSION),\n                npm: InheritOption::default(),\n                pnpm: InheritOption::default(),\n                yarn: InheritOption::default(),\n            };\n\n            let base = Platform {\n                node: Sourced::with_default(Version::from((10, 10, 10))),\n                npm: None,\n                pnpm: None,\n                yarn: None,\n            };\n\n            let merged = test.merge(base);\n\n            assert_eq!(merged.node.value, NODE_VERSION);\n            assert_eq!(merged.node.source, Source::CommandLine);\n        }\n\n        #[test]\n        fn inherits_node() {\n            let test = CliPlatform {\n                node: None,\n                npm: InheritOption::default(),\n                pnpm: InheritOption::default(),\n                yarn: InheritOption::default(),\n            };\n\n            let base = Platform {\n                node: Sourced::with_default(NODE_VERSION),\n                npm: None,\n                pnpm: None,\n                yarn: None,\n            };\n\n            let merged = test.merge(base);\n\n            assert_eq!(merged.node.value, NODE_VERSION);\n            assert_eq!(merged.node.source, Source::Default);\n        }\n\n        #[test]\n        fn uses_npm() {\n            let test = CliPlatform {\n                node: Some(NODE_VERSION),\n                npm: InheritOption::Some(NPM_VERSION),\n                pnpm: InheritOption::default(),\n                yarn: InheritOption::default(),\n            };\n\n            let base = Platform {\n                node: Sourced::with_default(Version::from((10, 10, 10))),\n                npm: Some(Sourced::with_default(Version::from((5, 6, 3)))),\n                pnpm: None,\n                yarn: None,\n            };\n\n            let merged = test.merge(base);\n\n            let merged_npm = merged.npm.unwrap();\n            assert_eq!(merged_npm.value, NPM_VERSION);\n            assert_eq!(merged_npm.source, Source::CommandLine);\n        }\n\n        #[test]\n        fn inherits_npm() {\n            let test = CliPlatform {\n                node: Some(NODE_VERSION),\n                npm: InheritOption::Inherit,\n                pnpm: InheritOption::default(),\n                yarn: InheritOption::default(),\n            };\n\n            let base = Platform {\n                node: Sourced::with_default(Version::from((10, 10, 10))),\n                npm: Some(Sourced::with_default(NPM_VERSION)),\n                pnpm: None,\n                yarn: None,\n            };\n\n            let merged = test.merge(base);\n\n            let merged_npm = merged.npm.unwrap();\n            assert_eq!(merged_npm.value, NPM_VERSION);\n            assert_eq!(merged_npm.source, Source::Default);\n        }\n\n        #[test]\n        fn none_does_not_inherit_npm() {\n            let test = CliPlatform {\n                node: Some(NODE_VERSION),\n                npm: InheritOption::None,\n                pnpm: InheritOption::default(),\n                yarn: InheritOption::default(),\n            };\n\n            let base = Platform {\n                node: Sourced::with_default(Version::from((10, 10, 10))),\n                npm: Some(Sourced::with_default(NPM_VERSION)),\n                pnpm: None,\n                yarn: None,\n            };\n\n            let merged = test.merge(base);\n\n            assert!(merged.npm.is_none());\n        }\n\n        #[test]\n        fn uses_yarn() {\n            let test = CliPlatform {\n                node: Some(NODE_VERSION),\n                npm: InheritOption::default(),\n                pnpm: InheritOption::default(),\n                yarn: InheritOption::Some(YARN_VERSION),\n            };\n\n            let base = Platform {\n                node: Sourced::with_default(Version::from((10, 10, 10))),\n                npm: None,\n                pnpm: None,\n                yarn: Some(Sourced::with_default(Version::from((1, 10, 3)))),\n            };\n\n            let merged = test.merge(base);\n\n            let merged_yarn = merged.yarn.unwrap();\n            assert_eq!(merged_yarn.value, YARN_VERSION);\n            assert_eq!(merged_yarn.source, Source::CommandLine);\n        }\n\n        #[test]\n        fn inherits_yarn() {\n            let test = CliPlatform {\n                node: Some(NODE_VERSION),\n                npm: InheritOption::default(),\n                pnpm: InheritOption::default(),\n                yarn: InheritOption::Inherit,\n            };\n\n            let base = Platform {\n                node: Sourced::with_default(Version::from((10, 10, 10))),\n                npm: None,\n                pnpm: None,\n                yarn: Some(Sourced::with_default(YARN_VERSION)),\n            };\n\n            let merged = test.merge(base);\n\n            let merged_yarn = merged.yarn.unwrap();\n            assert_eq!(merged_yarn.value, YARN_VERSION);\n            assert_eq!(merged_yarn.source, Source::Default);\n        }\n\n        #[test]\n        fn none_does_not_inherit_yarn() {\n            let test = CliPlatform {\n                node: Some(NODE_VERSION),\n                npm: InheritOption::default(),\n                pnpm: InheritOption::default(),\n                yarn: InheritOption::None,\n            };\n\n            let base = Platform {\n                node: Sourced::with_default(Version::from((10, 10, 10))),\n                npm: None,\n                pnpm: None,\n                yarn: Some(Sourced::with_default(YARN_VERSION)),\n            };\n\n            let merged = test.merge(base);\n\n            assert!(merged.yarn.is_none());\n        }\n    }\n\n    mod into_platform {\n        use super::super::super::*;\n        use super::*;\n\n        #[test]\n        fn none_if_no_node() {\n            let cli = CliPlatform {\n                node: None,\n                npm: InheritOption::default(),\n                pnpm: InheritOption::default(),\n                yarn: InheritOption::default(),\n            };\n\n            let transformed: Option<Platform> = cli.into();\n\n            assert!(transformed.is_none());\n        }\n\n        #[test]\n        fn uses_cli_node() {\n            let cli = CliPlatform {\n                node: Some(NODE_VERSION),\n                npm: InheritOption::default(),\n                pnpm: InheritOption::default(),\n                yarn: InheritOption::default(),\n            };\n\n            let transformed: Option<Platform> = cli.into();\n\n            let node = transformed.unwrap().node;\n            assert_eq!(node.value, NODE_VERSION);\n            assert_eq!(node.source, Source::CommandLine);\n        }\n\n        #[test]\n        fn uses_cli_npm() {\n            let cli = CliPlatform {\n                node: Some(NODE_VERSION),\n                npm: InheritOption::Some(NPM_VERSION),\n                pnpm: InheritOption::default(),\n                yarn: InheritOption::default(),\n            };\n\n            let transformed: Option<Platform> = cli.into();\n\n            let npm = transformed.unwrap().npm.unwrap();\n            assert_eq!(npm.value, NPM_VERSION);\n            assert_eq!(npm.source, Source::CommandLine);\n        }\n\n        #[test]\n        fn no_npm() {\n            let cli = CliPlatform {\n                node: Some(NODE_VERSION),\n                npm: InheritOption::None,\n                pnpm: InheritOption::default(),\n                yarn: InheritOption::default(),\n            };\n\n            let transformed: Option<Platform> = cli.into();\n\n            assert!(transformed.unwrap().npm.is_none());\n        }\n\n        #[test]\n        fn inherit_npm_becomes_none() {\n            let cli = CliPlatform {\n                node: Some(NODE_VERSION),\n                npm: InheritOption::Inherit,\n                pnpm: InheritOption::default(),\n                yarn: InheritOption::default(),\n            };\n\n            let transformed: Option<Platform> = cli.into();\n\n            assert!(transformed.unwrap().npm.is_none());\n        }\n\n        #[test]\n        fn uses_cli_yarn() {\n            let cli = CliPlatform {\n                node: Some(NODE_VERSION),\n                npm: InheritOption::default(),\n                pnpm: InheritOption::default(),\n                yarn: InheritOption::Some(YARN_VERSION),\n            };\n\n            let transformed: Option<Platform> = cli.into();\n\n            let yarn = transformed.unwrap().yarn.unwrap();\n            assert_eq!(yarn.value, YARN_VERSION);\n            assert_eq!(yarn.source, Source::CommandLine);\n        }\n\n        #[test]\n        fn no_yarn() {\n            let cli = CliPlatform {\n                node: Some(NODE_VERSION),\n                npm: InheritOption::default(),\n                pnpm: InheritOption::default(),\n                yarn: InheritOption::None,\n            };\n\n            let transformed: Option<Platform> = cli.into();\n\n            assert!(transformed.unwrap().yarn.is_none());\n        }\n\n        #[test]\n        fn inherit_yarn_becomes_none() {\n            let cli = CliPlatform {\n                node: Some(NODE_VERSION),\n                npm: InheritOption::default(),\n                pnpm: InheritOption::default(),\n                yarn: InheritOption::Inherit,\n            };\n\n            let transformed: Option<Platform> = cli.into();\n\n            assert!(transformed.unwrap().yarn.is_none());\n        }\n    }\n}\n"
  },
  {
    "path": "crates/volta-core/src/project/mod.rs",
    "content": "//! Provides the `Project` type, which represents a Node project tree in\n//! the filesystem.\n\nuse std::env;\nuse std::ffi::OsStr;\nuse std::iter::once;\nuse std::path::{Path, PathBuf};\n\nuse node_semver::Version;\nuse once_cell::unsync::OnceCell;\n\nuse crate::error::{Context, ErrorKind, Fallible, VoltaError};\nuse crate::layout::volta_home;\nuse crate::platform::PlatformSpec;\nuse crate::tool::BinConfig;\nuse chain_map::ChainMap;\nuse indexmap::IndexSet;\n\nmod serial;\n#[cfg(test)]\nmod tests;\n\nuse serial::{update_manifest, Manifest, ManifestKey};\n\n/// A lazily loaded Project\npub struct LazyProject {\n    project: OnceCell<Option<Project>>,\n}\n\nimpl LazyProject {\n    pub fn init() -> Self {\n        LazyProject {\n            project: OnceCell::new(),\n        }\n    }\n\n    pub fn get(&self) -> Fallible<Option<&Project>> {\n        let project = self.project.get_or_try_init(Project::for_current_dir)?;\n        Ok(project.as_ref())\n    }\n\n    pub fn get_mut(&mut self) -> Fallible<Option<&mut Project>> {\n        let _ = self.project.get_or_try_init(Project::for_current_dir)?;\n        Ok(self.project.get_mut().unwrap().as_mut())\n    }\n}\n\n/// A Node project workspace in the filesystem\n#[cfg_attr(test, derive(Debug))]\npub struct Project {\n    manifest_file: PathBuf,\n    workspace_manifests: IndexSet<PathBuf>,\n    dependencies: ChainMap<String, String>,\n    platform: Option<PlatformSpec>,\n}\n\nimpl Project {\n    /// Creates an optional Project instance from the current directory\n    fn for_current_dir() -> Fallible<Option<Self>> {\n        let current_dir = env::current_dir().with_context(|| ErrorKind::CurrentDirError)?;\n        Self::for_dir(current_dir)\n    }\n\n    /// Creates an optional Project instance from the specified directory\n    ///\n    /// Will search ancestors to find a `package.json` and use that as the root of the project\n    fn for_dir(base_dir: PathBuf) -> Fallible<Option<Self>> {\n        match find_closest_root(base_dir) {\n            Some(mut project) => {\n                project.push(\"package.json\");\n                Self::from_file(project).map(Some)\n            }\n            None => Ok(None),\n        }\n    }\n\n    /// Creates a Project instance from the given package manifest file (`package.json`)\n    fn from_file(manifest_file: PathBuf) -> Fallible<Self> {\n        let manifest = Manifest::from_file(&manifest_file)?;\n        let mut dependencies: ChainMap<String, String> = manifest.dependency_maps.collect();\n        let mut workspace_manifests = IndexSet::new();\n        let mut platform = manifest.platform;\n        let mut extends = manifest.extends;\n\n        // Iterate the `volta.extends` chain, parsing each file in turn\n        while let Some(path) = extends {\n            // Detect cycles to prevent infinite looping\n            if path == manifest_file || workspace_manifests.contains(&path) {\n                let mut paths = vec![manifest_file];\n                paths.extend(workspace_manifests);\n\n                return Err(ErrorKind::ExtensionCycleError {\n                    paths,\n                    duplicate: path,\n                }\n                .into());\n            }\n\n            let manifest = Manifest::from_file(&path)?;\n            workspace_manifests.insert(path);\n            dependencies.extend(manifest.dependency_maps);\n\n            platform = match (platform, manifest.platform) {\n                (Some(base), Some(ext)) => Some(base.merge(ext)),\n                (Some(plat), None) | (None, Some(plat)) => Some(plat),\n                (None, None) => None,\n            };\n\n            extends = manifest.extends;\n        }\n\n        let platform = platform.map(TryInto::try_into).transpose()?;\n\n        Ok(Project {\n            manifest_file,\n            workspace_manifests,\n            dependencies,\n            platform,\n        })\n    }\n\n    /// Returns a reference to the manifest file for the current project\n    pub fn manifest_file(&self) -> &Path {\n        &self.manifest_file\n    }\n\n    /// Returns an iterator of paths to all of the workspace roots\n    pub fn workspace_roots(&self) -> impl Iterator<Item = &Path> {\n        // Invariant: self.manifest_file and self.extensions will only contain paths to files that we successfully loaded\n        once(&self.manifest_file)\n            .chain(self.workspace_manifests.iter())\n            .map(|file| file.parent().expect(\"File paths always have a parent\"))\n    }\n\n    /// Returns a reference to the Project's `PlatformSpec`, if available\n    pub fn platform(&self) -> Option<&PlatformSpec> {\n        self.platform.as_ref()\n    }\n\n    /// Returns true if the project dependency map contains the specified dependency\n    pub fn has_direct_dependency(&self, dependency: &str) -> bool {\n        self.dependencies.contains_key(dependency)\n    }\n\n    /// Returns true if the input binary name is a direct dependency of the input project\n    pub fn has_direct_bin(&self, bin_name: &OsStr) -> Fallible<bool> {\n        if let Some(name) = bin_name.to_str() {\n            let config_path = volta_home()?.default_tool_bin_config(name);\n\n            return match BinConfig::from_file_if_exists(config_path)? {\n                None => Ok(false),\n                Some(config) => Ok(self.has_direct_dependency(&config.package)),\n            };\n        }\n        Ok(false)\n    }\n\n    /// Searches the project roots to find the path to a project-local binary file\n    pub fn find_bin<P: AsRef<Path>>(&self, bin_name: P) -> Option<PathBuf> {\n        self.workspace_roots().find_map(|root| {\n            let mut bin_path = root.join(\"node_modules\");\n            bin_path.push(\".bin\");\n            bin_path.push(&bin_name);\n\n            if bin_path.is_file() {\n                Some(bin_path)\n            } else {\n                None\n            }\n        })\n    }\n\n    /// Yarn projects that are using PnP or pnpm linker need to use yarn run.\n    // (project uses Yarn berry if 'yarnrc.yml' exists, uses PnP if '.pnp.js' or '.pnp.cjs' exist)\n    pub fn needs_yarn_run(&self) -> bool {\n        self.platform()\n            .is_some_and(|platform| platform.yarn.is_some())\n            && self.workspace_roots().any(|x| {\n                x.join(\".yarnrc.yml\").exists()\n                    || x.join(\".pnp.cjs\").exists()\n                    || x.join(\".pnp.js\").exists()\n            })\n    }\n\n    /// Pins the Node version in this project's manifest file\n    pub fn pin_node(&mut self, version: Version) -> Fallible<()> {\n        update_manifest(&self.manifest_file, ManifestKey::Node, Some(&version))?;\n\n        if let Some(platform) = self.platform.as_mut() {\n            platform.node = version;\n        } else {\n            self.platform = Some(PlatformSpec {\n                node: version,\n                npm: None,\n                pnpm: None,\n                yarn: None,\n            });\n        }\n\n        Ok(())\n    }\n\n    /// Pins the npm version in this project's manifest file\n    pub fn pin_npm(&mut self, version: Option<Version>) -> Fallible<()> {\n        if let Some(platform) = self.platform.as_mut() {\n            update_manifest(&self.manifest_file, ManifestKey::Npm, version.as_ref())?;\n\n            platform.npm = version;\n\n            Ok(())\n        } else {\n            Err(ErrorKind::NoPinnedNodeVersion { tool: \"npm\".into() }.into())\n        }\n    }\n\n    /// Pins the pnpm version in this project's manifest file\n    pub fn pin_pnpm(&mut self, version: Option<Version>) -> Fallible<()> {\n        if let Some(platform) = self.platform.as_mut() {\n            update_manifest(&self.manifest_file, ManifestKey::Pnpm, version.as_ref())?;\n\n            platform.pnpm = version;\n\n            Ok(())\n        } else {\n            Err(ErrorKind::NoPinnedNodeVersion {\n                tool: \"pnpm\".into(),\n            }\n            .into())\n        }\n    }\n\n    /// Pins the Yarn version in this project's manifest file\n    pub fn pin_yarn(&mut self, version: Option<Version>) -> Fallible<()> {\n        if let Some(platform) = self.platform.as_mut() {\n            update_manifest(&self.manifest_file, ManifestKey::Yarn, version.as_ref())?;\n\n            platform.yarn = version;\n\n            Ok(())\n        } else {\n            Err(ErrorKind::NoPinnedNodeVersion {\n                tool: \"Yarn\".into(),\n            }\n            .into())\n        }\n    }\n}\n\nfn is_node_root(dir: &Path) -> bool {\n    dir.join(\"package.json\").exists()\n}\n\nfn is_node_modules(dir: &Path) -> bool {\n    dir.file_name().is_some_and(|tail| tail == \"node_modules\")\n}\n\nfn is_dependency(dir: &Path) -> bool {\n    dir.parent().is_some_and(is_node_modules)\n}\n\nfn is_project_root(dir: &Path) -> bool {\n    is_node_root(dir) && !is_dependency(dir)\n}\n\n/// Starts at `base_dir` and walks up the directory tree until a package.json file is found\npub(crate) fn find_closest_root(mut dir: PathBuf) -> Option<PathBuf> {\n    while !is_project_root(&dir) {\n        if !dir.pop() {\n            return None;\n        }\n    }\n\n    Some(dir)\n}\n\nstruct PartialPlatform {\n    node: Option<Version>,\n    npm: Option<Version>,\n    pnpm: Option<Version>,\n    yarn: Option<Version>,\n}\n\nimpl PartialPlatform {\n    fn merge(self, other: PartialPlatform) -> PartialPlatform {\n        PartialPlatform {\n            node: self.node.or(other.node),\n            npm: self.npm.or(other.npm),\n            pnpm: self.pnpm.or(other.pnpm),\n            yarn: self.yarn.or(other.yarn),\n        }\n    }\n}\n\nimpl TryFrom<PartialPlatform> for PlatformSpec {\n    type Error = VoltaError;\n\n    fn try_from(partial: PartialPlatform) -> Fallible<PlatformSpec> {\n        let node = partial.node.ok_or(ErrorKind::NoProjectNodeInManifest)?;\n\n        Ok(PlatformSpec {\n            node,\n            npm: partial.npm,\n            pnpm: partial.pnpm,\n            yarn: partial.yarn,\n        })\n    }\n}\n"
  },
  {
    "path": "crates/volta-core/src/project/serial.rs",
    "content": "use std::collections::HashMap;\nuse std::fmt;\nuse std::fs::{read_to_string, File};\nuse std::io::Write;\nuse std::path::{Path, PathBuf};\n\nuse super::PartialPlatform;\nuse crate::error::{Context, ErrorKind, Fallible};\nuse crate::version::parse_version;\nuse dunce::canonicalize;\nuse node_semver::Version;\nuse serde::{Deserialize, Serialize};\nuse serde_json::{Map, Value};\n\npub type DependencyMapIterator = std::iter::Chain<\n    std::option::IntoIter<HashMap<String, String>>,\n    std::option::IntoIter<HashMap<String, String>>,\n>;\n\npub(super) struct Manifest {\n    pub dependency_maps: DependencyMapIterator,\n    pub platform: Option<PartialPlatform>,\n    pub extends: Option<PathBuf>,\n}\n\nimpl Manifest {\n    pub fn from_file(file: &Path) -> Fallible<Self> {\n        let raw = RawManifest::from_file(file)?;\n\n        let dependency_maps = raw.dependencies.into_iter().chain(raw.dev_dependencies);\n\n        let (platform, extends) = match raw.volta {\n            Some(toolchain) => {\n                let (partial, extends) = toolchain.parse_split()?;\n\n                let next = extends\n                    .map(|path| {\n                        // Invariant: Since we successfully parsed it, we know we have a path to a file\n                        let unresolved = file\n                            .parent()\n                            .expect(\"File paths always have a parent\")\n                            .join(&path);\n                        canonicalize(unresolved)\n                            .with_context(|| ErrorKind::ExtensionPathError { path })\n                    })\n                    .transpose()?;\n                (Some(partial), next)\n            }\n            None => (None, None),\n        };\n\n        Ok(Manifest {\n            dependency_maps,\n            platform,\n            extends,\n        })\n    }\n}\n\npub(super) enum ManifestKey {\n    Node,\n    Npm,\n    Pnpm,\n    Yarn,\n}\n\nimpl fmt::Display for ManifestKey {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        f.write_str(match self {\n            ManifestKey::Node => \"node\",\n            ManifestKey::Npm => \"npm\",\n            ManifestKey::Pnpm => \"pnpm\",\n            ManifestKey::Yarn => \"yarn\",\n        })\n    }\n}\n\n/// Updates the `volta` hash in the specified manifest with the given key and value\n///\n/// Will create the `volta` hash if it isn't already present\n///\n/// If the value is `None`, will remove the key from the hash\npub(super) fn update_manifest(\n    file: &Path,\n    key: ManifestKey,\n    value: Option<&Version>,\n) -> Fallible<()> {\n    let contents = read_to_string(file).with_context(|| ErrorKind::PackageReadError {\n        file: file.to_owned(),\n    })?;\n\n    let mut manifest: serde_json::Value =\n        serde_json::from_str(&contents).with_context(|| ErrorKind::PackageParseError {\n            file: file.to_owned(),\n        })?;\n\n    let root = manifest\n        .as_object_mut()\n        .ok_or_else(|| ErrorKind::PackageParseError {\n            file: file.to_owned(),\n        })?;\n\n    let key = key.to_string();\n\n    match (value, root.get_mut(\"volta\").and_then(|v| v.as_object_mut())) {\n        (Some(v), Some(hash)) => {\n            hash.insert(key, Value::String(v.to_string()));\n        }\n        (None, Some(hash)) => {\n            hash.remove(&key);\n        }\n        (Some(v), None) => {\n            let mut map = Map::new();\n            map.insert(key, Value::String(v.to_string()));\n            root.insert(\"volta\".into(), Value::Object(map));\n        }\n        (None, None) => {}\n    }\n\n    let indent = detect_indent::detect_indent(&contents);\n    let mut output = File::create(file).with_context(|| ErrorKind::PackageWriteError {\n        file: file.to_owned(),\n    })?;\n    let formatter = serde_json::ser::PrettyFormatter::with_indent(indent.indent().as_bytes());\n    let mut ser = serde_json::Serializer::with_formatter(&output, formatter);\n    manifest\n        .serialize(&mut ser)\n        .with_context(|| ErrorKind::PackageWriteError {\n            file: file.to_owned(),\n        })?;\n\n    if contents.ends_with('\\n') {\n        writeln!(output).with_context(|| ErrorKind::PackageWriteError {\n            file: file.to_owned(),\n        })?;\n    }\n\n    Ok(())\n}\n\n#[derive(Deserialize)]\nstruct RawManifest {\n    dependencies: Option<HashMap<String, String>>,\n\n    #[serde(rename = \"devDependencies\")]\n    dev_dependencies: Option<HashMap<String, String>>,\n\n    volta: Option<ToolchainSpec>,\n}\n\nimpl RawManifest {\n    fn from_file(package: &Path) -> Fallible<Self> {\n        let file = File::open(package).with_context(|| ErrorKind::PackageReadError {\n            file: package.to_owned(),\n        })?;\n\n        serde_json::de::from_reader(file).with_context(|| ErrorKind::PackageParseError {\n            file: package.to_owned(),\n        })\n    }\n}\n\n#[derive(Default, Deserialize, Serialize)]\nstruct ToolchainSpec {\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    node: Option<String>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    npm: Option<String>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pnpm: Option<String>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    yarn: Option<String>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    extends: Option<PathBuf>,\n}\n\nimpl ToolchainSpec {\n    /// Moves the tool versions into a `PartialPlatform` and returns that along with the `extends` value\n    fn parse_split(self) -> Fallible<(PartialPlatform, Option<PathBuf>)> {\n        let node = self.node.map(parse_version).transpose()?;\n        let npm = self.npm.map(parse_version).transpose()?;\n        let pnpm = self.pnpm.map(parse_version).transpose()?;\n        let yarn = self.yarn.map(parse_version).transpose()?;\n\n        let platform = PartialPlatform {\n            node,\n            npm,\n            pnpm,\n            yarn,\n        };\n\n        Ok((platform, self.extends))\n    }\n}\n"
  },
  {
    "path": "crates/volta-core/src/project/tests.rs",
    "content": "use std::path::PathBuf;\n\nuse super::*;\n\nfn fixture_path(fixture_dirs: &[&str]) -> PathBuf {\n    let mut cargo_manifest_dir = PathBuf::from(env!(\"CARGO_MANIFEST_DIR\"));\n    cargo_manifest_dir.push(\"fixtures\");\n\n    for fixture_dir in fixture_dirs.iter() {\n        cargo_manifest_dir.push(fixture_dir);\n    }\n\n    cargo_manifest_dir\n}\n\nmod find_closest_root {\n    use super::*;\n\n    #[test]\n    fn test_find_closest_root_direct() {\n        let base_dir = fixture_path(&[\"basic\"]);\n        let project_dir =\n            find_closest_root(base_dir.clone()).expect(\"Failed to find project directory\");\n\n        assert_eq!(project_dir, base_dir);\n    }\n\n    #[test]\n    fn test_find_closest_root_ancestor() {\n        let base_dir = fixture_path(&[\"basic\", \"subdir\"]);\n        let project_dir = find_closest_root(base_dir).expect(\"Failed to find project directory\");\n\n        assert_eq!(project_dir, fixture_path(&[\"basic\"]));\n    }\n\n    #[test]\n    fn test_find_closest_root_dependency() {\n        let base_dir = fixture_path(&[\"basic\", \"node_modules\", \"eslint\"]);\n        let project_dir = find_closest_root(base_dir).expect(\"Failed to find project directory\");\n\n        assert_eq!(project_dir, fixture_path(&[\"basic\"]));\n    }\n}\n\nmod project {\n    use super::*;\n\n    #[test]\n    fn manifest_file() {\n        let project_path = fixture_path(&[\"basic\"]);\n        let test_project = Project::for_dir(project_path).unwrap().unwrap();\n\n        let expected = fixture_path(&[\"basic\", \"package.json\"]);\n        assert_eq!(test_project.manifest_file(), &expected);\n    }\n\n    #[test]\n    fn workspace_roots() {\n        let project_path = fixture_path(&[\"nested\", \"subproject\", \"inner_project\"]);\n        let expected_base = project_path.clone();\n        let test_project = Project::for_dir(project_path).unwrap().unwrap();\n\n        let expected = vec![\n            &*expected_base,\n            expected_base.parent().unwrap(),\n            expected_base.parent().unwrap().parent().unwrap(),\n        ];\n\n        assert_eq!(test_project.workspace_roots().collect::<Vec<_>>(), expected);\n    }\n\n    #[test]\n    fn platform_simple() {\n        let project_path = fixture_path(&[\"basic\"]);\n        let test_project = Project::for_dir(project_path).unwrap().unwrap();\n        let platform = test_project.platform().unwrap();\n\n        assert_eq!(platform.node, \"6.11.1\".parse().unwrap());\n        assert_eq!(platform.npm, Some(\"3.10.10\".parse().unwrap()));\n        assert_eq!(platform.yarn, Some(\"1.2.0\".parse().unwrap()));\n    }\n\n    #[test]\n    fn platform_workspace() {\n        let project_path = fixture_path(&[\"nested\", \"subproject\", \"inner_project\"]);\n        let test_project = Project::for_dir(project_path).unwrap().unwrap();\n        let platform = test_project.platform().unwrap();\n\n        // From the top level `nested/package.json`\n        assert_eq!(platform.node, \"12.14.0\".parse().unwrap());\n        // From the middle project `nested/subproject/package.json`\n        assert_eq!(platform.npm, Some(\"6.9.0\".parse().unwrap()));\n        // From the innermost project `nested/subproject/inner_project/package.json`\n        assert_eq!(platform.yarn, Some(\"1.22.4\".parse().unwrap()));\n    }\n\n    #[test]\n    fn direct_dependencies_single() {\n        let project_path = fixture_path(&[\"basic\"]);\n        let test_project = Project::for_dir(project_path).unwrap().unwrap();\n\n        // eslint, rsvp, bin-1, and bin-2 are direct dependencies\n        assert!(test_project.has_direct_dependency(\"eslint\"));\n        assert!(test_project.has_direct_dependency(\"rsvp\"));\n        assert!(test_project.has_direct_dependency(\"@namespace/some-dep\"));\n        assert!(test_project.has_direct_dependency(\"@namespaced/something-else\"));\n\n        // typescript is not a direct dependency\n        assert!(!test_project.has_direct_dependency(\"typescript\"));\n    }\n\n    #[test]\n    fn direct_dependencies_workspace() {\n        let project_path = fixture_path(&[\"nested\", \"subproject\", \"inner_project\"]);\n        let test_project = Project::for_dir(project_path).unwrap().unwrap();\n\n        // express and typescript are direct dependencies of the innermost project\n        assert!(test_project.has_direct_dependency(\"express\"));\n        assert!(test_project.has_direct_dependency(\"typescript\"));\n        // rsvp and glob are direct dependencies of the middle project\n        assert!(test_project.has_direct_dependency(\"rsvp\"));\n        assert!(test_project.has_direct_dependency(\"glob\"));\n        // lodash and eslint are direct dependencies of the top-level workspace\n        assert!(test_project.has_direct_dependency(\"lodash\"));\n        assert!(test_project.has_direct_dependency(\"eslint\"));\n\n        // react is not a direct dependency of any project\n        assert!(!test_project.has_direct_dependency(\"react\"));\n    }\n\n    #[test]\n    fn find_bin_single() {\n        let project_path = fixture_path(&[\"basic\"]);\n        let test_project = Project::for_dir(project_path).unwrap().unwrap();\n\n        assert_eq!(\n            test_project.find_bin(\"rsvp\"),\n            Some(fixture_path(&[\"basic\", \"node_modules\", \".bin\", \"rsvp\"]))\n        );\n\n        assert!(test_project.find_bin(\"eslint\").is_none());\n    }\n\n    #[test]\n    fn find_bin_workspace() {\n        // eslint, rsvp, tsc\n        let project_path = fixture_path(&[\"nested\", \"subproject\", \"inner_project\"]);\n        let test_project = Project::for_dir(project_path).unwrap().unwrap();\n\n        // eslint is a binary in the root workspace\n        assert_eq!(\n            test_project.find_bin(\"eslint\"),\n            Some(fixture_path(&[\"nested\", \"node_modules\", \".bin\", \"eslint\"]))\n        );\n\n        // rsvp is a binary in the middle project\n        assert_eq!(\n            test_project.find_bin(\"rsvp\"),\n            Some(fixture_path(&[\n                \"nested\",\n                \"subproject\",\n                \"node_modules\",\n                \".bin\",\n                \"rsvp\"\n            ]))\n        );\n\n        // tsc is a binary in the inner project\n        assert_eq!(\n            test_project.find_bin(\"tsc\"),\n            Some(fixture_path(&[\n                \"nested\",\n                \"subproject\",\n                \"inner_project\",\n                \"node_modules\",\n                \".bin\",\n                \"tsc\"\n            ]))\n        );\n\n        assert!(test_project.find_bin(\"ember\").is_none());\n    }\n\n    #[test]\n    fn detects_workspace_cycles() {\n        // cycle-1 has a cycle with the original package.json\n        let cycle_path = fixture_path(&[\"cycle-1\"]);\n        let project_error = Project::for_dir(cycle_path).unwrap_err();\n\n        match project_error.kind() {\n            ErrorKind::ExtensionCycleError { paths, duplicate } => {\n                let expected_paths = vec![\n                    fixture_path(&[\"cycle-1\", \"package.json\"]),\n                    fixture_path(&[\"cycle-1\", \"volta.json\"]),\n                ];\n                assert_eq!(&expected_paths, paths);\n                assert_eq!(&expected_paths[0], duplicate);\n            }\n            kind => panic!(\"Wrong error kind: {:?}\", kind),\n        }\n\n        // cycle-2 has a cycle with 2 separate extensions, not including the original package.json\n        let cycle_path = fixture_path(&[\"cycle-2\"]);\n        let project_error = Project::for_dir(cycle_path).unwrap_err();\n\n        match project_error.kind() {\n            ErrorKind::ExtensionCycleError { paths, duplicate } => {\n                let expected_paths = vec![\n                    fixture_path(&[\"cycle-2\", \"package.json\"]),\n                    fixture_path(&[\"cycle-2\", \"workspace-1.json\"]),\n                    fixture_path(&[\"cycle-2\", \"workspace-2.json\"]),\n                ];\n                assert_eq!(&expected_paths, paths);\n                assert_eq!(&expected_paths[1], duplicate);\n            }\n            kind => panic!(\"Wrong error kind: {:?}\", kind),\n        }\n    }\n}\n\nmod needs_yarn_run {\n    use super::*;\n\n    #[test]\n    fn project_does_not_need_yarn_run() {\n        let project_path = fixture_path(&[\"basic\"]);\n        let test_project = Project::for_dir(project_path).unwrap().unwrap();\n        assert!(!test_project.needs_yarn_run());\n    }\n\n    #[test]\n    fn project_has_yarnrc_yml() {\n        let project_path = fixture_path(&[\"yarn\", \"yarnrc-yml\"]);\n        let test_project = Project::for_dir(project_path).unwrap().unwrap();\n        assert!(test_project.needs_yarn_run());\n    }\n\n    #[test]\n    fn project_has_pnp_js() {\n        let project_path = fixture_path(&[\"yarn\", \"pnp-js\"]);\n        let test_project = Project::for_dir(project_path).unwrap().unwrap();\n        assert!(test_project.needs_yarn_run());\n    }\n\n    #[test]\n    fn project_has_pnp_cjs() {\n        let project_path = fixture_path(&[\"yarn\", \"pnp-cjs\"]);\n        let test_project = Project::for_dir(project_path).unwrap().unwrap();\n        assert!(test_project.needs_yarn_run());\n    }\n}\n"
  },
  {
    "path": "crates/volta-core/src/run/binary.rs",
    "content": "use std::env;\nuse std::ffi::{OsStr, OsString};\nuse std::path::PathBuf;\n\nuse super::executor::{Executor, ToolCommand, ToolKind};\nuse super::{debug_active_image, debug_no_platform};\nuse crate::error::{Context, ErrorKind, Fallible};\nuse crate::layout::volta_home;\nuse crate::platform::{Platform, Sourced, System};\nuse crate::session::Session;\nuse crate::tool::package::BinConfig;\nuse log::debug;\n\n/// Determine the correct command to run for a 3rd-party binary\n///\n/// Will detect if we should delegate to the project-local version or use the default version\npub(super) fn command(exe: &OsStr, args: &[OsString], session: &mut Session) -> Fallible<Executor> {\n    let bin = exe.to_string_lossy().to_string();\n    // First try to use the project toolchain\n    if let Some(project) = session.project()? {\n        // Check if the executable is a direct dependency\n        if project.has_direct_bin(exe)? {\n            match project.find_bin(exe) {\n                Some(path_to_bin) => {\n                    debug!(\"Found {} in project at '{}'\", bin, path_to_bin.display());\n\n                    let platform = Platform::current(session)?;\n                    return Ok(ToolCommand::new(\n                        path_to_bin,\n                        args,\n                        platform,\n                        ToolKind::ProjectLocalBinary(bin),\n                    )\n                    .into());\n                }\n                None => {\n                    if project.needs_yarn_run() {\n                        debug!(\n                            \"Project needs to use yarn to run command, calling {} with 'yarn'\",\n                            bin\n                        );\n                        let platform = Platform::current(session)?;\n                        let mut exe_and_args = vec![exe.to_os_string()];\n                        exe_and_args.extend_from_slice(args);\n                        return Ok(ToolCommand::new(\n                            \"yarn\",\n                            exe_and_args,\n                            platform,\n                            ToolKind::Yarn,\n                        )\n                        .into());\n                    } else {\n                        return Err(ErrorKind::ProjectLocalBinaryNotFound {\n                            command: exe.to_string_lossy().to_string(),\n                        }\n                        .into());\n                    }\n                }\n            }\n        }\n    }\n\n    // Try to use the default toolchain\n    if let Some(default_tool) = DefaultBinary::from_name(exe, session)? {\n        debug!(\n            \"Found default {} in '{}'\",\n            bin,\n            default_tool.bin_path.display()\n        );\n\n        let mut command = ToolCommand::new(\n            default_tool.bin_path,\n            args,\n            Some(default_tool.platform),\n            ToolKind::DefaultBinary(bin),\n        );\n        command.env(\"NODE_PATH\", shared_module_path()?);\n\n        return Ok(command.into());\n    }\n\n    // At this point, the binary is not known to Volta, so we have no platform to use to execute it\n    // This should be rare, as anything we have a shim for should have a config file to load\n    Ok(ToolCommand::new(exe, args, None, ToolKind::DefaultBinary(bin)).into())\n}\n\n/// Determine the execution context (PATH and failure error message) for a project-local binary\npub(super) fn local_execution_context(\n    tool: String,\n    platform: Option<Platform>,\n    session: &mut Session,\n) -> Fallible<(OsString, ErrorKind)> {\n    match platform {\n        Some(plat) => {\n            let image = plat.checkout(session)?;\n            let path = image.path()?;\n            debug_active_image(&image);\n\n            Ok((\n                path,\n                ErrorKind::ProjectLocalBinaryExecError { command: tool },\n            ))\n        }\n        None => {\n            let path = System::path()?;\n            debug_no_platform();\n\n            Ok((path, ErrorKind::NoPlatform))\n        }\n    }\n}\n\n/// Determine the execution context (PATH and failure error message) for a default binary\npub(super) fn default_execution_context(\n    tool: String,\n    platform: Option<Platform>,\n    session: &mut Session,\n) -> Fallible<(OsString, ErrorKind)> {\n    match platform {\n        Some(plat) => {\n            let image = plat.checkout(session)?;\n            let path = image.path()?;\n            debug_active_image(&image);\n\n            Ok((path, ErrorKind::BinaryExecError))\n        }\n        None => {\n            let path = System::path()?;\n            debug_no_platform();\n\n            Ok((path, ErrorKind::BinaryNotFound { name: tool }))\n        }\n    }\n}\n\n/// Information about the location and execution context of default binaries\n///\n/// Fetched from the config files in the Volta directory, represents the binary that is executed\n/// when the user is outside of a project that has the given bin as a dependency.\npub struct DefaultBinary {\n    pub bin_path: PathBuf,\n    pub platform: Platform,\n}\n\nimpl DefaultBinary {\n    pub fn from_config(bin_config: BinConfig, session: &mut Session) -> Fallible<Self> {\n        let package_dir = volta_home()?.package_image_dir(&bin_config.package);\n        let mut bin_path = bin_config.manager.binary_dir(package_dir);\n        bin_path.push(&bin_config.name);\n\n        // If the user does not have yarn set in the platform for this binary, use the default\n        // This is necessary because some tools (e.g. ember-cli with the `--yarn` option) invoke `yarn`\n        let yarn = match bin_config.platform.yarn {\n            Some(yarn) => Some(yarn),\n            None => session\n                .default_platform()?\n                .and_then(|plat| plat.yarn.clone()),\n        };\n        let platform = Platform {\n            node: Sourced::with_binary(bin_config.platform.node),\n            npm: bin_config.platform.npm.map(Sourced::with_binary),\n            pnpm: bin_config.platform.pnpm.map(Sourced::with_binary),\n            yarn: yarn.map(Sourced::with_binary),\n        };\n\n        Ok(DefaultBinary { bin_path, platform })\n    }\n\n    /// Load information about a default binary by name, if available\n    ///\n    /// A `None` response here means that the tool information couldn't be found. Either the tool\n    /// name is not a valid UTF-8 string, or the tool config doesn't exist.\n    pub fn from_name(tool_name: &OsStr, session: &mut Session) -> Fallible<Option<Self>> {\n        let bin_config_file = match tool_name.to_str() {\n            Some(name) => volta_home()?.default_tool_bin_config(name),\n            None => return Ok(None),\n        };\n\n        match BinConfig::from_file_if_exists(bin_config_file)? {\n            Some(config) => DefaultBinary::from_config(config, session).map(Some),\n            None => Ok(None),\n        }\n    }\n}\n\n/// Determine the value for NODE_PATH, with the shared lib directory prepended\n///\n/// This will ensure that global bins can `require` other global libs\nfn shared_module_path() -> Fallible<OsString> {\n    let node_path = match env::var(\"NODE_PATH\") {\n        Ok(path) => envoy::Var::from(path),\n        Err(_) => envoy::Var::from(\"\"),\n    };\n\n    node_path\n        .split()\n        .prefix_entry(volta_home()?.shared_lib_root())\n        .join()\n        .with_context(|| ErrorKind::BuildPathError)\n}\n"
  },
  {
    "path": "crates/volta-core/src/run/executor.rs",
    "content": "use std::collections::HashMap;\nuse std::ffi::OsStr;\n#[cfg(unix)]\nuse std::os::unix::process::ExitStatusExt;\n#[cfg(windows)]\nuse std::os::windows::process::ExitStatusExt;\nuse std::process::{Command, ExitStatus};\n\nuse super::RECURSION_ENV_VAR;\nuse crate::command::create_command;\nuse crate::error::{Context, ErrorKind, Fallible};\nuse crate::layout::volta_home;\nuse crate::platform::{CliPlatform, Platform, System};\nuse crate::session::Session;\nuse crate::signal::pass_control_to_shim;\nuse crate::style::{note_prefix, tool_version};\nuse crate::sync::VoltaLock;\nuse crate::tool::package::{DirectInstall, InPlaceUpgrade, PackageConfig, PackageManager};\nuse crate::tool::Spec;\nuse log::{info, warn};\n\npub enum Executor {\n    Tool(Box<ToolCommand>),\n    PackageInstall(Box<PackageInstallCommand>),\n    PackageLink(Box<PackageLinkCommand>),\n    PackageUpgrade(Box<PackageUpgradeCommand>),\n    InternalInstall(Box<InternalInstallCommand>),\n    Uninstall(Box<UninstallCommand>),\n    Multiple(Vec<Executor>),\n}\n\nimpl Executor {\n    pub fn envs<K, V, S>(&mut self, envs: &HashMap<K, V, S>)\n    where\n        K: AsRef<OsStr>,\n        V: AsRef<OsStr>,\n    {\n        match self {\n            Executor::Tool(cmd) => cmd.envs(envs),\n            Executor::PackageInstall(cmd) => cmd.envs(envs),\n            Executor::PackageLink(cmd) => cmd.envs(envs),\n            Executor::PackageUpgrade(cmd) => cmd.envs(envs),\n            // Internal installs use Volta's logic and don't rely on the environment variables\n            Executor::InternalInstall(_) => {}\n            // Uninstalls use Volta's logic and don't rely on environment variables\n            Executor::Uninstall(_) => {}\n            Executor::Multiple(executors) => {\n                for exe in executors {\n                    exe.envs(envs);\n                }\n            }\n        }\n    }\n\n    pub fn cli_platform(&mut self, cli: CliPlatform) {\n        match self {\n            Executor::Tool(cmd) => cmd.cli_platform(cli),\n            Executor::PackageInstall(cmd) => cmd.cli_platform(cli),\n            Executor::PackageLink(cmd) => cmd.cli_platform(cli),\n            Executor::PackageUpgrade(cmd) => cmd.cli_platform(cli),\n            // Internal installs use Volta's logic and don't rely on the Node platform\n            Executor::InternalInstall(_) => {}\n            // Uninstall use Volta's logic and don't rely on the Node platform\n            Executor::Uninstall(_) => {}\n            Executor::Multiple(executors) => {\n                for exe in executors {\n                    exe.cli_platform(cli.clone());\n                }\n            }\n        }\n    }\n\n    pub fn execute(self, session: &mut Session) -> Fallible<ExitStatus> {\n        match self {\n            Executor::Tool(cmd) => cmd.execute(session),\n            Executor::PackageInstall(cmd) => cmd.execute(session),\n            Executor::PackageLink(cmd) => cmd.execute(session),\n            Executor::PackageUpgrade(cmd) => cmd.execute(session),\n            Executor::InternalInstall(cmd) => cmd.execute(session),\n            Executor::Uninstall(cmd) => cmd.execute(),\n            Executor::Multiple(executors) => {\n                info!(\n                    \"{} Volta is processing each package separately\",\n                    note_prefix()\n                );\n                for exe in executors {\n                    let status = exe.execute(session)?;\n                    // If any of the sub-commands fail, then we should stop installing and return\n                    // that failure.\n                    if !status.success() {\n                        return Ok(status);\n                    }\n                }\n                // If we get here, then all of the sub-commands succeeded, so we should report success\n                Ok(ExitStatus::from_raw(0))\n            }\n        }\n    }\n}\n\nimpl From<Vec<Executor>> for Executor {\n    fn from(mut executors: Vec<Executor>) -> Self {\n        if executors.len() == 1 {\n            executors.pop().unwrap()\n        } else {\n            Executor::Multiple(executors)\n        }\n    }\n}\n\n/// Process builder for launching a Volta-managed tool\n///\n/// Tracks the Platform as well as what kind of tool is being executed, to allow individual tools\n/// to customize the behavior before execution.\npub struct ToolCommand {\n    command: Command,\n    platform: Option<Platform>,\n    kind: ToolKind,\n}\n\n/// The kind of tool being executed, used to determine the correct execution context\npub enum ToolKind {\n    Node,\n    Npm,\n    Npx,\n    Pnpm,\n    Yarn,\n    ProjectLocalBinary(String),\n    DefaultBinary(String),\n    Bypass(String),\n}\n\nimpl ToolCommand {\n    pub fn new<E, A, S>(exe: E, args: A, platform: Option<Platform>, kind: ToolKind) -> Self\n    where\n        E: AsRef<OsStr>,\n        A: IntoIterator<Item = S>,\n        S: AsRef<OsStr>,\n    {\n        let mut command = create_command(exe);\n        command.args(args);\n\n        Self {\n            command,\n            platform,\n            kind,\n        }\n    }\n\n    /// Adds or updates environment variables that the command will use\n    pub fn envs<E, K, V>(&mut self, envs: E)\n    where\n        E: IntoIterator<Item = (K, V)>,\n        K: AsRef<OsStr>,\n        V: AsRef<OsStr>,\n    {\n        self.command.envs(envs);\n    }\n\n    /// Adds or updates a single environment variable that the command will use\n    pub fn env<K, V>(&mut self, key: K, value: V)\n    where\n        K: AsRef<OsStr>,\n        V: AsRef<OsStr>,\n    {\n        self.command.env(key, value);\n    }\n\n    /// Updates the Platform for the command to include values from the command-line\n    pub fn cli_platform(&mut self, cli: CliPlatform) {\n        self.platform = match self.platform.take() {\n            Some(base) => Some(cli.merge(base)),\n            None => cli.into(),\n        };\n    }\n\n    /// Runs the command, returning the `ExitStatus` if it successfully launches\n    pub fn execute(mut self, session: &mut Session) -> Fallible<ExitStatus> {\n        let (path, on_failure) = match self.kind {\n            ToolKind::Node => super::node::execution_context(self.platform, session)?,\n            ToolKind::Npm => super::npm::execution_context(self.platform, session)?,\n            ToolKind::Npx => super::npx::execution_context(self.platform, session)?,\n            ToolKind::Pnpm => super::pnpm::execution_context(self.platform, session)?,\n            ToolKind::Yarn => super::yarn::execution_context(self.platform, session)?,\n            ToolKind::DefaultBinary(bin) => {\n                super::binary::default_execution_context(bin, self.platform, session)?\n            }\n            ToolKind::ProjectLocalBinary(bin) => {\n                super::binary::local_execution_context(bin, self.platform, session)?\n            }\n            ToolKind::Bypass(command) => (System::path()?, ErrorKind::BypassError { command }),\n        };\n\n        self.command.env(RECURSION_ENV_VAR, \"1\");\n        self.command.env(\"PATH\", path);\n\n        pass_control_to_shim();\n        self.command.status().with_context(|| on_failure)\n    }\n}\n\nimpl From<ToolCommand> for Executor {\n    fn from(cmd: ToolCommand) -> Self {\n        Executor::Tool(Box::new(cmd))\n    }\n}\n\n/// Process builder for launching a package install command (e.g. `npm install --global`)\n///\n/// This will use a `DirectInstall` instance to modify the command before running to point it to\n/// the Volta directory. It will also complete the install, writing config files and shims\npub struct PackageInstallCommand {\n    /// The command that will ultimately be executed\n    command: Command,\n    /// The installer that modifies the command as necessary and provides the completion method\n    installer: DirectInstall,\n    /// The platform to use when running the command.\n    platform: Platform,\n}\n\nimpl PackageInstallCommand {\n    pub fn new<A, S>(args: A, platform: Platform, manager: PackageManager) -> Fallible<Self>\n    where\n        A: IntoIterator<Item = S>,\n        S: AsRef<OsStr>,\n    {\n        let installer = DirectInstall::new(manager)?;\n\n        let mut command = match manager {\n            PackageManager::Npm => create_command(\"npm\"),\n            PackageManager::Pnpm => create_command(\"pnpm\"),\n            PackageManager::Yarn => create_command(\"yarn\"),\n        };\n        command.args(args);\n\n        Ok(PackageInstallCommand {\n            command,\n            installer,\n            platform,\n        })\n    }\n\n    pub fn for_npm_link<A, S>(args: A, platform: Platform, name: String) -> Fallible<Self>\n    where\n        A: IntoIterator<Item = S>,\n        S: AsRef<OsStr>,\n    {\n        let installer = DirectInstall::with_name(PackageManager::Npm, name)?;\n\n        let mut command = create_command(\"npm\");\n        command.args(args);\n\n        Ok(PackageInstallCommand {\n            command,\n            installer,\n            platform,\n        })\n    }\n\n    /// Adds or updates environment variables that the command will use\n    pub fn envs<E, K, V>(&mut self, envs: E)\n    where\n        E: IntoIterator<Item = (K, V)>,\n        K: AsRef<OsStr>,\n        V: AsRef<OsStr>,\n    {\n        self.command.envs(envs);\n    }\n\n    /// Updates the Platform for the command to include values from the command-line\n    pub fn cli_platform(&mut self, cli: CliPlatform) {\n        self.platform = cli.merge(self.platform.clone());\n    }\n\n    /// Runs the install command, applying the necessary modifications to install into the Volta\n    /// data directory\n    pub fn execute(mut self, session: &mut Session) -> Fallible<ExitStatus> {\n        let _lock = VoltaLock::acquire();\n        let image = self.platform.checkout(session)?;\n        let path = image.path()?;\n\n        self.command.env(RECURSION_ENV_VAR, \"1\");\n        self.command.env(\"PATH\", path);\n        self.installer.setup_command(&mut self.command);\n\n        let status = self\n            .command\n            .status()\n            .with_context(|| ErrorKind::BinaryExecError)?;\n\n        if status.success() {\n            self.installer.complete_install(&image)?;\n        }\n\n        Ok(status)\n    }\n}\n\nimpl From<PackageInstallCommand> for Executor {\n    fn from(cmd: PackageInstallCommand) -> Self {\n        Executor::PackageInstall(Box::new(cmd))\n    }\n}\n\n/// Process builder for launching a `npm link <package>` command\n///\n/// This will set the appropriate environment variables to ensure that the linked package can be\n/// found.\npub struct PackageLinkCommand {\n    /// The command that will ultimately be executed\n    command: Command,\n    /// The tool the user wants to link\n    tool: String,\n    /// The platform to use when running the command\n    platform: Platform,\n}\n\nimpl PackageLinkCommand {\n    pub fn new<A, S>(args: A, platform: Platform, tool: String) -> Self\n    where\n        A: IntoIterator<Item = S>,\n        S: AsRef<OsStr>,\n    {\n        let mut command = create_command(\"npm\");\n        command.args(args);\n\n        PackageLinkCommand {\n            command,\n            tool,\n            platform,\n        }\n    }\n\n    /// Adds or updates environment variables that the command will use\n    pub fn envs<E, K, V>(&mut self, envs: E)\n    where\n        E: IntoIterator<Item = (K, V)>,\n        K: AsRef<OsStr>,\n        V: AsRef<OsStr>,\n    {\n        self.command.envs(envs);\n    }\n\n    /// Updates the Platform for the command to include values from the command-line\n    pub fn cli_platform(&mut self, cli: CliPlatform) {\n        self.platform = cli.merge(self.platform.clone());\n    }\n\n    /// Runs the link command, applying the necessary modifications to pull from the Volta data\n    /// directory.\n    ///\n    /// This will also check for some common failure cases and alert the user\n    pub fn execute(mut self, session: &mut Session) -> Fallible<ExitStatus> {\n        self.check_linked_package(session)?;\n\n        let image = self.platform.checkout(session)?;\n        let path = image.path()?;\n\n        self.command.env(RECURSION_ENV_VAR, \"1\");\n        self.command.env(\"PATH\", path);\n        let package_root = volta_home()?.package_image_dir(&self.tool);\n        PackageManager::Npm.setup_global_command(&mut self.command, package_root);\n\n        self.command\n            .status()\n            .with_context(|| ErrorKind::BinaryExecError)\n    }\n\n    /// Check for possible failure cases with the linked package:\n    ///     - The package is not found as a global\n    ///     - The package exists, but was linked using a different package manager\n    ///     - The package is using a different version of Node than the current project (warning)\n    fn check_linked_package(&self, session: &mut Session) -> Fallible<()> {\n        let config =\n            PackageConfig::from_file(volta_home()?.default_package_config_file(&self.tool))\n                .with_context(|| ErrorKind::NpmLinkMissingPackage {\n                    package: self.tool.clone(),\n                })?;\n\n        if config.manager != PackageManager::Npm {\n            return Err(ErrorKind::NpmLinkWrongManager {\n                package: self.tool.clone(),\n            }\n            .into());\n        }\n\n        if let Some(platform) = session.project_platform()? {\n            if platform.node.major != config.platform.node.major {\n                warn!(\n                    \"the current project is using {}, but package '{}' was linked using {}. These might not interact correctly.\",\n                    tool_version(\"node\", &platform.node),\n                    self.tool,\n                    tool_version(\"node\", &config.platform.node)\n                );\n            }\n        }\n\n        Ok(())\n    }\n}\n\nimpl From<PackageLinkCommand> for Executor {\n    fn from(cmd: PackageLinkCommand) -> Self {\n        Executor::PackageLink(Box::new(cmd))\n    }\n}\n\n/// Process builder for launching a global package upgrade command (e.g. `npm update -g`)\n///\n/// This will use an `InPlaceUpgrade` instance to modify the command and point at the appropriate\n/// image directory. It will also complete the install, writing any updated configs and shims\npub struct PackageUpgradeCommand {\n    /// The command that will ultimately be executed\n    command: Command,\n    /// Helper utility to modify the command and provide the completion method\n    upgrader: InPlaceUpgrade,\n    /// The platform to run the command under\n    platform: Platform,\n}\n\nimpl PackageUpgradeCommand {\n    pub fn new<A, S>(\n        args: A,\n        package: String,\n        platform: Platform,\n        manager: PackageManager,\n    ) -> Fallible<Self>\n    where\n        A: IntoIterator<Item = S>,\n        S: AsRef<OsStr>,\n    {\n        let upgrader = InPlaceUpgrade::new(package, manager)?;\n\n        let mut command = match manager {\n            PackageManager::Npm => create_command(\"npm\"),\n            PackageManager::Pnpm => create_command(\"pnpm\"),\n            PackageManager::Yarn => create_command(\"yarn\"),\n        };\n        command.args(args);\n\n        Ok(PackageUpgradeCommand {\n            command,\n            upgrader,\n            platform,\n        })\n    }\n\n    /// Adds or updates environment variables that the command will use\n    pub fn envs<E, K, V>(&mut self, envs: E)\n    where\n        E: IntoIterator<Item = (K, V)>,\n        K: AsRef<OsStr>,\n        V: AsRef<OsStr>,\n    {\n        self.command.envs(envs);\n    }\n\n    /// Updates the Platform for the command to include values from the command-line\n    pub fn cli_platform(&mut self, cli: CliPlatform) {\n        self.platform = cli.merge(self.platform.clone());\n    }\n\n    /// Runs the upgrade command, applying the necessary modifications to point at the Volta image\n    /// directory\n    ///\n    /// Will also check for common failure cases, such as non-existant package or wrong package\n    /// manager\n    pub fn execute(mut self, session: &mut Session) -> Fallible<ExitStatus> {\n        self.upgrader.check_upgraded_package()?;\n\n        let _lock = VoltaLock::acquire();\n        let image = self.platform.checkout(session)?;\n        let path = image.path()?;\n\n        self.command.env(RECURSION_ENV_VAR, \"1\");\n        self.command.env(\"PATH\", path);\n        self.upgrader.setup_command(&mut self.command);\n\n        let status = self\n            .command\n            .status()\n            .with_context(|| ErrorKind::BinaryExecError)?;\n\n        if status.success() {\n            self.upgrader.complete_upgrade(&image)?;\n        }\n\n        Ok(status)\n    }\n}\n\nimpl From<PackageUpgradeCommand> for Executor {\n    fn from(cmd: PackageUpgradeCommand) -> Self {\n        Executor::PackageUpgrade(Box::new(cmd))\n    }\n}\n\n/// Executor for running an internal install (installing Node, npm, pnpm or Yarn using the `volta\n/// install` logic)\n///\n/// Note: This is not intended to be used for Package installs. Those should go through the\n/// `PackageInstallCommand` above, to more seamlessly integrate with the package manager\npub struct InternalInstallCommand {\n    tool: Spec,\n}\n\nimpl InternalInstallCommand {\n    pub fn new(tool: Spec) -> Self {\n        InternalInstallCommand { tool }\n    }\n\n    /// Runs the install, using Volta's internal install logic for the appropriate tool\n    fn execute(self, session: &mut Session) -> Fallible<ExitStatus> {\n        info!(\n            \"{} using Volta to install {}\",\n            note_prefix(),\n            self.tool.name()\n        );\n\n        self.tool.resolve(session)?.install(session)?;\n\n        Ok(ExitStatus::from_raw(0))\n    }\n}\n\nimpl From<InternalInstallCommand> for Executor {\n    fn from(cmd: InternalInstallCommand) -> Self {\n        Executor::InternalInstall(Box::new(cmd))\n    }\n}\n\n/// Executor for running a tool uninstall command.\n///\n/// This will use the `volta uninstall` logic to correctly ensure that the package is fully\n/// uninstalled\npub struct UninstallCommand {\n    tool: Spec,\n}\n\nimpl UninstallCommand {\n    pub fn new(tool: Spec) -> Self {\n        UninstallCommand { tool }\n    }\n\n    /// Runs the uninstall with Volta's internal uninstall logic\n    fn execute(self) -> Fallible<ExitStatus> {\n        info!(\n            \"{} using Volta to uninstall {}\",\n            note_prefix(),\n            self.tool.name()\n        );\n\n        self.tool.uninstall()?;\n\n        Ok(ExitStatus::from_raw(0))\n    }\n}\n\nimpl From<UninstallCommand> for Executor {\n    fn from(cmd: UninstallCommand) -> Self {\n        Executor::Uninstall(Box::new(cmd))\n    }\n}\n"
  },
  {
    "path": "crates/volta-core/src/run/mod.rs",
    "content": "use std::collections::HashMap;\nuse std::env::{self, ArgsOs};\nuse std::ffi::{OsStr, OsString};\nuse std::path::Path;\nuse std::process::ExitStatus;\n\nuse crate::error::{ErrorKind, Fallible};\nuse crate::platform::{CliPlatform, Image, Sourced};\nuse crate::session::Session;\nuse crate::VOLTA_FEATURE_PNPM;\nuse log::debug;\nuse node_semver::Version;\n\npub mod binary;\nmod executor;\nmod node;\nmod npm;\nmod npx;\nmod parser;\nmod pnpm;\nmod yarn;\n\n/// Environment variable set internally when a shim has been executed and the context evaluated\n///\n/// This is set when executing a shim command. If this is already, then the built-in shims (Node,\n/// npm, npx, pnpm and Yarn) will assume that the context has already been evaluated & the PATH has\n/// already been modified, so they will use the pass-through behavior.\n///\n/// Shims should only be called recursively when the environment is misconfigured, so this will\n/// prevent infinite recursion as the pass-through logic removes the shim directory from the PATH.\n///\n/// Note: This is explicitly _removed_ when calling a command through `volta run`, as that will\n/// never happen due to the Volta environment.\nconst RECURSION_ENV_VAR: &str = \"_VOLTA_TOOL_RECURSION\";\nconst VOLTA_BYPASS: &str = \"VOLTA_BYPASS\";\n\n/// Execute a shim command, based on the command-line arguments to the current process\npub fn execute_shim(session: &mut Session) -> Fallible<ExitStatus> {\n    let mut native_args = env::args_os();\n    let exe = get_tool_name(&mut native_args)?;\n    let args: Vec<_> = native_args.collect();\n\n    get_executor(&exe, &args, session)?.execute(session)\n}\n\n/// Execute a tool with the provided arguments\npub fn execute_tool<K, V, S>(\n    exe: &OsStr,\n    args: &[OsString],\n    envs: &HashMap<K, V, S>,\n    cli: CliPlatform,\n    session: &mut Session,\n) -> Fallible<ExitStatus>\nwhere\n    K: AsRef<OsStr>,\n    V: AsRef<OsStr>,\n{\n    // Remove the recursion environment variable so that the context is correctly re-evaluated\n    // when calling `volta run` (even when called from a Node script)\n    env::remove_var(RECURSION_ENV_VAR);\n\n    let mut runner = get_executor(exe, args, session)?;\n    runner.cli_platform(cli);\n    runner.envs(envs);\n\n    runner.execute(session)\n}\n\n/// Get the appropriate Tool command, based on the requested executable and arguments\nfn get_executor(\n    exe: &OsStr,\n    args: &[OsString],\n    session: &mut Session,\n) -> Fallible<executor::Executor> {\n    if env::var_os(VOLTA_BYPASS).is_some() {\n        Ok(executor::ToolCommand::new(\n            exe,\n            args,\n            None,\n            executor::ToolKind::Bypass(exe.to_string_lossy().to_string()),\n        )\n        .into())\n    } else {\n        match exe.to_str() {\n            Some(\"volta-shim\") => Err(ErrorKind::RunShimDirectly.into()),\n            Some(\"node\") => node::command(args, session),\n            Some(\"npm\") => npm::command(args, session),\n            Some(\"npx\") => npx::command(args, session),\n            Some(\"pnpm\") => {\n                // If the pnpm feature flag variable is set, delegate to the pnpm handler\n                // If not, use the binary handler as a fallback (prior to pnpm support, installing\n                // pnpm would be handled the same as any other global binary)\n                if env::var_os(VOLTA_FEATURE_PNPM).is_some() {\n                    pnpm::command(args, session)\n                } else {\n                    binary::command(exe, args, session)\n                }\n            }\n            Some(\"yarn\") | Some(\"yarnpkg\") => yarn::command(args, session),\n            _ => binary::command(exe, args, session),\n        }\n    }\n}\n\n/// Determine the name of the command to run by inspecting the first argument to the active process\nfn get_tool_name(args: &mut ArgsOs) -> Fallible<OsString> {\n    args.next()\n        .and_then(|arg0| Path::new(&arg0).file_name().map(tool_name_from_file_name))\n        .ok_or_else(|| ErrorKind::CouldNotDetermineTool.into())\n}\n\n#[cfg(unix)]\nfn tool_name_from_file_name(file_name: &OsStr) -> OsString {\n    file_name.to_os_string()\n}\n\n#[cfg(windows)]\nfn tool_name_from_file_name(file_name: &OsStr) -> OsString {\n    // On Windows PowerShell, the file name includes the .exe suffix,\n    // and the Windows file system is case-insensitive\n    // We need to remove that to get the raw tool name\n    match file_name.to_str() {\n        Some(file) => OsString::from(file.to_ascii_lowercase().trim_end_matches(\".exe\")),\n        None => OsString::from(file_name),\n    }\n}\n\n/// Write a debug message that there is no platform available\n#[inline]\nfn debug_no_platform() {\n    debug!(\"Could not find Volta-managed platform, delegating to system\");\n}\n\n/// Write a debug message with the full image that will be used to execute a command\n#[inline]\nfn debug_active_image(image: &Image) {\n    debug!(\n        \"Active Image:\n    Node: {}\n    npm: {}\n    pnpm: {}\n    Yarn: {}\",\n        format_tool_version(&image.node),\n        image\n            .resolve_npm()\n            .ok()\n            .as_ref()\n            .map(format_tool_version)\n            .unwrap_or_else(|| \"Bundled with Node\".into()),\n        image\n            .pnpm\n            .as_ref()\n            .map(format_tool_version)\n            .unwrap_or_else(|| \"None\".into()),\n        image\n            .yarn\n            .as_ref()\n            .map(format_tool_version)\n            .unwrap_or_else(|| \"None\".into()),\n    )\n}\n\nfn format_tool_version(version: &Sourced<Version>) -> String {\n    format!(\"{} from {} configuration\", version.value, version.source)\n}\n"
  },
  {
    "path": "crates/volta-core/src/run/node.rs",
    "content": "use std::env;\nuse std::ffi::OsString;\n\nuse super::executor::{Executor, ToolCommand, ToolKind};\nuse super::{debug_active_image, debug_no_platform, RECURSION_ENV_VAR};\nuse crate::error::{ErrorKind, Fallible};\nuse crate::platform::{Platform, System};\nuse crate::session::{ActivityKind, Session};\n\n/// Build a `ToolCommand` for Node\npub(super) fn command(args: &[OsString], session: &mut Session) -> Fallible<Executor> {\n    session.add_event_start(ActivityKind::Node);\n    // Don't re-evaluate the platform if this is a recursive call\n    let platform = match env::var_os(RECURSION_ENV_VAR) {\n        Some(_) => None,\n        None => Platform::current(session)?,\n    };\n\n    Ok(ToolCommand::new(\"node\", args, platform, ToolKind::Node).into())\n}\n\n/// Determine the execution context (PATH and failure error message) for Node\npub(super) fn execution_context(\n    platform: Option<Platform>,\n    session: &mut Session,\n) -> Fallible<(OsString, ErrorKind)> {\n    match platform {\n        Some(plat) => {\n            let image = plat.checkout(session)?;\n            let path = image.path()?;\n            debug_active_image(&image);\n\n            Ok((path, ErrorKind::BinaryExecError))\n        }\n        None => {\n            let path = System::path()?;\n            debug_no_platform();\n            Ok((path, ErrorKind::NoPlatform))\n        }\n    }\n}\n"
  },
  {
    "path": "crates/volta-core/src/run/npm.rs",
    "content": "use std::env;\nuse std::ffi::OsString;\nuse std::fs::File;\n\nuse super::executor::{Executor, ToolCommand, ToolKind, UninstallCommand};\nuse super::parser::{CommandArg, InterceptedCommand};\nuse super::{debug_active_image, debug_no_platform, RECURSION_ENV_VAR};\nuse crate::error::{ErrorKind, Fallible};\nuse crate::platform::{Platform, System};\nuse crate::session::{ActivityKind, Session};\nuse crate::tool::{PackageManifest, Spec};\nuse crate::version::VersionSpec;\n\n/// Build an `Executor` for npm\n///\n/// If the command is a global install or uninstall and we have a default platform available, then\n/// we will use custom logic to ensure that the package is correctly installed / uninstalled in the\n/// Volta directory.\n///\n/// If the command is _not_ a global install / uninstall or we don't have a default platform, then\n/// we will allow npm to execute the command as usual.\npub(super) fn command(args: &[OsString], session: &mut Session) -> Fallible<Executor> {\n    session.add_event_start(ActivityKind::Npm);\n    // Don't re-evaluate the context or global install interception if this is a recursive call\n    let platform = match env::var_os(RECURSION_ENV_VAR) {\n        Some(_) => None,\n        None => {\n            match CommandArg::for_npm(args) {\n                CommandArg::Global(cmd) => {\n                    // For globals, only intercept if the default platform exists\n                    if let Some(default_platform) = session.default_platform()? {\n                        return cmd.executor(default_platform);\n                    }\n                }\n                CommandArg::Intercepted(InterceptedCommand::Link(link)) => {\n                    // For link commands, only intercept if a platform exists\n                    if let Some(platform) = Platform::current(session)? {\n                        return link.executor(platform, current_project_name(session));\n                    }\n                }\n                CommandArg::Intercepted(InterceptedCommand::Unlink) => {\n                    // For unlink, attempt to find the current project name. If successful, treat\n                    // this as a global uninstall of the current project.\n                    if let Some(name) = current_project_name(session) {\n                        // Same as for link, only intercept if a platform exists\n                        if Platform::current(session)?.is_some() {\n                            return Ok(UninstallCommand::new(Spec::Package(\n                                name,\n                                VersionSpec::None,\n                            ))\n                            .into());\n                        }\n                    }\n                }\n                _ => {}\n            }\n\n            Platform::current(session)?\n        }\n    };\n\n    Ok(ToolCommand::new(\"npm\", args, platform, ToolKind::Npm).into())\n}\n\n/// Determine the execution context (PATH and failure error message) for npm\npub(super) fn execution_context(\n    platform: Option<Platform>,\n    session: &mut Session,\n) -> Fallible<(OsString, ErrorKind)> {\n    match platform {\n        Some(plat) => {\n            let image = plat.checkout(session)?;\n            let path = image.path()?;\n            debug_active_image(&image);\n\n            Ok((path, ErrorKind::BinaryExecError))\n        }\n        None => {\n            let path = System::path()?;\n            debug_no_platform();\n            Ok((path, ErrorKind::NoPlatform))\n        }\n    }\n}\n\n/// Determine the name of the current project, if possible\nfn current_project_name(session: &mut Session) -> Option<String> {\n    let project = session.project().ok()??;\n    let manifest_file = File::open(project.manifest_file()).ok()?;\n    let manifest: PackageManifest = serde_json::de::from_reader(manifest_file).ok()?;\n\n    Some(manifest.name)\n}\n"
  },
  {
    "path": "crates/volta-core/src/run/npx.rs",
    "content": "use std::env;\nuse std::ffi::OsString;\n\nuse super::executor::{Executor, ToolCommand, ToolKind};\nuse super::{debug_active_image, debug_no_platform, RECURSION_ENV_VAR};\nuse crate::error::{ErrorKind, Fallible};\nuse crate::platform::{Platform, System};\nuse crate::session::{ActivityKind, Session};\nuse node_semver::Version;\nuse once_cell::sync::Lazy;\n\nstatic REQUIRED_NPM_VERSION: Lazy<Version> = Lazy::new(|| Version {\n    major: 5,\n    minor: 2,\n    patch: 0,\n    build: vec![],\n    pre_release: vec![],\n});\n\n/// Build a `ToolCommand` for npx\npub(super) fn command(args: &[OsString], session: &mut Session) -> Fallible<Executor> {\n    session.add_event_start(ActivityKind::Npx);\n    // Don't re-evaluate the context if this is a recursive call\n    let platform = match env::var_os(RECURSION_ENV_VAR) {\n        Some(_) => None,\n        None => Platform::current(session)?,\n    };\n\n    Ok(ToolCommand::new(\"npx\", args, platform, ToolKind::Npx).into())\n}\n\n/// Determine the execution context (PATH and failure error message) for npx\npub(super) fn execution_context(\n    platform: Option<Platform>,\n    session: &mut Session,\n) -> Fallible<(OsString, ErrorKind)> {\n    match platform {\n        Some(plat) => {\n            let image = plat.checkout(session)?;\n\n            // If the npm version is lower than the minimum required, we can show a helpful error\n            // message instead of a 'command not found' error.\n            let active_npm = image.resolve_npm()?;\n            if active_npm.value < *REQUIRED_NPM_VERSION {\n                return Err(ErrorKind::NpxNotAvailable {\n                    version: active_npm.value.to_string(),\n                }\n                .into());\n            }\n\n            let path = image.path()?;\n            debug_active_image(&image);\n\n            Ok((path, ErrorKind::BinaryExecError))\n        }\n        None => {\n            let path = System::path()?;\n            debug_no_platform();\n            Ok((path, ErrorKind::NoPlatform))\n        }\n    }\n}\n"
  },
  {
    "path": "crates/volta-core/src/run/parser.rs",
    "content": "use std::env;\nuse std::ffi::OsStr;\nuse std::iter::once;\n\nuse super::executor::{\n    Executor, InternalInstallCommand, PackageInstallCommand, PackageLinkCommand,\n    PackageUpgradeCommand, UninstallCommand,\n};\nuse crate::error::{ErrorKind, Fallible};\nuse crate::inventory::package_configs;\nuse crate::platform::{Platform, PlatformSpec};\nuse crate::tool::package::PackageManager;\nuse crate::tool::Spec;\nuse log::debug;\n\nconst UNSAFE_GLOBAL: &str = \"VOLTA_UNSAFE_GLOBAL\";\n/// Aliases that npm supports for the 'install' command\nconst NPM_INSTALL_ALIASES: [&str; 12] = [\n    \"i\", \"in\", \"ins\", \"inst\", \"insta\", \"instal\", \"install\", \"isnt\", \"isnta\", \"isntal\", \"isntall\",\n    \"add\",\n];\n/// Aliases that npm supports for the 'uninstall' command\nconst NPM_UNINSTALL_ALIASES: [&str; 5] = [\"un\", \"uninstall\", \"remove\", \"rm\", \"r\"];\n/// Aliases that npm supports for the 'link' command\nconst NPM_LINK_ALIASES: [&str; 2] = [\"link\", \"ln\"];\n/// Aliases that npm supports for the `update` command\nconst NPM_UPDATE_ALIASES: [&str; 4] = [\"update\", \"udpate\", \"upgrade\", \"up\"];\n/// Aliases that pnpm supports for the 'remove' command,\n/// see: https://pnpm.io/cli/remove\nconst PNPM_UNINSTALL_ALIASES: [&str; 4] = [\"remove\", \"uninstall\", \"rm\", \"un\"];\n/// Aliases that pnpm supports for the 'update' command,\n/// see: https://pnpm.io/cli/update\nconst PNPM_UPDATE_ALIASES: [&str; 3] = [\"update\", \"upgrade\", \"up\"];\n/// Aliases that pnpm supports for the 'link' command\n/// see: https://pnpm.io/cli/link\nconst PNPM_LINK_ALIASES: [&str; 2] = [\"link\", \"ln\"];\n\npub enum CommandArg<'a> {\n    Global(GlobalCommand<'a>),\n    Intercepted(InterceptedCommand<'a>),\n    Standard,\n}\n\nimpl<'a> CommandArg<'a> {\n    /// Parse the given set of arguments to see if they correspond to an intercepted npm command\n    pub fn for_npm<S>(args: &'a [S]) -> Self\n    where\n        S: AsRef<OsStr>,\n    {\n        // If VOLTA_UNSAFE_GLOBAL is set, then we always skip any interception parsing\n        if env::var_os(UNSAFE_GLOBAL).is_some() {\n            return CommandArg::Standard;\n        }\n\n        let mut positionals = args.iter().filter(is_positional).map(AsRef::as_ref);\n\n        // The first positional argument will always be the command, however npm supports multiple\n        // aliases for commands (see https://github.com/npm/cli/blob/latest/lib/utils/cmd-list.js)\n        // Additionally, if we have a global install or uninstall, all of the remaining positional\n        // arguments will be the tools to install or uninstall. If there are _no_ other arguments,\n        // then we treat the command not a global and allow npm to handle any error messages.\n        match positionals.next() {\n            Some(cmd) if NPM_INSTALL_ALIASES.iter().any(|a| a == &cmd) => {\n                if has_global_without_prefix(args) {\n                    let tools: Vec<_> = positionals.collect();\n\n                    if tools.is_empty() {\n                        CommandArg::Standard\n                    } else {\n                        // The common args for an install should be the command combined with any flags\n                        let mut common_args = vec![cmd];\n                        common_args.extend(args.iter().filter(is_flag).map(AsRef::as_ref));\n\n                        CommandArg::Global(GlobalCommand::Install(InstallArgs {\n                            manager: PackageManager::Npm,\n                            common_args,\n                            tools,\n                        }))\n                    }\n                } else {\n                    CommandArg::Standard\n                }\n            }\n            Some(cmd) if NPM_UNINSTALL_ALIASES.iter().any(|a| a == &cmd) => {\n                if has_global_without_prefix(args) {\n                    let tools: Vec<_> = positionals.collect();\n\n                    if tools.is_empty() {\n                        CommandArg::Standard\n                    } else {\n                        CommandArg::Global(GlobalCommand::Uninstall(UninstallArgs { tools }))\n                    }\n                } else {\n                    CommandArg::Standard\n                }\n            }\n            Some(cmd) if cmd == \"unlink\" => {\n                let tools: Vec<_> = positionals.collect();\n\n                if tools.is_empty() {\n                    // `npm unlink` without any arguments is used to unlink the current project\n                    CommandArg::Intercepted(InterceptedCommand::Unlink)\n                } else if has_global_without_prefix(args) {\n                    // With arguments, `npm unlink` is an alias of `npm remove`\n                    CommandArg::Global(GlobalCommand::Uninstall(UninstallArgs { tools }))\n                } else {\n                    CommandArg::Standard\n                }\n            }\n            Some(cmd) if NPM_LINK_ALIASES.iter().any(|a| a == &cmd) => {\n                // Much like install, the common args for a link are the command combined with any flags\n                let mut common_args = vec![cmd];\n                common_args.extend(args.iter().filter(is_flag).map(AsRef::as_ref));\n                let tools: Vec<_> = positionals.collect();\n\n                CommandArg::Intercepted(InterceptedCommand::Link(LinkArgs { common_args, tools }))\n            }\n            Some(cmd) if NPM_UPDATE_ALIASES.iter().any(|a| a == &cmd) => {\n                if has_global_without_prefix(args) {\n                    // Once again, the common args are the command combined with any flags\n                    let mut common_args = vec![cmd];\n                    common_args.extend(args.iter().filter(is_flag).map(AsRef::as_ref));\n                    let tools: Vec<_> = positionals.collect();\n\n                    CommandArg::Global(GlobalCommand::Upgrade(UpgradeArgs {\n                        common_args,\n                        tools,\n                        manager: PackageManager::Npm,\n                    }))\n                } else {\n                    CommandArg::Standard\n                }\n            }\n            _ => CommandArg::Standard,\n        }\n    }\n\n    /// Parse the given set of arguments to see if they correspond to an intercepted pnpm command\n    #[allow(dead_code)]\n    pub fn for_pnpm<S>(args: &'a [S]) -> CommandArg<'a>\n    where\n        S: AsRef<OsStr>,\n    {\n        // If VOLTA_UNSAFE_GLOBAL is set, then we always skip any global parsing\n        if env::var_os(UNSAFE_GLOBAL).is_some() {\n            return CommandArg::Standard;\n        }\n\n        let (flags, positionals): (Vec<&OsStr>, Vec<&OsStr>) =\n            args.iter().map(AsRef::<OsStr>::as_ref).partition(is_flag);\n\n        // The first positional argument will always be the subcommand for pnpm\n        match positionals.split_first() {\n            None => CommandArg::Standard,\n            Some((&subcommand, tools)) => {\n                let is_global = flags.iter().any(|&f| f == \"--global\" || f == \"-g\");\n                // Do not intercept if a custom global dir is explicitly specified\n                // See: https://pnpm.io/npmrc#global-dir\n                let prefixed = flags.iter().any(|&f| f == \"--global-dir\");\n\n                // pnpm subcommands that support the `global` flag:\n                // `add`, `update`, `remove`, `link`, `list`, `outdated`,\n                // `why`, `env`, `root`, `bin`.\n                match is_global && !prefixed {\n                    false => CommandArg::Standard,\n                    true => match subcommand.to_str() {\n                        // `add`\n                        Some(\"add\") => {\n                            let manager = PackageManager::Pnpm;\n                            let mut common_args = vec![subcommand];\n                            common_args.extend(flags);\n\n                            CommandArg::Global(GlobalCommand::Install(InstallArgs {\n                                manager,\n                                common_args,\n                                tools: tools.to_vec(),\n                            }))\n                        }\n                        // `update`\n                        Some(cmd) if PNPM_UPDATE_ALIASES.iter().any(|&a| a == cmd) => {\n                            let manager = PackageManager::Pnpm;\n                            let mut common_args = vec![subcommand];\n                            common_args.extend(flags);\n                            CommandArg::Global(GlobalCommand::Upgrade(UpgradeArgs {\n                                manager,\n                                common_args,\n                                tools: tools.to_vec(),\n                            }))\n                        }\n                        // `remove`\n                        Some(cmd) if PNPM_UNINSTALL_ALIASES.iter().any(|&a| a == cmd) => {\n                            CommandArg::Global(GlobalCommand::Uninstall(UninstallArgs {\n                                tools: tools.to_vec(),\n                            }))\n                        }\n                        // `link`\n                        Some(cmd) if PNPM_LINK_ALIASES.iter().any(|&a| a == cmd) => {\n                            let mut common_args = vec![subcommand];\n                            common_args.extend(flags);\n                            CommandArg::Intercepted(InterceptedCommand::Link(LinkArgs {\n                                common_args,\n                                tools: tools.to_vec(),\n                            }))\n                        }\n                        _ => CommandArg::Standard,\n                    },\n                }\n            }\n        }\n    }\n\n    /// Parse the given set of arguments to see if they correspond to an intercepted Yarn command\n    pub fn for_yarn<S>(args: &'a [S]) -> Self\n    where\n        S: AsRef<OsStr>,\n    {\n        // If VOLTA_UNSAFE_GLOBAL is set, then we always skip any global parsing\n        if env::var_os(UNSAFE_GLOBAL).is_some() {\n            return CommandArg::Standard;\n        }\n\n        let mut positionals = args.iter().filter(is_positional).map(AsRef::as_ref);\n\n        // Yarn globals must always start with `global <command>`\n        // If we have a global add, remove, or upgrade, then all of the remaining positional\n        // arguments will be the tools to modify. As with npm, if there are no arguments then we\n        // can treat it as if it's not a global command and allow Yarn to show any errors.\n        match (positionals.next(), positionals.next()) {\n            (Some(global), Some(add)) if global == \"global\" && add == \"add\" => {\n                let tools: Vec<_> = positionals.collect();\n\n                if tools.is_empty() {\n                    CommandArg::Standard\n                } else {\n                    // The common args for an install should be `global add` and any flags\n                    let mut common_args = vec![global, add];\n                    common_args.extend(args.iter().filter(is_flag).map(AsRef::as_ref));\n\n                    CommandArg::Global(GlobalCommand::Install(InstallArgs {\n                        manager: PackageManager::Yarn,\n                        common_args,\n                        tools,\n                    }))\n                }\n            }\n            (Some(global), Some(remove)) if global == \"global\" && remove == \"remove\" => {\n                let tools: Vec<_> = positionals.collect();\n\n                if tools.is_empty() {\n                    CommandArg::Standard\n                } else {\n                    CommandArg::Global(GlobalCommand::Uninstall(UninstallArgs { tools }))\n                }\n            }\n            (Some(global), Some(upgrade)) if global == \"global\" && upgrade == \"upgrade\" => {\n                // The common args for an upgrade should be `global upgrade` and any flags\n                let mut common_args = vec![global, upgrade];\n                common_args.extend(args.iter().filter(is_flag).map(AsRef::as_ref));\n\n                CommandArg::Global(GlobalCommand::Upgrade(UpgradeArgs {\n                    common_args,\n                    tools: positionals.collect(),\n                    manager: PackageManager::Yarn,\n                }))\n            }\n            _ => CommandArg::Standard,\n        }\n    }\n}\n\npub enum GlobalCommand<'a> {\n    Install(InstallArgs<'a>),\n    Uninstall(UninstallArgs<'a>),\n    Upgrade(UpgradeArgs<'a>),\n}\n\nimpl GlobalCommand<'_> {\n    pub fn executor(self, platform: &PlatformSpec) -> Fallible<Executor> {\n        match self {\n            GlobalCommand::Install(cmd) => cmd.executor(platform),\n            GlobalCommand::Uninstall(cmd) => cmd.executor(),\n            GlobalCommand::Upgrade(cmd) => cmd.executor(platform),\n        }\n    }\n}\n\n/// The arguments passed to a global install command\npub struct InstallArgs<'a> {\n    /// The package manager being used\n    manager: PackageManager,\n    /// Common arguments that apply to each tool (e.g. flags)\n    common_args: Vec<&'a OsStr>,\n    /// The individual tool arguments\n    tools: Vec<&'a OsStr>,\n}\n\nimpl InstallArgs<'_> {\n    /// Convert these global install arguments into an executor for the command\n    ///\n    /// If there are multiple packages specified to install, then they will be broken out into\n    /// individual commands and run separately. That allows us to keep Volta's sandboxing for each\n    /// package while still supporting the ability to install multiple packages at once.\n    pub fn executor(self, platform_spec: &PlatformSpec) -> Fallible<Executor> {\n        let mut executors = Vec::with_capacity(self.tools.len());\n\n        for tool in self.tools {\n            // External tool installs may be in a form that doesn't match a `Spec` (such as a git\n            // URL or path to a tarball). If parsing into a `Spec` fails, we assume that it's a\n            // 3rd-party Tool and attempt to install anyway.\n            match Spec::try_from_str(&tool.to_string_lossy()) {\n                Ok(Spec::Package(_, _)) | Err(_) => {\n                    let platform = platform_spec.as_default();\n                    // The args for an individual install command are the common args combined\n                    // with the name of the tool.\n                    let args = self.common_args.iter().chain(once(&tool));\n                    let command = PackageInstallCommand::new(args, platform, self.manager)?;\n                    executors.push(command.into());\n                }\n                Ok(internal) => executors.push(InternalInstallCommand::new(internal).into()),\n            }\n        }\n\n        Ok(executors.into())\n    }\n}\n\n/// The list of tools passed to an uninstall command\npub struct UninstallArgs<'a> {\n    tools: Vec<&'a OsStr>,\n}\n\nimpl UninstallArgs<'_> {\n    /// Convert the tools into an executor for the uninstall command\n    ///\n    /// Since the packages are sandboxed, each needs to be uninstalled separately\n    pub fn executor(self) -> Fallible<Executor> {\n        let mut executors = Vec::with_capacity(self.tools.len());\n\n        for tool_name in self.tools {\n            let tool = Spec::try_from_str(&tool_name.to_string_lossy())?;\n            executors.push(UninstallCommand::new(tool).into());\n        }\n\n        Ok(executors.into())\n    }\n}\n\n/// The list of tools passed to an upgrade command\npub struct UpgradeArgs<'a> {\n    /// The package manager being used\n    manager: PackageManager,\n    /// Common arguments that apply to each tool (e.g. flags)\n    common_args: Vec<&'a OsStr>,\n    /// The individual tool arguments\n    tools: Vec<&'a OsStr>,\n}\n\nimpl UpgradeArgs<'_> {\n    /// Convert these global upgrade arguments into an executor for the command\n    ///\n    /// If there are multiple packages specified to upgrade, then they will be broken out into\n    /// individual commands and run separately. If no packages are specified, then we will upgrade\n    /// _all_ installed packages that were installed with the same package manager.\n    pub fn executor(self, platform_spec: &PlatformSpec) -> Fallible<Executor> {\n        if self.tools.is_empty() {\n            return self.executor_all_packages(platform_spec);\n        }\n\n        let mut executors = Vec::with_capacity(self.tools.len());\n\n        for tool in self.tools {\n            match Spec::try_from_str(&tool.to_string_lossy()) {\n                Ok(Spec::Package(package, _)) => {\n                    let platform = platform_spec.as_default();\n                    let args = self.common_args.iter().chain(once(&tool));\n                    executors.push(\n                        PackageUpgradeCommand::new(args, package, platform, self.manager)?.into(),\n                    );\n                }\n                Ok(internal) => {\n                    executors.push(UninstallCommand::new(internal).into());\n                }\n                Err(_) => {\n                    return Err(ErrorKind::UpgradePackageNotFound {\n                        package: tool.to_string_lossy().to_string(),\n                        manager: self.manager,\n                    }\n                    .into())\n                }\n            }\n        }\n\n        Ok(executors.into())\n    }\n\n    /// Build an executor to upgrade _all_ global packages that were installed with the same\n    /// package manager as we are currently running.\n    fn executor_all_packages(self, platform_spec: &PlatformSpec) -> Fallible<Executor> {\n        package_configs()?\n            .into_iter()\n            .filter(|config| config.manager == self.manager)\n            .map(|config| {\n                let platform = platform_spec.as_default();\n                let package_name = config.name.as_ref();\n                let args = self.common_args.iter().chain(once(&package_name));\n\n                let executor =\n                    PackageUpgradeCommand::new(args, config.name.clone(), platform, self.manager)?\n                        .into();\n                Ok(executor)\n            })\n            .collect::<Fallible<Vec<_>>>()\n            .map(Into::into)\n    }\n}\n\n/// An intercepted local command\npub enum InterceptedCommand<'a> {\n    Link(LinkArgs<'a>),\n    Unlink,\n}\n\n/// The arguments passed to an `npm link` command\npub struct LinkArgs<'a> {\n    /// The common arguments that apply to each tool\n    common_args: Vec<&'a OsStr>,\n    /// The list of tools to link (if any)\n    tools: Vec<&'a OsStr>,\n}\n\nimpl LinkArgs<'_> {\n    pub fn executor(self, platform: Platform, project_name: Option<String>) -> Fallible<Executor> {\n        if self.tools.is_empty() {\n            // If no tools are specified, then this is a bare link command, linking the current\n            // project as a global package. We treat this like a global install except we look up\n            // the name from the current directory first.\n            match project_name {\n                Some(name) => PackageInstallCommand::for_npm_link(self.common_args, platform, name),\n                None => PackageInstallCommand::new(self.common_args, platform, PackageManager::Npm),\n            }\n            .map(Into::into)\n        } else {\n            // If there are tools specified, then this represents a command to link a global\n            // package into the current project. We handle each tool separately to support Volta's\n            // package sandboxing.\n            let common_args = self.common_args;\n\n            Ok(self\n                .tools\n                .into_iter()\n                .map(|tool| {\n                    let args = common_args.iter().chain(once(&tool));\n                    PackageLinkCommand::new(\n                        args,\n                        platform.clone(),\n                        tool.to_string_lossy().to_string(),\n                    )\n                    .into()\n                })\n                .collect::<Vec<_>>()\n                .into())\n        }\n    }\n}\n\n/// Check if the provided argument list includes a global flag and _doesn't_ have a prefix setting\n///\n/// For our interception, we only want to intercept global commands. Additionally, if the user\n/// passes a prefix setting, that will override the logic we use to redirect the install, so our\n/// process won't work and will cause an error. We should avoid intercepting in those cases since\n/// a command with an explicit prefix is something beyond the \"standard\" global install anyway.\nfn has_global_without_prefix<A>(args: &[A]) -> bool\nwhere\n    A: AsRef<OsStr>,\n{\n    let (has_global, has_prefix) = args.iter().fold((false, false), |(global, prefix), arg| {\n        match arg.as_ref().to_str() {\n            Some(\"-g\") | Some(\"--global\") => (true, prefix),\n            Some(pre) if pre.starts_with(\"--prefix\") => (global, true),\n            _ => (global, prefix),\n        }\n    });\n\n    if has_global && has_prefix {\n        debug!(\"Skipping global interception due to prefix argument\");\n    }\n\n    has_global && !has_prefix\n}\n\nfn is_flag<A>(arg: &A) -> bool\nwhere\n    A: AsRef<OsStr>,\n{\n    match arg.as_ref().to_str() {\n        Some(a) => a.starts_with('-'),\n        None => false,\n    }\n}\n\nfn is_positional<A>(arg: &A) -> bool\nwhere\n    A: AsRef<OsStr>,\n{\n    !is_flag(arg)\n}\n\n#[cfg(test)]\nmod tests {\n    use std::ffi::{OsStr, OsString};\n\n    fn arg_list<A, S>(args: A) -> Vec<OsString>\n    where\n        A: IntoIterator<Item = S>,\n        S: AsRef<OsStr>,\n    {\n        args.into_iter().map(|a| a.as_ref().to_owned()).collect()\n    }\n\n    mod npm {\n        use super::super::*;\n        use super::arg_list;\n\n        #[test]\n        fn handles_global_install() {\n            match CommandArg::for_npm(&arg_list([\"install\", \"--global\", \"typescript@3\"])) {\n                CommandArg::Global(GlobalCommand::Install(install)) => {\n                    assert_eq!(install.manager, PackageManager::Npm);\n                    assert_eq!(install.common_args, vec![\"install\", \"--global\"]);\n                    assert_eq!(install.tools, vec![\"typescript@3\"]);\n                }\n                _ => panic!(\"Doesn't parse global install as a global\"),\n            };\n        }\n\n        #[test]\n        fn handles_local_install() {\n            match CommandArg::for_npm(&arg_list([\"install\", \"--save-dev\", \"typescript\"])) {\n                CommandArg::Standard => (),\n                _ => panic!(\"Parses local install as global\"),\n            };\n        }\n\n        #[test]\n        fn handles_global_uninstall() {\n            match CommandArg::for_npm(&arg_list([\"uninstall\", \"--global\", \"typescript\"])) {\n                CommandArg::Global(GlobalCommand::Uninstall(uninstall)) => {\n                    assert_eq!(uninstall.tools, vec![\"typescript\"]);\n                }\n                _ => panic!(\"Doesn't parse global uninstall as a global\"),\n            };\n        }\n\n        #[test]\n        fn handles_local_uninstall() {\n            match CommandArg::for_npm(&arg_list([\"uninstall\", \"--save-dev\", \"typescript\"])) {\n                CommandArg::Standard => (),\n                _ => panic!(\"Parses local uninstall as global\"),\n            };\n        }\n\n        #[test]\n        fn handles_multiple_install() {\n            match CommandArg::for_npm(&arg_list([\n                \"install\",\n                \"--global\",\n                \"typescript@3\",\n                \"cowsay@1\",\n                \"ember-cli@2\",\n            ])) {\n                CommandArg::Global(GlobalCommand::Install(install)) => {\n                    assert_eq!(install.manager, PackageManager::Npm);\n                    assert_eq!(install.common_args, vec![\"install\", \"--global\"]);\n                    assert_eq!(\n                        install.tools,\n                        vec![\"typescript@3\", \"cowsay@1\", \"ember-cli@2\"]\n                    );\n                }\n                _ => panic!(\"Doesn't parse global install as a global\"),\n            };\n        }\n\n        #[test]\n        fn handles_multiple_uninstall() {\n            match CommandArg::for_npm(&arg_list([\n                \"uninstall\",\n                \"--global\",\n                \"typescript\",\n                \"cowsay\",\n                \"ember-cli\",\n            ])) {\n                CommandArg::Global(GlobalCommand::Uninstall(uninstall)) => {\n                    assert_eq!(uninstall.tools, vec![\"typescript\", \"cowsay\", \"ember-cli\"]);\n                }\n                _ => panic!(\"Doesn't parse global uninstall as a global\"),\n            };\n        }\n\n        #[test]\n        fn handles_bare_link() {\n            match CommandArg::for_npm(&arg_list([\"link\"])) {\n                CommandArg::Intercepted(InterceptedCommand::Link(_)) => (),\n                _ => panic!(\"Doesn't parse bare link command ('npm link' with no packages\"),\n            };\n        }\n\n        #[test]\n        fn handles_multiple_link() {\n            match CommandArg::for_npm(&arg_list([\"link\", \"typescript\", \"react\"])) {\n                CommandArg::Intercepted(InterceptedCommand::Link(link)) => {\n                    assert_eq!(link.tools, vec![\"typescript\", \"react\"]);\n                }\n                _ => panic!(\"Doesn't parse link command with packages\"),\n            };\n        }\n\n        #[test]\n        fn handles_bare_unlink() {\n            match CommandArg::for_npm(&arg_list([\"unlink\"])) {\n                CommandArg::Intercepted(InterceptedCommand::Unlink) => (),\n                _ => panic!(\"Doesn't parse bare unlink command ('npm unlink' with no packages\"),\n            };\n        }\n\n        #[test]\n        fn handles_local_unlink() {\n            match CommandArg::for_npm(&arg_list([\"unlink\", \"@angular/cli\"])) {\n                CommandArg::Standard => (),\n                _ => panic!(\"Doesn't pass through local 'unlink' command\"),\n            }\n        }\n\n        #[test]\n        fn handles_global_aliases() {\n            match CommandArg::for_npm(&arg_list([\"install\", \"--global\", \"typescript\"])) {\n                CommandArg::Global(GlobalCommand::Install(_)) => (),\n                _ => panic!(\"Doesn't parse long form (--global)\"),\n            };\n\n            match CommandArg::for_npm(&arg_list([\"install\", \"-g\", \"typescript\"])) {\n                CommandArg::Global(GlobalCommand::Install(_)) => (),\n                _ => panic!(\"Doesn't parse short form (-g)\"),\n            };\n        }\n\n        #[test]\n        fn handles_install_aliases() {\n            match CommandArg::for_npm(&arg_list([\"i\", \"--global\", \"typescript\"])) {\n                CommandArg::Global(GlobalCommand::Install(_)) => (),\n                _ => panic!(\"Doesn't parse short form (i)\"),\n            };\n\n            match CommandArg::for_npm(&arg_list([\"in\", \"--global\", \"typescript\"])) {\n                CommandArg::Global(GlobalCommand::Install(_)) => (),\n                _ => panic!(\"Doesn't parse short form (in)\"),\n            };\n\n            match CommandArg::for_npm(&arg_list([\"ins\", \"--global\", \"typescript\"])) {\n                CommandArg::Global(GlobalCommand::Install(_)) => (),\n                _ => panic!(\"Doesn't parse short form (ins)\"),\n            };\n\n            match CommandArg::for_npm(&arg_list([\"inst\", \"--global\", \"typescript\"])) {\n                CommandArg::Global(GlobalCommand::Install(_)) => (),\n                _ => panic!(\"Doesn't parse short form (inst)\"),\n            };\n\n            match CommandArg::for_npm(&arg_list([\"insta\", \"--global\", \"typescript\"])) {\n                CommandArg::Global(GlobalCommand::Install(_)) => (),\n                _ => panic!(\"Doesn't parse short form (insta)\"),\n            };\n\n            match CommandArg::for_npm(&arg_list([\"instal\", \"--global\", \"typescript\"])) {\n                CommandArg::Global(GlobalCommand::Install(_)) => (),\n                _ => panic!(\"Doesn't parse short form (instal)\"),\n            };\n\n            match CommandArg::for_npm(&arg_list([\"install\", \"--global\", \"typescript\"])) {\n                CommandArg::Global(GlobalCommand::Install(_)) => (),\n                _ => panic!(\"Doesn't parse exact command (install)\"),\n            };\n\n            match CommandArg::for_npm(&arg_list([\"isnt\", \"--global\", \"typescript\"])) {\n                CommandArg::Global(GlobalCommand::Install(_)) => (),\n                _ => panic!(\"Doesn't parse short form misspelling (isnt)\"),\n            };\n\n            match CommandArg::for_npm(&arg_list([\"isnta\", \"--global\", \"typescript\"])) {\n                CommandArg::Global(GlobalCommand::Install(_)) => (),\n                _ => panic!(\"Doesn't parse short form misspelling (isnta)\"),\n            };\n\n            match CommandArg::for_npm(&arg_list([\"isntal\", \"--global\", \"typescript\"])) {\n                CommandArg::Global(GlobalCommand::Install(_)) => (),\n                _ => panic!(\"Doesn't parse short form misspelling (isntal)\"),\n            };\n\n            match CommandArg::for_npm(&arg_list([\"isntall\", \"--global\", \"typescript\"])) {\n                CommandArg::Global(GlobalCommand::Install(_)) => (),\n                _ => panic!(\"Doesn't parse misspelling (isntall)\"),\n            };\n\n            match CommandArg::for_npm(&arg_list([\"add\", \"--global\", \"typescript\"])) {\n                CommandArg::Global(GlobalCommand::Install(_)) => (),\n                _ => panic!(\"Doesn't parse 'add' alias\"),\n            };\n        }\n\n        #[test]\n        fn handles_uninstall_aliases() {\n            match CommandArg::for_npm(&arg_list([\"uninstall\", \"--global\", \"typescript\"])) {\n                CommandArg::Global(GlobalCommand::Uninstall(_)) => (),\n                _ => panic!(\"Doesn't parse long form (uninstall)\"),\n            };\n\n            match CommandArg::for_npm(&arg_list([\"unlink\", \"--global\", \"typescript\"])) {\n                CommandArg::Global(GlobalCommand::Uninstall(_)) => (),\n                _ => panic!(\"Doesn't parse 'unlink'\"),\n            };\n\n            match CommandArg::for_npm(&arg_list([\"remove\", \"--global\", \"typescript\"])) {\n                CommandArg::Global(GlobalCommand::Uninstall(_)) => (),\n                _ => panic!(\"Doesn't parse 'remove'\"),\n            };\n\n            match CommandArg::for_npm(&arg_list([\"un\", \"--global\", \"typescript\"])) {\n                CommandArg::Global(GlobalCommand::Uninstall(_)) => (),\n                _ => panic!(\"Doesn't parse short form (un)\"),\n            };\n\n            match CommandArg::for_npm(&arg_list([\"rm\", \"--global\", \"typescript\"])) {\n                CommandArg::Global(GlobalCommand::Uninstall(_)) => (),\n                _ => panic!(\"Doesn't parse short form (rm)\"),\n            };\n\n            match CommandArg::for_npm(&arg_list([\"r\", \"--global\", \"typescript\"])) {\n                CommandArg::Global(GlobalCommand::Uninstall(_)) => (),\n                _ => panic!(\"Doesn't parse short form (r)\"),\n            };\n        }\n\n        #[test]\n        fn handles_link_aliases() {\n            match CommandArg::for_npm(&arg_list([\"link\"])) {\n                CommandArg::Intercepted(InterceptedCommand::Link(_)) => (),\n                _ => panic!(\"Doesn't parse long form (link)\"),\n            };\n\n            match CommandArg::for_npm(&arg_list([\"ln\"])) {\n                CommandArg::Intercepted(InterceptedCommand::Link(_)) => (),\n                _ => panic!(\"Doesn't parse short form (ln)\"),\n            };\n        }\n\n        #[test]\n        fn processes_flags() {\n            match CommandArg::for_npm(&arg_list([\n                \"--global\",\n                \"install\",\n                \"typescript\",\n                \"--no-audit\",\n                \"cowsay\",\n                \"--no-update-notifier\",\n            ])) {\n                CommandArg::Global(GlobalCommand::Install(install)) => {\n                    // The command gets moved to the front of common_args\n                    assert_eq!(\n                        install.common_args,\n                        vec![\"install\", \"--global\", \"--no-audit\", \"--no-update-notifier\"]\n                    );\n                    assert_eq!(install.tools, vec![\"typescript\", \"cowsay\"]);\n                }\n                _ => panic!(\"Doesn't parse install with extra flags as a global\"),\n            };\n\n            match CommandArg::for_npm(&arg_list([\n                \"uninstall\",\n                \"--silent\",\n                \"typescript\",\n                \"-g\",\n                \"cowsay\",\n            ])) {\n                CommandArg::Global(GlobalCommand::Uninstall(uninstall)) => {\n                    assert_eq!(uninstall.tools, vec![\"typescript\", \"cowsay\"]);\n                }\n                _ => panic!(\"Doesn't parse uninstall with extra flags as a global\"),\n            }\n        }\n\n        #[test]\n        fn skips_commands_with_prefix() {\n            match CommandArg::for_npm(&arg_list([\"install\", \"-g\", \"--prefix\", \"~/\", \"ember\"])) {\n                CommandArg::Standard => {}\n                _ => panic!(\"Parsed command with prefix as a global\"),\n            }\n\n            match CommandArg::for_npm(&arg_list([\"install\", \"-g\", \"--prefix=~/\", \"ember\"])) {\n                CommandArg::Standard => {}\n                _ => panic!(\"Parsed command with prefix as a global\"),\n            }\n\n            match CommandArg::for_npm(&arg_list([\"uninstall\", \"-g\", \"--prefix\", \"~/\", \"ember\"])) {\n                CommandArg::Standard => {}\n                _ => panic!(\"Parsed command with prefix as a global\"),\n            }\n\n            match CommandArg::for_npm(&arg_list([\"uninstall\", \"-g\", \"--prefix=~/\", \"ember\"])) {\n                CommandArg::Standard => {}\n                _ => panic!(\"Parsed command with prefix as a global\"),\n            }\n\n            match CommandArg::for_npm(&arg_list([\"unlink\", \"-g\", \"--prefix\", \"~/\", \"ember\"])) {\n                CommandArg::Standard => {}\n                _ => panic!(\"Parsed command with prefix as a global\"),\n            }\n\n            match CommandArg::for_npm(&arg_list([\"unlink\", \"-g\", \"--prefix=~/\", \"ember\"])) {\n                CommandArg::Standard => {}\n                _ => panic!(\"Parsed command with prefix as a global\"),\n            }\n\n            match CommandArg::for_npm(&arg_list([\"update\", \"-g\", \"--prefix\", \"~/\"])) {\n                CommandArg::Standard => {}\n                _ => panic!(\"Parsed command with prefix as a global\"),\n            }\n\n            match CommandArg::for_npm(&arg_list([\"update\", \"-g\", \"--prefix=~/\"])) {\n                CommandArg::Standard => {}\n                _ => panic!(\"Parsed command with prefix as a global\"),\n            }\n        }\n    }\n\n    mod yarn {\n        use super::super::*;\n        use super::*;\n\n        #[test]\n        fn handles_global_add() {\n            match CommandArg::for_yarn(&arg_list([\"global\", \"add\", \"typescript\"])) {\n                CommandArg::Global(GlobalCommand::Install(install)) => {\n                    assert_eq!(install.manager, PackageManager::Yarn);\n                    assert_eq!(install.common_args, vec![\"global\", \"add\"]);\n                    assert_eq!(install.tools, vec![\"typescript\"]);\n                }\n                _ => panic!(\"Doesn't parse global add as a global\"),\n            };\n        }\n\n        #[test]\n        fn handles_local_add() {\n            match CommandArg::for_yarn(&arg_list([\"add\", \"typescript\"])) {\n                CommandArg::Standard => (),\n                _ => panic!(\"Parses local add as a global\"),\n            };\n\n            match CommandArg::for_yarn(&arg_list([\"add\", \"global\"])) {\n                CommandArg::Standard => (),\n                _ => panic!(\"Incorrectly handles bad order\"),\n            };\n        }\n\n        #[test]\n        fn handles_global_remove() {\n            match CommandArg::for_yarn(&arg_list([\"global\", \"remove\", \"typescript\"])) {\n                CommandArg::Global(GlobalCommand::Uninstall(uninstall)) => {\n                    assert_eq!(uninstall.tools, vec![\"typescript\"]);\n                }\n                _ => panic!(\"Doesn't parse global remove as a global\"),\n            };\n        }\n\n        #[test]\n        fn handles_local_remove() {\n            match CommandArg::for_yarn(&arg_list([\"remove\", \"typescript\"])) {\n                CommandArg::Standard => (),\n                _ => panic!(\"Parses local remove as a global\"),\n            };\n\n            match CommandArg::for_yarn(&arg_list([\"remove\", \"global\"])) {\n                CommandArg::Standard => (),\n                _ => panic!(\"Incorrectly handles bad order\"),\n            };\n        }\n\n        #[test]\n        fn handles_multiple_add() {\n            match CommandArg::for_yarn(&arg_list([\n                \"global\",\n                \"add\",\n                \"typescript\",\n                \"cowsay\",\n                \"ember-cli\",\n            ])) {\n                CommandArg::Global(GlobalCommand::Install(install)) => {\n                    assert_eq!(install.manager, PackageManager::Yarn);\n                    assert_eq!(install.common_args, vec![\"global\", \"add\"]);\n                    assert_eq!(install.tools, vec![\"typescript\", \"cowsay\", \"ember-cli\"]);\n                }\n                _ => panic!(\"Doesn't parse global add as a global\"),\n            };\n        }\n\n        #[test]\n        fn handles_multiple_remove() {\n            match CommandArg::for_yarn(&arg_list([\n                \"global\",\n                \"remove\",\n                \"typescript\",\n                \"cowsay\",\n                \"ember-cli\",\n            ])) {\n                CommandArg::Global(GlobalCommand::Uninstall(uninstall)) => {\n                    assert_eq!(uninstall.tools, vec![\"typescript\", \"cowsay\", \"ember-cli\"]);\n                }\n                _ => panic!(\"Doesn't parse global remove as a global\"),\n            };\n        }\n\n        #[test]\n        fn processes_flags() {\n            match CommandArg::for_yarn(&arg_list([\n                \"global\",\n                \"--silent\",\n                \"add\",\n                \"ember-cli\",\n                \"--prefix=~/\",\n                \"typescript\",\n            ])) {\n                CommandArg::Global(GlobalCommand::Install(install)) => {\n                    // The commands get moved to the front of common_args\n                    assert_eq!(\n                        install.common_args,\n                        vec![\"global\", \"add\", \"--silent\", \"--prefix=~/\"]\n                    );\n                    assert_eq!(install.tools, vec![\"ember-cli\", \"typescript\"]);\n                }\n                _ => panic!(\"Doesn't parse global add as a global\"),\n            };\n\n            match CommandArg::for_yarn(&arg_list([\n                \"global\",\n                \"--silent\",\n                \"remove\",\n                \"ember-cli\",\n                \"--prefix=~/\",\n                \"typescript\",\n            ])) {\n                CommandArg::Global(GlobalCommand::Uninstall(uninstall)) => {\n                    assert_eq!(uninstall.tools, vec![\"ember-cli\", \"typescript\"]);\n                }\n                _ => panic!(\"Doesn't parse global add as a global\"),\n            };\n        }\n    }\n}\n"
  },
  {
    "path": "crates/volta-core/src/run/pnpm.rs",
    "content": "use std::env;\nuse std::ffi::OsString;\n\nuse super::executor::{Executor, ToolCommand, ToolKind};\nuse super::{debug_active_image, debug_no_platform, RECURSION_ENV_VAR};\nuse crate::error::{ErrorKind, Fallible};\nuse crate::platform::{Platform, Source, System};\nuse crate::session::{ActivityKind, Session};\n\npub(super) fn command(args: &[OsString], session: &mut Session) -> Fallible<Executor> {\n    session.add_event_start(ActivityKind::Pnpm);\n    // Don't re-evaluate the context or global install interception if this is a recursive call\n    let platform = match env::var_os(RECURSION_ENV_VAR) {\n        Some(_) => None,\n        None => {\n            // FIXME: Figure out how to intercept pnpm global commands properly.\n            // This guard prevents all global commands from running, it should\n            // be removed when we fully implement global command interception.\n            let is_global = args.iter().any(|f| f == \"--global\" || f == \"-g\");\n            if is_global {\n                return Err(ErrorKind::Unimplemented {\n                    feature: \"pnpm global commands\".into(),\n                }\n                .into());\n            }\n\n            Platform::current(session)?\n        }\n    };\n\n    Ok(ToolCommand::new(\"pnpm\", args, platform, ToolKind::Pnpm).into())\n}\n\n/// Determine the execution context (PATH and failure error message) for pnpm\npub(super) fn execution_context(\n    platform: Option<Platform>,\n    session: &mut Session,\n) -> Fallible<(OsString, ErrorKind)> {\n    match platform {\n        Some(plat) => {\n            validate_platform_pnpm(&plat)?;\n\n            let image = plat.checkout(session)?;\n            let path = image.path()?;\n            debug_active_image(&image);\n\n            Ok((path, ErrorKind::BinaryExecError))\n        }\n        None => {\n            let path = System::path()?;\n            debug_no_platform();\n            Ok((path, ErrorKind::NoPlatform))\n        }\n    }\n}\n\nfn validate_platform_pnpm(platform: &Platform) -> Fallible<()> {\n    match &platform.pnpm {\n        Some(_) => Ok(()),\n        None => match platform.node.source {\n            Source::Project => Err(ErrorKind::NoProjectPnpm.into()),\n            Source::Default | Source::Binary => Err(ErrorKind::NoDefaultPnpm.into()),\n            Source::CommandLine => Err(ErrorKind::NoCommandLinePnpm.into()),\n        },\n    }\n}\n"
  },
  {
    "path": "crates/volta-core/src/run/yarn.rs",
    "content": "use std::env;\nuse std::ffi::OsString;\n\nuse super::executor::{Executor, ToolCommand, ToolKind};\nuse super::parser::CommandArg;\nuse super::{debug_active_image, debug_no_platform, RECURSION_ENV_VAR};\nuse crate::error::{ErrorKind, Fallible};\nuse crate::platform::{Platform, Source, System};\nuse crate::session::{ActivityKind, Session};\n\n/// Build an `Executor` for Yarn\n///\n/// If the command is a global add or remove and we have a default platform available, then we will\n/// use custom logic to ensure that the package is correctly installed / uninstalled in the Volta\n/// directory.\n///\n/// If the command is _not_ a global add / remove or we don't have a default platform, then\n/// we will allow Yarn to execute the command as usual.\npub(super) fn command(args: &[OsString], session: &mut Session) -> Fallible<Executor> {\n    session.add_event_start(ActivityKind::Yarn);\n    // Don't re-evaluate the context or global install interception if this is a recursive call\n    let platform = match env::var_os(RECURSION_ENV_VAR) {\n        Some(_) => None,\n        None => {\n            if let CommandArg::Global(cmd) = CommandArg::for_yarn(args) {\n                // For globals, only intercept if the default platform exists\n                if let Some(default_platform) = session.default_platform()? {\n                    return cmd.executor(default_platform);\n                }\n            }\n\n            Platform::current(session)?\n        }\n    };\n\n    Ok(ToolCommand::new(\"yarn\", args, platform, ToolKind::Yarn).into())\n}\n\n/// Determine the execution context (PATH and failure error message) for Yarn\npub(super) fn execution_context(\n    platform: Option<Platform>,\n    session: &mut Session,\n) -> Fallible<(OsString, ErrorKind)> {\n    match platform {\n        Some(plat) => {\n            validate_platform_yarn(&plat)?;\n\n            let image = plat.checkout(session)?;\n            let path = image.path()?;\n            debug_active_image(&image);\n\n            Ok((path, ErrorKind::BinaryExecError))\n        }\n        None => {\n            let path = System::path()?;\n            debug_no_platform();\n            Ok((path, ErrorKind::NoPlatform))\n        }\n    }\n}\n\nfn validate_platform_yarn(platform: &Platform) -> Fallible<()> {\n    match &platform.yarn {\n        Some(_) => Ok(()),\n        None => match platform.node.source {\n            Source::Project => Err(ErrorKind::NoProjectYarn.into()),\n            Source::Default | Source::Binary => Err(ErrorKind::NoDefaultYarn.into()),\n            Source::CommandLine => Err(ErrorKind::NoCommandLineYarn.into()),\n        },\n    }\n}\n"
  },
  {
    "path": "crates/volta-core/src/session.rs",
    "content": "//! Provides the `Session` type, which represents the user's state during an\n//! execution of a Volta tool, including their current directory, Volta\n//! hook configuration, and the state of the local inventory.\n\nuse std::fmt::{self, Display, Formatter};\nuse std::process::exit;\n\nuse crate::error::{ExitCode, Fallible, VoltaError};\nuse crate::event::EventLog;\nuse crate::hook::{HookConfig, LazyHookConfig};\nuse crate::platform::PlatformSpec;\nuse crate::project::{LazyProject, Project};\nuse crate::toolchain::{LazyToolchain, Toolchain};\nuse log::debug;\n\n#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Copy)]\npub enum ActivityKind {\n    Fetch,\n    Install,\n    Uninstall,\n    List,\n    Current,\n    Default,\n    Pin,\n    Node,\n    Npm,\n    Npx,\n    Pnpm,\n    Yarn,\n    Volta,\n    Tool,\n    Help,\n    Version,\n    Binary,\n    Shim,\n    Completions,\n    Which,\n    Setup,\n    Run,\n    Args,\n}\n\nimpl Display for ActivityKind {\n    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {\n        let s = match self {\n            ActivityKind::Fetch => \"fetch\",\n            ActivityKind::Install => \"install\",\n            ActivityKind::Uninstall => \"uninstall\",\n            ActivityKind::List => \"list\",\n            ActivityKind::Current => \"current\",\n            ActivityKind::Default => \"default\",\n            ActivityKind::Pin => \"pin\",\n            ActivityKind::Node => \"node\",\n            ActivityKind::Npm => \"npm\",\n            ActivityKind::Npx => \"npx\",\n            ActivityKind::Pnpm => \"pnpm\",\n            ActivityKind::Yarn => \"yarn\",\n            ActivityKind::Volta => \"volta\",\n            ActivityKind::Tool => \"tool\",\n            ActivityKind::Help => \"help\",\n            ActivityKind::Version => \"version\",\n            ActivityKind::Binary => \"binary\",\n            ActivityKind::Setup => \"setup\",\n            ActivityKind::Shim => \"shim\",\n            ActivityKind::Completions => \"completions\",\n            ActivityKind::Which => \"which\",\n            ActivityKind::Run => \"run\",\n            ActivityKind::Args => \"args\",\n        };\n        f.write_str(s)\n    }\n}\n\n/// Represents the user's state during an execution of a Volta tool. The session\n/// encapsulates a number of aspects of the environment in which the tool was\n/// invoked, including:\n///\n/// - the current directory\n/// - the Node project tree that contains the current directory (if any)\n/// - the Volta hook configuration\n/// - the inventory of locally-fetched Volta tools\npub struct Session {\n    hooks: LazyHookConfig,\n    toolchain: LazyToolchain,\n    project: LazyProject,\n    event_log: EventLog,\n}\n\nimpl Session {\n    /// Constructs a new `Session`.\n    pub fn init() -> Session {\n        Session {\n            hooks: LazyHookConfig::init(),\n            toolchain: LazyToolchain::init(),\n            project: LazyProject::init(),\n            event_log: EventLog::init(),\n        }\n    }\n\n    /// Produces a reference to the current Node project, if any.\n    pub fn project(&self) -> Fallible<Option<&Project>> {\n        self.project.get()\n    }\n\n    /// Produces a mutable reference to the current Node project, if any.\n    pub fn project_mut(&mut self) -> Fallible<Option<&mut Project>> {\n        self.project.get_mut()\n    }\n\n    /// Returns the user's default platform, if any\n    pub fn default_platform(&self) -> Fallible<Option<&PlatformSpec>> {\n        self.toolchain.get().map(Toolchain::platform)\n    }\n\n    /// Returns the current project's pinned platform image, if any.\n    pub fn project_platform(&self) -> Fallible<Option<&PlatformSpec>> {\n        if let Some(project) = self.project()? {\n            return Ok(project.platform());\n        }\n        Ok(None)\n    }\n\n    /// Produces a reference to the current toolchain (default platform specification)\n    pub fn toolchain(&self) -> Fallible<&Toolchain> {\n        self.toolchain.get()\n    }\n\n    /// Produces a mutable reference to the current toolchain\n    pub fn toolchain_mut(&mut self) -> Fallible<&mut Toolchain> {\n        self.toolchain.get_mut()\n    }\n\n    /// Produces a reference to the hook configuration\n    pub fn hooks(&self) -> Fallible<&HookConfig> {\n        self.hooks.get(self.project()?)\n    }\n\n    pub fn add_event_start(&mut self, activity_kind: ActivityKind) {\n        self.event_log.add_event_start(activity_kind)\n    }\n    pub fn add_event_end(&mut self, activity_kind: ActivityKind, exit_code: ExitCode) {\n        self.event_log.add_event_end(activity_kind, exit_code)\n    }\n    pub fn add_event_tool_end(&mut self, activity_kind: ActivityKind, exit_code: i32) {\n        self.event_log.add_event_tool_end(activity_kind, exit_code)\n    }\n    pub fn add_event_error(&mut self, activity_kind: ActivityKind, error: &VoltaError) {\n        self.event_log.add_event_error(activity_kind, error)\n    }\n\n    fn publish_to_event_log(self) {\n        let Self {\n            project,\n            hooks,\n            mut event_log,\n            ..\n        } = self;\n        let plugin_res = project\n            .get()\n            .and_then(|p| hooks.get(p))\n            .map(|hooks| hooks.events().and_then(|e| e.publish.as_ref()));\n        match plugin_res {\n            Ok(plugin) => {\n                event_log.add_event_args();\n                event_log.publish(plugin);\n            }\n            Err(e) => {\n                debug!(\"Unable to publish event log.\\n{}\", e);\n            }\n        }\n    }\n\n    pub fn exit(self, code: ExitCode) -> ! {\n        self.publish_to_event_log();\n        code.exit();\n    }\n\n    pub fn exit_tool(self, code: i32) -> ! {\n        self.publish_to_event_log();\n        exit(code);\n    }\n}\n\n#[cfg(test)]\npub mod tests {\n\n    use crate::session::Session;\n    use std::env;\n    use std::path::PathBuf;\n\n    fn fixture_path(fixture_dir: &str) -> PathBuf {\n        let mut cargo_manifest_dir = PathBuf::from(env!(\"CARGO_MANIFEST_DIR\"));\n        cargo_manifest_dir.push(\"fixtures\");\n        cargo_manifest_dir.push(fixture_dir);\n        cargo_manifest_dir\n    }\n\n    #[test]\n    fn test_in_pinned_project() {\n        let project_pinned = fixture_path(\"basic\");\n        env::set_current_dir(project_pinned).expect(\"Could not set current directory\");\n        let pinned_session = Session::init();\n        let pinned_platform = pinned_session\n            .project_platform()\n            .expect(\"Couldn't create Project\");\n        assert!(pinned_platform.is_some());\n\n        let project_unpinned = fixture_path(\"no_toolchain\");\n        env::set_current_dir(project_unpinned).expect(\"Could not set current directory\");\n        let unpinned_session = Session::init();\n        let unpinned_platform = unpinned_session\n            .project_platform()\n            .expect(\"Couldn't create Project\");\n        assert!(unpinned_platform.is_none());\n    }\n}\n"
  },
  {
    "path": "crates/volta-core/src/shim.rs",
    "content": "//! Provides utilities for modifying shims for 3rd-party executables\n\nuse std::collections::HashSet;\nuse std::fs;\nuse std::io;\nuse std::path::Path;\n\nuse crate::error::{Context, ErrorKind, Fallible, VoltaError};\nuse crate::fs::read_dir_eager;\nuse crate::layout::volta_home;\nuse crate::sync::VoltaLock;\nuse log::debug;\n\npub use platform::create;\n\npub fn regenerate_shims_for_dir(dir: &Path) -> Fallible<()> {\n    // Acquire a lock on the Volta directory, if possible, to prevent concurrent changes\n    let _lock = VoltaLock::acquire();\n    debug!(\"Rebuilding shims for directory: {}\", dir.display());\n    for shim_name in get_shim_list_deduped(dir)?.iter() {\n        delete(shim_name)?;\n        create(shim_name)?;\n    }\n\n    Ok(())\n}\n\nfn get_shim_list_deduped(dir: &Path) -> Fallible<HashSet<String>> {\n    let contents = read_dir_eager(dir).with_context(|| ErrorKind::ReadDirError {\n        dir: dir.to_owned(),\n    })?;\n\n    #[cfg(unix)]\n    {\n        let mut shims: HashSet<String> =\n            contents.filter_map(platform::entry_to_shim_name).collect();\n        shims.insert(\"node\".into());\n        shims.insert(\"npm\".into());\n        shims.insert(\"npx\".into());\n        shims.insert(\"pnpm\".into());\n        shims.insert(\"yarn\".into());\n        shims.insert(\"yarnpkg\".into());\n        Ok(shims)\n    }\n\n    #[cfg(windows)]\n    {\n        // On Windows, the default shims are installed in Program Files, so we don't need to generate them here\n        Ok(contents.filter_map(platform::entry_to_shim_name).collect())\n    }\n}\n\n#[derive(PartialEq, Eq)]\npub enum ShimResult {\n    Created,\n    AlreadyExists,\n    Deleted,\n    DoesntExist,\n}\n\npub fn delete(shim_name: &str) -> Fallible<ShimResult> {\n    let shim = volta_home()?.shim_file(shim_name);\n\n    #[cfg(windows)]\n    platform::delete_git_bash_script(shim_name)?;\n\n    match fs::remove_file(shim) {\n        Ok(_) => Ok(ShimResult::Deleted),\n        Err(err) => {\n            if err.kind() == io::ErrorKind::NotFound {\n                Ok(ShimResult::DoesntExist)\n            } else {\n                Err(VoltaError::from_source(\n                    err,\n                    ErrorKind::ShimRemoveError {\n                        name: shim_name.to_string(),\n                    },\n                ))\n            }\n        }\n    }\n}\n\n#[cfg(unix)]\nmod platform {\n    //! Unix-specific shim utilities\n    //!\n    //! On macOS and Linux, creating a shim involves creating a symlink to the `volta-shim`\n    //! executable. Additionally, filtering the shims from directory entries means looking\n    //! for symlinks and ignoring the actual binaries\n    use std::ffi::OsStr;\n    use std::fs::{DirEntry, Metadata};\n    use std::io;\n\n    use super::ShimResult;\n    use crate::error::{ErrorKind, Fallible, VoltaError};\n    use crate::fs::symlink_file;\n    use crate::layout::{volta_home, volta_install};\n\n    pub fn create(shim_name: &str) -> Fallible<ShimResult> {\n        let executable = volta_install()?.shim_executable();\n        let shim = volta_home()?.shim_file(shim_name);\n\n        match symlink_file(executable, shim) {\n            Ok(_) => Ok(ShimResult::Created),\n            Err(err) => {\n                if err.kind() == io::ErrorKind::AlreadyExists {\n                    Ok(ShimResult::AlreadyExists)\n                } else {\n                    Err(VoltaError::from_source(\n                        err,\n                        ErrorKind::ShimCreateError {\n                            name: shim_name.to_string(),\n                        },\n                    ))\n                }\n            }\n        }\n    }\n\n    pub fn entry_to_shim_name((entry, metadata): (DirEntry, Metadata)) -> Option<String> {\n        if metadata.file_type().is_symlink() {\n            entry\n                .path()\n                .file_stem()\n                .and_then(OsStr::to_str)\n                .map(ToOwned::to_owned)\n        } else {\n            None\n        }\n    }\n}\n\n#[cfg(windows)]\nmod platform {\n    //! Windows-specific shim utilities\n    //!\n    //! On Windows, creating a shim involves creating a small .cmd script, rather than a symlink.\n    //! This allows us to create shims without requiring administrator privileges or developer\n    //! mode. Also, to support Git Bash, we create a similar script with bash syntax that doesn't\n    //! have a file extension. This allows Powershell and Cmd to ignore it, while Bash detects it\n    //! as an executable script.\n    //!\n    //! Finally, filtering directory entries to find the shim files involves looking for the .cmd\n    //! files.\n    use std::ffi::OsStr;\n    use std::fs::{write, DirEntry, Metadata};\n\n    use super::ShimResult;\n    use crate::error::{Context, ErrorKind, Fallible};\n    use crate::fs::remove_file_if_exists;\n    use crate::layout::volta_home;\n\n    const SHIM_SCRIPT_CONTENTS: &str = r#\"@echo off\nvolta run %~n0 %*\n\"#;\n\n    const GIT_BASH_SCRIPT_CONTENTS: &str = r#\"#!/bin/bash\nvolta run \"$(basename $0)\" \"$@\"\"#;\n\n    pub fn create(shim_name: &str) -> Fallible<ShimResult> {\n        let shim = volta_home()?.shim_file(shim_name);\n\n        write(shim, SHIM_SCRIPT_CONTENTS).with_context(|| ErrorKind::ShimCreateError {\n            name: shim_name.to_owned(),\n        })?;\n\n        let git_bash_script = volta_home()?.shim_git_bash_script_file(shim_name);\n\n        write(git_bash_script, GIT_BASH_SCRIPT_CONTENTS).with_context(|| {\n            ErrorKind::ShimCreateError {\n                name: shim_name.to_owned(),\n            }\n        })?;\n\n        Ok(ShimResult::Created)\n    }\n\n    pub fn entry_to_shim_name((entry, _): (DirEntry, Metadata)) -> Option<String> {\n        let path = entry.path();\n\n        if path.extension().is_some_and(|ext| ext == \"cmd\") {\n            path.file_stem()\n                .and_then(OsStr::to_str)\n                .map(ToOwned::to_owned)\n        } else {\n            None\n        }\n    }\n\n    pub fn delete_git_bash_script(shim_name: &str) -> Fallible<()> {\n        let script_path = volta_home()?.shim_git_bash_script_file(shim_name);\n        remove_file_if_exists(script_path).with_context(|| ErrorKind::ShimRemoveError {\n            name: shim_name.to_string(),\n        })\n    }\n}\n"
  },
  {
    "path": "crates/volta-core/src/signal.rs",
    "content": "use std::process::exit;\nuse std::sync::atomic::{AtomicBool, Ordering};\n\nuse log::debug;\n\nstatic SHIM_HAS_CONTROL: AtomicBool = AtomicBool::new(false);\nconst INTERRUPTED_EXIT_CODE: i32 = 130;\n\npub fn pass_control_to_shim() {\n    SHIM_HAS_CONTROL.store(true, Ordering::SeqCst);\n}\n\npub fn setup_signal_handler() {\n    let result = ctrlc::set_handler(|| {\n        if !SHIM_HAS_CONTROL.load(Ordering::SeqCst) {\n            exit(INTERRUPTED_EXIT_CODE);\n        }\n    });\n\n    if result.is_err() {\n        debug!(\"Unable to set Ctrl+C handler, SIGINT will not be handled correctly\");\n    }\n}\n"
  },
  {
    "path": "crates/volta-core/src/style.rs",
    "content": "//! The view layer of Volta, with utilities for styling command-line output.\nuse std::borrow::Cow;\nuse std::error::Error;\nuse std::time::Duration;\n\nuse archive::Origin;\nuse cfg_if::cfg_if;\nuse console::{style, StyledObject};\nuse indicatif::{ProgressBar, ProgressStyle};\nuse terminal_size::{terminal_size, Width};\n\npub const MAX_WIDTH: usize = 100;\nconst MAX_PROGRESS_WIDTH: usize = 40;\n\n/// Generate the styled prefix for a success message\npub fn success_prefix() -> StyledObject<&'static str> {\n    style(\"success:\").green().bold()\n}\n\n/// Generate the styled prefix for a note\npub fn note_prefix() -> StyledObject<&'static str> {\n    style(\"   note:\").magenta().bold()\n}\n\n/// Format the underlying cause of an error\npub(crate) fn format_error_cause(inner: &dyn Error) -> String {\n    format!(\n        \"{}{} {}\",\n        style(\"Error cause\").underlined().bold(),\n        style(\":\").bold(),\n        inner\n    )\n}\n\n/// Determines the string to display based on the Origin of the operation.\nfn action_str(origin: Origin) -> &'static str {\n    match origin {\n        Origin::Local => \"Unpacking\",\n        Origin::Remote => \"Fetching\",\n    }\n}\n\npub fn tool_version<N, V>(name: N, version: V) -> String\nwhere\n    N: std::fmt::Display + Sized,\n    V: std::fmt::Display + Sized,\n{\n    format!(\"{:}@{:}\", name, version)\n}\n\n/// Get the width of the terminal, limited to a maximum of MAX_WIDTH\npub fn text_width() -> Option<usize> {\n    terminal_size().map(|(Width(w), _)| (w as usize).min(MAX_WIDTH))\n}\n\n/// Constructs a command-line progress bar based on the specified Origin enum\n/// (e.g., `Origin::Remote`), details string (e.g., `\"v1.23.4\"`), and logical\n/// length (i.e., the number of logical progress steps in the process being\n/// visualized by the progress bar).\npub fn progress_bar(origin: Origin, details: &str, len: u64) -> ProgressBar {\n    let action = action_str(origin);\n    let action_width = action.len() + 2; // plus 2 spaces to look nice\n    let msg_width = action_width + 1 + details.len();\n\n    //   Fetching node@9.11.2  [=============>                          ]  34%\n    // |--------| |---------|   |--------------------------------------|  |-|\n    //    action    details                      bar                 percentage\n    let bar_width = match text_width() {\n        Some(width) => MAX_PROGRESS_WIDTH.min(width - 2 - msg_width - 2 - 2 - 1 - 3 - 1),\n        None => MAX_PROGRESS_WIDTH,\n    };\n\n    let progress = ProgressBar::new(len);\n\n    progress.set_message(format!(\n        \"{: >width$} {}\",\n        style(action).green().bold(),\n        details,\n        width = action_width,\n    ));\n    progress.set_style(\n        ProgressStyle::default_bar()\n            .template(&format!(\n                \"{{msg}}  [{{bar:{}.cyan/blue}}] {{percent:>3}}%\",\n                bar_width\n            ))\n            .expect(\"template is valid\")\n            .progress_chars(\"=> \"),\n    );\n\n    progress\n}\n\ncfg_if! {\n    if #[cfg(windows)] {\n        /// Constructs a command-line progress spinner with the specified \"message\"\n        /// string. The spinner is ticked by default every 100ms.\n        pub fn progress_spinner<S>(message: S) -> ProgressBar\n        where\n            S: Into<Cow<'static, str>>,\n        {\n            let spinner = ProgressBar::new_spinner();\n            // Windows CMD prompt doesn't support Unicode characters, so use a simplified spinner\n            let style = ProgressStyle::default_spinner().tick_chars(r#\"-\\|/-\"#);\n\n            spinner.set_message(message);\n            spinner.set_style(style);\n            spinner.enable_steady_tick(Duration::from_millis(100));\n\n            spinner\n        }\n    } else {\n        /// Constructs a command-line progress spinner with the specified \"message\"\n        /// string. The spinner is ticked by default every 50ms.\n        pub fn progress_spinner<S>(message: S) -> ProgressBar\n        where\n            S: Into<Cow<'static, str>>,\n        {\n            // ⠋ Fetching public registry: https://nodejs.org/dist/index.json\n            let spinner = ProgressBar::new_spinner();\n\n            spinner.set_message(message);\n            spinner.set_style(ProgressStyle::default_spinner());\n            spinner.enable_steady_tick(Duration::from_millis(50));\n\n            spinner\n        }\n    }\n}\n"
  },
  {
    "path": "crates/volta-core/src/sync.rs",
    "content": "//! Inter-process locking on the Volta directory\n//!\n//! To avoid issues where multiple separate invocations of Volta modify the\n//! data directory simultaneously, we provide a locking mechanism that only\n//! allows a single process to modify the directory at a time.\n//!\n//! However, within a single process, we may attempt to lock the directory in\n//! different code paths. For example, when installing a package we require a\n//! lock, however we also may need to install Node, which requires a lock as\n//! well. To avoid deadlocks in those situations, we track the state of the\n//! lock globally:\n//!\n//! - If a lock is requested and no locks are active, then we acquire a file\n//!   lock on the `volta.lock` file and initialize the state with a count of 1\n//! - If a lock already exists, then we increment the count of active locks\n//! - When a lock is no longer needed, we decrement the count of active locks\n//! - When the last lock is released, we release the file lock and clear the\n//!   global lock state.\n//!\n//! This allows multiple code paths to request a lock and not worry about\n//! potential deadlocks, while still preventing multiple processes from making\n//! concurrent changes.\n\nuse std::fs::{File, OpenOptions};\nuse std::marker::PhantomData;\nuse std::ops::Drop;\nuse std::sync::Mutex;\n\nuse crate::error::{Context, ErrorKind, Fallible};\nuse crate::layout::volta_home;\nuse crate::style::progress_spinner;\nuse fs2::FileExt;\nuse log::debug;\nuse once_cell::sync::Lazy;\n\nstatic LOCK_STATE: Lazy<Mutex<Option<LockState>>> = Lazy::new(|| Mutex::new(None));\n\n/// The current state of locks for this process.\n///\n/// Note: To ensure thread safety _within_ this process, we enclose the\n/// state in a Mutex. This Mutex and it's associated locks are separate\n/// from the overall process lock and are only used to ensure the count\n/// is accurately maintained within a given process.\nstruct LockState {\n    file: File,\n    count: usize,\n}\n\nconst LOCK_FILE: &str = \"volta.lock\";\n\n/// An RAII implementation of a process lock on the Volta directory. A given Volta process can have\n/// multiple active locks, but only one process can have any locks at a time.\n///\n/// Once all of the `VoltaLock` objects go out of scope, the lock will be released to other\n/// processes.\npub struct VoltaLock {\n    // Private field ensures that this cannot be created except for with the `acquire()` method\n    _private: PhantomData<()>,\n}\n\nimpl VoltaLock {\n    pub fn acquire() -> Fallible<Self> {\n        let mut state = LOCK_STATE\n            .lock()\n            .with_context(|| ErrorKind::LockAcquireError)?;\n\n        // Check if there is an active lock for this process. If so, increment\n        // the count of active locks. If not, create a file lock and initialize\n        // the state with a count of 1\n        match &mut *state {\n            Some(inner) => {\n                inner.count += 1;\n            }\n            None => {\n                let path = volta_home()?.root().join(LOCK_FILE);\n                debug!(\"Acquiring lock on Volta directory: {}\", path.display());\n\n                let file = OpenOptions::new()\n                    .write(true)\n                    .create(true)\n                    .open(path)\n                    .with_context(|| ErrorKind::LockAcquireError)?;\n                // First we try to lock the file without blocking. If that fails, then we show a spinner\n                // and block until the lock completes.\n                if file.try_lock_exclusive().is_err() {\n                    let spinner = progress_spinner(\"Waiting for file lock on Volta directory\");\n                    // Note: Blocks until the file can be locked\n                    let lock_result = file\n                        .lock_exclusive()\n                        .with_context(|| ErrorKind::LockAcquireError);\n                    spinner.finish_and_clear();\n                    lock_result?;\n                }\n\n                *state = Some(LockState { file, count: 1 });\n            }\n        }\n\n        Ok(Self {\n            _private: PhantomData,\n        })\n    }\n}\n\nimpl Drop for VoltaLock {\n    fn drop(&mut self) {\n        // On drop, decrement the count of active locks. If the count is 1,\n        // then this is the last active lock, so instead unlock the file and\n        // clear out the lock state.\n        if let Ok(mut state) = LOCK_STATE.lock() {\n            match &mut *state {\n                Some(inner) => {\n                    if inner.count == 1 {\n                        debug!(\"Unlocking Volta Directory\");\n                        let _ = inner.file.unlock();\n                        *state = None;\n                    } else {\n                        inner.count -= 1;\n                    }\n                }\n                None => {\n                    debug!(\"Unexpected unlock of Volta directory when it wasn't locked\");\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "crates/volta-core/src/tool/mod.rs",
    "content": "use std::env;\nuse std::fmt::{self, Display};\nuse std::path::PathBuf;\n\nuse crate::error::{ErrorKind, Fallible};\nuse crate::layout::volta_home;\nuse crate::session::Session;\nuse crate::style::{note_prefix, success_prefix, tool_version};\nuse crate::sync::VoltaLock;\nuse crate::version::VersionSpec;\nuse crate::VOLTA_FEATURE_PNPM;\nuse cfg_if::cfg_if;\nuse log::{debug, info};\n\npub mod node;\npub mod npm;\npub mod package;\npub mod pnpm;\nmod registry;\nmod serial;\npub mod yarn;\n\npub use node::{\n    load_default_npm_version, Node, NODE_DISTRO_ARCH, NODE_DISTRO_EXTENSION, NODE_DISTRO_OS,\n};\npub use npm::{BundledNpm, Npm};\npub use package::{BinConfig, Package, PackageConfig, PackageManifest};\npub use pnpm::Pnpm;\npub use registry::PackageDetails;\npub use yarn::Yarn;\n\nfn debug_already_fetched<T: Display>(tool: T) {\n    debug!(\"{} has already been fetched, skipping download\", tool);\n}\n\nfn info_installed<T: Display>(tool: T) {\n    info!(\"{} installed and set {tool} as default\", success_prefix());\n}\n\nfn info_fetched<T: Display>(tool: T) {\n    info!(\"{} fetched {tool}\", success_prefix());\n}\n\nfn info_pinned<T: Display>(tool: T) {\n    info!(\"{} pinned {tool} in package.json\", success_prefix());\n}\n\nfn info_project_version<P, D>(project_version: P, default_version: D)\nwhere\n    P: Display,\n    D: Display,\n{\n    info!(\n        r#\"{} you are using {project_version} in the current project; to\n         instead use {default_version}, run `volta pin {default_version}`\"#,\n        note_prefix()\n    );\n}\n\n/// Trait representing all of the actions that can be taken with a tool\npub trait Tool: Display {\n    /// Fetch a Tool into the local inventory\n    fn fetch(self: Box<Self>, session: &mut Session) -> Fallible<()>;\n    /// Install a tool, making it the default so it is available everywhere on the user's machine\n    fn install(self: Box<Self>, session: &mut Session) -> Fallible<()>;\n    /// Pin a tool in the local project so that it is usable within the project\n    fn pin(self: Box<Self>, session: &mut Session) -> Fallible<()>;\n}\n\n/// Specification for a tool and its associated version.\n#[derive(Debug)]\n#[cfg_attr(test, derive(PartialEq, Eq))]\npub enum Spec {\n    Node(VersionSpec),\n    Npm(VersionSpec),\n    Pnpm(VersionSpec),\n    Yarn(VersionSpec),\n    Package(String, VersionSpec),\n}\n\nimpl Spec {\n    /// Resolve a tool spec into a fully realized Tool that can be fetched\n    pub fn resolve(self, session: &mut Session) -> Fallible<Box<dyn Tool>> {\n        match self {\n            Spec::Node(version) => {\n                let version = node::resolve(version, session)?;\n                Ok(Box::new(Node::new(version)))\n            }\n            Spec::Npm(version) => match npm::resolve(version, session)? {\n                Some(version) => Ok(Box::new(Npm::new(version))),\n                None => Ok(Box::new(BundledNpm)),\n            },\n            Spec::Pnpm(version) => {\n                // If the pnpm feature flag is set, use the special-cased package manager logic\n                // to handle resolving (and ultimately fetching / installing) pnpm. If not, then\n                // fall back to the global package behavior, which was the case prior to pnpm\n                // support being added\n                if env::var_os(VOLTA_FEATURE_PNPM).is_some() {\n                    let version = pnpm::resolve(version, session)?;\n                    Ok(Box::new(Pnpm::new(version)))\n                } else {\n                    let package = Package::new(\"pnpm\".to_owned(), version)?;\n                    Ok(Box::new(package))\n                }\n            }\n            Spec::Yarn(version) => {\n                let version = yarn::resolve(version, session)?;\n                Ok(Box::new(Yarn::new(version)))\n            }\n            // When using global package install, we allow the package manager to perform the version resolution\n            Spec::Package(name, version) => {\n                let package = Package::new(name, version)?;\n                Ok(Box::new(package))\n            }\n        }\n    }\n\n    /// Uninstall a tool, removing it from the local inventory\n    ///\n    /// This is implemented on Spec, instead of Resolved, because there is currently no need to\n    /// resolve the specific version before uninstalling a tool.\n    pub fn uninstall(self) -> Fallible<()> {\n        match self {\n            Spec::Node(_) => Err(ErrorKind::Unimplemented {\n                feature: \"Uninstalling node\".into(),\n            }\n            .into()),\n            Spec::Npm(_) => Err(ErrorKind::Unimplemented {\n                feature: \"Uninstalling npm\".into(),\n            }\n            .into()),\n            Spec::Pnpm(_) => {\n                if env::var_os(VOLTA_FEATURE_PNPM).is_some() {\n                    Err(ErrorKind::Unimplemented {\n                        feature: \"Uninstalling pnpm\".into(),\n                    }\n                    .into())\n                } else {\n                    package::uninstall(\"pnpm\")\n                }\n            }\n            Spec::Yarn(_) => Err(ErrorKind::Unimplemented {\n                feature: \"Uninstalling yarn\".into(),\n            }\n            .into()),\n            Spec::Package(name, _) => package::uninstall(&name),\n        }\n    }\n\n    /// The name of the tool, without the version, used for messaging\n    pub fn name(&self) -> &str {\n        match self {\n            Spec::Node(_) => \"Node\",\n            Spec::Npm(_) => \"npm\",\n            Spec::Pnpm(_) => \"pnpm\",\n            Spec::Yarn(_) => \"Yarn\",\n            Spec::Package(name, _) => name,\n        }\n    }\n}\n\nimpl Display for Spec {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        let s = match self {\n            Spec::Node(ref version) => tool_version(\"node\", version),\n            Spec::Npm(ref version) => tool_version(\"npm\", version),\n            Spec::Pnpm(ref version) => tool_version(\"pnpm\", version),\n            Spec::Yarn(ref version) => tool_version(\"yarn\", version),\n            Spec::Package(ref name, ref version) => tool_version(name, version),\n        };\n        f.write_str(&s)\n    }\n}\n\n/// Represents the result of checking if a tool is available locally or not\n///\n/// If a fetch is required, will include an exclusive lock on the Volta directory where possible\nenum FetchStatus {\n    AlreadyFetched,\n    FetchNeeded(Option<VoltaLock>),\n}\n\n/// Uses the supplied `already_fetched` predicate to determine if a tool is available or not.\n///\n/// This uses double-checking logic, to correctly handle concurrent fetch requests:\n///\n/// - If `already_fetched` indicates that a fetch is needed, we acquire an exclusive lock on the Volta directory\n/// - Then, we check _again_, to confirm that no other process completed the fetch while we waited for the lock\n///\n/// Note: If acquiring the lock fails, we proceed anyway, since the fetch is still necessary.\nfn check_fetched<F>(already_fetched: F) -> Fallible<FetchStatus>\nwhere\n    F: Fn() -> Fallible<bool>,\n{\n    if !already_fetched()? {\n        let lock = match VoltaLock::acquire() {\n            Ok(l) => Some(l),\n            Err(_) => {\n                debug!(\"Unable to acquire lock on Volta directory!\");\n                None\n            }\n        };\n\n        if !already_fetched()? {\n            Ok(FetchStatus::FetchNeeded(lock))\n        } else {\n            Ok(FetchStatus::AlreadyFetched)\n        }\n    } else {\n        Ok(FetchStatus::AlreadyFetched)\n    }\n}\n\nfn download_tool_error(tool: Spec, from_url: impl AsRef<str>) -> impl FnOnce() -> ErrorKind {\n    let from_url = from_url.as_ref().to_string();\n    || ErrorKind::DownloadToolNetworkError { tool, from_url }\n}\n\nfn registry_fetch_error(\n    tool: impl AsRef<str>,\n    from_url: impl AsRef<str>,\n) -> impl FnOnce() -> ErrorKind {\n    let tool = tool.as_ref().to_string();\n    let from_url = from_url.as_ref().to_string();\n    || ErrorKind::RegistryFetchError { tool, from_url }\n}\n\ncfg_if!(\n    if #[cfg(windows)] {\n        const PATH_VAR_NAME: &str = \"Path\";\n    } else {\n        const PATH_VAR_NAME: &str = \"PATH\";\n    }\n);\n\n/// Check if a newly-installed shim is first on the PATH. If it isn't, we want to inform the user\n/// that they'll want to move it to the start of PATH to make sure things work as expected.\npub fn check_shim_reachable(shim_name: &str) {\n    let Some(expected_dir) = find_expected_shim_dir(shim_name) else {\n        return;\n    };\n\n    let Ok(resolved) = which::which(shim_name) else {\n        info!(\n            \"{} cannot find command {}. Please ensure that {} is available on your {}.\",\n            note_prefix(),\n            shim_name,\n            expected_dir.display(),\n            PATH_VAR_NAME,\n        );\n        return;\n    };\n\n    if !resolved.starts_with(&expected_dir) {\n        info!(\n            \"{} {} is shadowed by another binary of the same name at {}. To ensure your commands work as expected, please move {} to the start of your {}.\",\n            note_prefix(),\n            shim_name,\n            resolved.display(),\n            expected_dir.display(),\n            PATH_VAR_NAME\n        );\n    }\n}\n\n/// Locate the base directory for the relevant shim in the Volta directories.\n///\n/// On Unix, all of the shims, including the default ones, are installed in `VoltaHome::shim_dir`\n#[cfg(unix)]\nfn find_expected_shim_dir(_shim_name: &str) -> Option<PathBuf> {\n    volta_home().ok().map(|home| home.shim_dir().to_owned())\n}\n\n/// Locate the base directory for the relevant shim in the Volta directories.\n///\n/// On Windows, the default shims (node, npm, yarn, etc.) are installed in `Program Files`\n/// alongside the Volta binaries. To determine where we should be checking, we first look for the\n/// relevant shim inside of `VoltaHome::shim_dir`. If it's there, we use that directory. If it\n/// isn't, we assume it must be a default shim and return `VoltaInstall::root`, which is where\n/// Volta itself is installed.\n#[cfg(windows)]\nfn find_expected_shim_dir(shim_name: &str) -> Option<PathBuf> {\n    use crate::layout::volta_install;\n\n    let home = volta_home().ok()?;\n\n    if home.shim_file(shim_name).exists() {\n        Some(home.shim_dir().to_owned())\n    } else {\n        volta_install()\n            .ok()\n            .map(|install| install.root().to_owned())\n    }\n}\n"
  },
  {
    "path": "crates/volta-core/src/tool/node/fetch.rs",
    "content": "//! Provides fetcher for Node distributions\n\nuse std::fs::{read_to_string, write, File};\nuse std::path::{Path, PathBuf};\n\nuse super::NodeVersion;\nuse crate::error::{Context, ErrorKind, Fallible};\nuse crate::fs::{create_staging_dir, create_staging_file, rename};\nuse crate::hook::ToolHooks;\nuse crate::layout::volta_home;\nuse crate::style::{progress_bar, tool_version};\nuse crate::tool::{self, download_tool_error, Node};\nuse crate::version::{parse_version, VersionSpec};\nuse archive::{self, Archive};\nuse cfg_if::cfg_if;\nuse fs_utils::ensure_containing_dir_exists;\nuse log::debug;\nuse node_semver::Version;\nuse serde::Deserialize;\n\ncfg_if! {\n    if #[cfg(feature = \"mock-network\")] {\n        // TODO: We need to reconsider our mocking strategy in light of mockito deprecating the\n        // SERVER_URL constant: Since our acceptance tests run the binary in a separate process,\n        // we can't use `mockito::server_url()`, which relies on shared memory.\n        fn public_node_server_root() -> String {\n            #[allow(deprecated)]\n            mockito::SERVER_URL.to_string()\n        }\n    } else {\n        fn public_node_server_root() -> String {\n            \"https://nodejs.org/dist\".to_string()\n        }\n    }\n}\n\nfn npm_manifest_path(version: &Version) -> PathBuf {\n    let mut manifest = PathBuf::from(Node::archive_basename(version));\n\n    #[cfg(unix)]\n    manifest.push(\"lib\");\n\n    manifest.push(\"node_modules\");\n    manifest.push(\"npm\");\n    manifest.push(\"package.json\");\n\n    manifest\n}\n\npub fn fetch(version: &Version, hooks: Option<&ToolHooks<Node>>) -> Fallible<NodeVersion> {\n    let home = volta_home()?;\n    let node_dir = home.node_inventory_dir();\n    let cache_file = node_dir.join(Node::archive_filename(version));\n\n    let (archive, staging) = match load_cached_distro(&cache_file) {\n        Some(archive) => {\n            debug!(\n                \"Loading {} from cached archive at '{}'\",\n                tool_version(\"node\", version),\n                cache_file.display()\n            );\n            (archive, None)\n        }\n        None => {\n            let staging = create_staging_file()?;\n            let remote_url = determine_remote_url(version, hooks)?;\n            let archive = fetch_remote_distro(version, &remote_url, staging.path())?;\n            (archive, Some(staging))\n        }\n    };\n\n    let node_version = unpack_archive(archive, version)?;\n\n    if let Some(staging_file) = staging {\n        ensure_containing_dir_exists(&cache_file).with_context(|| {\n            ErrorKind::ContainingDirError {\n                path: cache_file.clone(),\n            }\n        })?;\n        staging_file\n            .persist(cache_file)\n            .with_context(|| ErrorKind::PersistInventoryError {\n                tool: \"Node\".into(),\n            })?;\n    }\n\n    Ok(node_version)\n}\n\n/// Unpack the node archive into the image directory so that it is ready for use\nfn unpack_archive(archive: Box<dyn Archive>, version: &Version) -> Fallible<NodeVersion> {\n    let temp = create_staging_dir()?;\n    debug!(\"Unpacking node into '{}'\", temp.path().display());\n\n    let progress = progress_bar(\n        archive.origin(),\n        &tool_version(\"node\", version),\n        archive.compressed_size(),\n    );\n    let version_string = version.to_string();\n\n    archive\n        .unpack(temp.path(), &mut |_, read| {\n            progress.inc(read as u64);\n        })\n        .with_context(|| ErrorKind::UnpackArchiveError {\n            tool: \"Node\".into(),\n            version: version_string.clone(),\n        })?;\n\n    // Save the npm version number in the npm version file for this distro\n    let npm_package_json = temp.path().join(npm_manifest_path(version));\n    let npm = Manifest::version(&npm_package_json)?;\n    save_default_npm_version(version, &npm)?;\n\n    let dest = volta_home()?.node_image_dir(&version_string);\n    ensure_containing_dir_exists(&dest)\n        .with_context(|| ErrorKind::ContainingDirError { path: dest.clone() })?;\n\n    rename(temp.path().join(Node::archive_basename(version)), &dest).with_context(|| {\n        ErrorKind::SetupToolImageError {\n            tool: \"Node\".into(),\n            version: version_string,\n            dir: dest.clone(),\n        }\n    })?;\n\n    progress.finish_and_clear();\n\n    // Note: We write these after the progress bar is finished to avoid display bugs with re-renders of the progress\n    debug!(\"Saving bundled npm version ({})\", npm);\n    debug!(\"Installing node in '{}'\", dest.display());\n\n    Ok(NodeVersion {\n        runtime: version.clone(),\n        npm,\n    })\n}\n\n/// Return the archive if it is valid. It may have been corrupted or interrupted in the middle of\n/// downloading.\n// ISSUE(#134) - verify checksum\nfn load_cached_distro(file: &Path) -> Option<Box<dyn Archive>> {\n    if file.is_file() {\n        let file = File::open(file).ok()?;\n        archive::load_native(file).ok()\n    } else {\n        None\n    }\n}\n\n/// Determine the remote URL to download from, using the hooks if available\nfn determine_remote_url(version: &Version, hooks: Option<&ToolHooks<Node>>) -> Fallible<String> {\n    let distro_file_name = Node::archive_filename(version);\n    match hooks {\n        Some(&ToolHooks {\n            distro: Some(ref hook),\n            ..\n        }) => {\n            debug!(\"Using node.distro hook to determine download URL\");\n            hook.resolve(version, &distro_file_name)\n        }\n        _ => Ok(format!(\n            \"{}/v{}/{}\",\n            public_node_server_root(),\n            version,\n            distro_file_name\n        )),\n    }\n}\n\n/// Fetch the distro archive from the internet\nfn fetch_remote_distro(\n    version: &Version,\n    url: &str,\n    staging_path: &Path,\n) -> Fallible<Box<dyn Archive>> {\n    debug!(\"Downloading {} from {}\", tool_version(\"node\", version), url);\n    archive::fetch_native(url, staging_path).with_context(download_tool_error(\n        tool::Spec::Node(VersionSpec::Exact(version.clone())),\n        url,\n    ))\n}\n\n/// The portion of npm's `package.json` file that we care about\n#[derive(Deserialize)]\nstruct Manifest {\n    version: String,\n}\n\nimpl Manifest {\n    /// Parse the version out of a package.json file\n    fn version(path: &Path) -> Fallible<Version> {\n        let file = File::open(path).with_context(|| ErrorKind::ReadNpmManifestError)?;\n        let manifest: Manifest =\n            serde_json::de::from_reader(file).with_context(|| ErrorKind::ParseNpmManifestError)?;\n        parse_version(manifest.version)\n    }\n}\n\n/// Load the local npm version file to determine the default npm version for a given version of Node\npub fn load_default_npm_version(node: &Version) -> Fallible<Version> {\n    let npm_version_file_path = volta_home()?.node_npm_version_file(&node.to_string());\n    let npm_version =\n        read_to_string(&npm_version_file_path).with_context(|| ErrorKind::ReadDefaultNpmError {\n            file: npm_version_file_path,\n        })?;\n    parse_version(npm_version)\n}\n\n/// Save the default npm version to the filesystem for a given version of Node\nfn save_default_npm_version(node: &Version, npm: &Version) -> Fallible<()> {\n    let npm_version_file_path = volta_home()?.node_npm_version_file(&node.to_string());\n    write(&npm_version_file_path, npm.to_string().as_bytes()).with_context(|| {\n        ErrorKind::WriteDefaultNpmError {\n            file: npm_version_file_path,\n        }\n    })\n}\n"
  },
  {
    "path": "crates/volta-core/src/tool/node/metadata.rs",
    "content": "use std::collections::HashSet;\n\nuse super::NODE_DISTRO_IDENTIFIER;\n#[cfg(any(\n    all(target_os = \"macos\", target_arch = \"aarch64\"),\n    all(target_os = \"windows\", target_arch = \"aarch64\")\n))]\nuse super::NODE_DISTRO_IDENTIFIER_FALLBACK;\nuse crate::version::{option_version_serde, version_serde};\nuse node_semver::Version;\nuse serde::{Deserialize, Deserializer};\n\n/// The index of the public Node server.\npub struct NodeIndex {\n    pub(super) entries: Vec<NodeEntry>,\n}\n\n#[derive(Debug)]\npub struct NodeEntry {\n    pub version: Version,\n    pub lts: bool,\n}\n\n#[derive(Deserialize)]\npub struct RawNodeIndex(Vec<RawNodeEntry>);\n\n#[derive(Deserialize)]\npub struct RawNodeEntry {\n    #[serde(with = \"version_serde\")]\n    version: Version,\n    #[serde(default)] // handles Option\n    #[serde(with = \"option_version_serde\")]\n    npm: Option<Version>,\n    files: HashSet<String>,\n    #[serde(deserialize_with = \"lts_version_serde\")]\n    lts: bool,\n}\n\nimpl From<RawNodeIndex> for NodeIndex {\n    fn from(raw: RawNodeIndex) -> NodeIndex {\n        let entries = raw\n            .0\n            .into_iter()\n            .filter_map(|entry| {\n                #[cfg(not(any(\n                    all(target_os = \"macos\", target_arch = \"aarch64\"),\n                    all(target_os = \"windows\", target_arch = \"aarch64\")\n                )))]\n                if entry.npm.is_some() && entry.files.contains(NODE_DISTRO_IDENTIFIER) {\n                    Some(NodeEntry {\n                        version: entry.version,\n                        lts: entry.lts,\n                    })\n                } else {\n                    None\n                }\n\n                #[cfg(any(\n                    all(target_os = \"macos\", target_arch = \"aarch64\"),\n                    all(target_os = \"windows\", target_arch = \"aarch64\")\n                ))]\n                if entry.npm.is_some()\n                    && (entry.files.contains(NODE_DISTRO_IDENTIFIER)\n                        || entry.files.contains(NODE_DISTRO_IDENTIFIER_FALLBACK))\n                {\n                    Some(NodeEntry {\n                        version: entry.version,\n                        lts: entry.lts,\n                    })\n                } else {\n                    None\n                }\n            })\n            .collect();\n\n        NodeIndex { entries }\n    }\n}\n\n#[allow(clippy::unnecessary_wraps)] // Needs to match the API expected by Serde\nfn lts_version_serde<'de, D>(deserializer: D) -> Result<bool, D::Error>\nwhere\n    D: Deserializer<'de>,\n{\n    match String::deserialize(deserializer) {\n        Ok(_) => Ok(true),\n        Err(_) => Ok(false),\n    }\n}\n"
  },
  {
    "path": "crates/volta-core/src/tool/node/mod.rs",
    "content": "use std::fmt::{self, Display};\n\nuse super::{\n    check_fetched, check_shim_reachable, debug_already_fetched, info_fetched, info_installed,\n    info_pinned, info_project_version, FetchStatus, Tool,\n};\nuse crate::error::{ErrorKind, Fallible};\nuse crate::inventory::node_available;\nuse crate::session::Session;\nuse crate::style::{note_prefix, tool_version};\nuse crate::sync::VoltaLock;\nuse cfg_if::cfg_if;\nuse log::info;\nuse node_semver::Version;\n\nmod fetch;\nmod metadata;\nmod resolve;\n\npub use fetch::load_default_npm_version;\npub use resolve::resolve;\n\ncfg_if! {\n    if #[cfg(all(target_os = \"windows\", target_arch = \"x86\"))] {\n        /// The OS component of a Node distro filename\n        pub const NODE_DISTRO_OS: &str = \"win\";\n        /// The architecture component of a Node distro filename\n        pub const NODE_DISTRO_ARCH: &str = \"x86\";\n        /// The extension for Node distro files\n        pub const NODE_DISTRO_EXTENSION: &str = \"zip\";\n        /// The file identifier in the Node index `files` array\n        pub const NODE_DISTRO_IDENTIFIER: &str = \"win-x86-zip\";\n    } else if #[cfg(all(target_os = \"windows\", target_arch = \"x86_64\"))] {\n        /// The OS component of a Node distro filename\n        pub const NODE_DISTRO_OS: &str = \"win\";\n        /// The architecture component of a Node distro filename\n        pub const NODE_DISTRO_ARCH: &str = \"x64\";\n        /// The extension for Node distro files\n        pub const NODE_DISTRO_EXTENSION: &str = \"zip\";\n        /// The file identifier in the Node index `files` array\n        pub const NODE_DISTRO_IDENTIFIER: &str = \"win-x64-zip\";\n    } else if #[cfg(all(target_os = \"windows\", target_arch = \"aarch64\"))] {\n        /// The OS component of a Node distro filename\n        pub const NODE_DISTRO_OS: &str = \"win\";\n        /// The architecture component of a Node distro filename\n        pub const NODE_DISTRO_ARCH: &str = \"arm64\";\n        /// The extension for Node distro files\n        pub const NODE_DISTRO_EXTENSION: &str = \"zip\";\n        /// The file identifier in the Node index `files` array\n        pub const NODE_DISTRO_IDENTIFIER: &str = \"win-arm64-zip\";\n\n        // NOTE: Node support for pre-built ARM64 binaries on Windows was added in major version 20\n        // For versions prior to that, we need to fall back on the x64 binaries via emulator\n\n        /// The fallback architecture component of a Node distro filename\n        pub const NODE_DISTRO_ARCH_FALLBACK: &str = \"x64\";\n        /// The fallback file identifier in the Node index `files` array\n        pub const NODE_DISTRO_IDENTIFIER_FALLBACK: &str = \"win-x64-zip\";\n    } else if #[cfg(all(target_os = \"macos\", target_arch = \"x86_64\"))] {\n        /// The OS component of a Node distro filename\n        pub const NODE_DISTRO_OS: &str = \"darwin\";\n        /// The architecture component of a Node distro filename\n        pub const NODE_DISTRO_ARCH: &str = \"x64\";\n        /// The extension for Node distro files\n        pub const NODE_DISTRO_EXTENSION: &str = \"tar.gz\";\n        /// The file identifier in the Node index `files` array\n        pub const NODE_DISTRO_IDENTIFIER: &str = \"osx-x64-tar\";\n    } else if #[cfg(all(target_os = \"macos\", target_arch = \"aarch64\"))] {\n        /// The OS component of a Node distro filename\n        pub const NODE_DISTRO_OS: &str = \"darwin\";\n        /// The architecture component of a Node distro filename\n        pub const NODE_DISTRO_ARCH: &str = \"arm64\";\n        /// The extension for Node distro files\n        pub const NODE_DISTRO_EXTENSION: &str = \"tar.gz\";\n        /// The file identifier in the Node index `files` array\n        pub const NODE_DISTRO_IDENTIFIER: &str = \"osx-arm64-tar\";\n\n        // NOTE: Node support for pre-built Apple Silicon binaries was added in major version 16\n        // For versions prior to that, we need to fall back on the x64 binaries via Rosetta 2\n\n        /// The fallback architecture component of a Node distro filename\n        pub const NODE_DISTRO_ARCH_FALLBACK: &str = \"x64\";\n        /// The fallback file identifier in the Node index `files` array\n        pub const NODE_DISTRO_IDENTIFIER_FALLBACK: &str = \"osx-x64-tar\";\n    } else if #[cfg(all(target_os = \"linux\", target_arch = \"x86_64\"))] {\n        /// The OS component of a Node distro filename\n        pub const NODE_DISTRO_OS: &str = \"linux\";\n        /// The architecture component of a Node distro filename\n        pub const NODE_DISTRO_ARCH: &str = \"x64\";\n        /// The extension for Node distro files\n        pub const NODE_DISTRO_EXTENSION: &str = \"tar.gz\";\n        /// The file identifier in the Node index `files` array\n        pub const NODE_DISTRO_IDENTIFIER: &str = \"linux-x64\";\n    } else if #[cfg(all(target_os = \"linux\", target_arch = \"aarch64\"))] {\n        /// The OS component of a Node distro filename\n        pub const NODE_DISTRO_OS: &str = \"linux\";\n        /// The architecture component of a Node distro filename\n        pub const NODE_DISTRO_ARCH: &str = \"arm64\";\n        /// The extension for Node distro files\n        pub const NODE_DISTRO_EXTENSION: &str = \"tar.gz\";\n        /// The file identifier in the Node index `files` array\n        pub const NODE_DISTRO_IDENTIFIER: &str = \"linux-arm64\";\n    } else if #[cfg(all(target_os = \"linux\", target_arch = \"arm\"))] {\n        /// The OS component of a Node distro filename\n        pub const NODE_DISTRO_OS: &str = \"linux\";\n        /// The architecture component of a Node distro filename\n        pub const NODE_DISTRO_ARCH: &str = \"armv7l\";\n        /// The extension for Node distro files\n        pub const NODE_DISTRO_EXTENSION: &str = \"tar.gz\";\n        /// The file identifier in the Node index `files` array\n        pub const NODE_DISTRO_IDENTIFIER: &str = \"linux-armv7l\";\n    } else {\n        compile_error!(\"Unsuppored operating system + architecture combination\");\n    }\n}\n\n/// A full Node version including not just the version of Node itself\n/// but also the specific version of npm installed globally with that\n/// Node installation.\n#[derive(Clone, Debug)]\npub struct NodeVersion {\n    /// The version of Node itself.\n    pub runtime: Version,\n    /// The npm version globally installed with the Node distro.\n    pub npm: Version,\n}\n\nimpl Display for NodeVersion {\n    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {\n        write!(\n            f,\n            \"{} (with {})\",\n            tool_version(\"node\", &self.runtime),\n            tool_version(\"npm\", &self.npm)\n        )\n    }\n}\n\n/// The Tool implementation for fetching and installing Node\npub struct Node {\n    pub(super) version: Version,\n}\n\nimpl Node {\n    pub fn new(version: Version) -> Self {\n        Node { version }\n    }\n\n    #[cfg(not(any(\n        all(target_os = \"macos\", target_arch = \"aarch64\"),\n        all(target_os = \"windows\", target_arch = \"aarch64\")\n    )))]\n    pub fn archive_basename(version: &Version) -> String {\n        format!(\"node-v{}-{}-{}\", version, NODE_DISTRO_OS, NODE_DISTRO_ARCH)\n    }\n\n    #[cfg(all(target_os = \"macos\", target_arch = \"aarch64\"))]\n    pub fn archive_basename(version: &Version) -> String {\n        // Note: Node began shipping pre-built binaries for Apple Silicon with Major version 16\n        // Prior to that, we need to fall back on the x64 binaries\n        format!(\n            \"node-v{}-{}-{}\",\n            version,\n            NODE_DISTRO_OS,\n            if version.major >= 16 {\n                NODE_DISTRO_ARCH\n            } else {\n                NODE_DISTRO_ARCH_FALLBACK\n            }\n        )\n    }\n\n    #[cfg(all(target_os = \"windows\", target_arch = \"aarch64\"))]\n    pub fn archive_basename(version: &Version) -> String {\n        // Note: Node began shipping pre-built binaries for Windows ARM with Major version 20\n        // Prior to that, we need to fall back on the x64 binaries\n        format!(\n            \"node-v{}-{}-{}\",\n            version,\n            NODE_DISTRO_OS,\n            if version.major >= 20 {\n                NODE_DISTRO_ARCH\n            } else {\n                NODE_DISTRO_ARCH_FALLBACK\n            }\n        )\n    }\n\n    pub fn archive_filename(version: &Version) -> String {\n        format!(\n            \"{}.{}\",\n            Node::archive_basename(version),\n            NODE_DISTRO_EXTENSION\n        )\n    }\n\n    pub(crate) fn ensure_fetched(&self, session: &mut Session) -> Fallible<NodeVersion> {\n        match check_fetched(|| node_available(&self.version))? {\n            FetchStatus::AlreadyFetched => {\n                debug_already_fetched(self);\n                let npm = fetch::load_default_npm_version(&self.version)?;\n\n                Ok(NodeVersion {\n                    runtime: self.version.clone(),\n                    npm,\n                })\n            }\n            FetchStatus::FetchNeeded(_lock) => fetch::fetch(&self.version, session.hooks()?.node()),\n        }\n    }\n}\n\nimpl Tool for Node {\n    fn fetch(self: Box<Self>, session: &mut Session) -> Fallible<()> {\n        let node_version = self.ensure_fetched(session)?;\n\n        info_fetched(node_version);\n        Ok(())\n    }\n    fn install(self: Box<Self>, session: &mut Session) -> Fallible<()> {\n        // Acquire a lock on the Volta directory, if possible, to prevent concurrent changes\n        let _lock = VoltaLock::acquire();\n        let node_version = self.ensure_fetched(session)?;\n\n        let default_toolchain = session.toolchain_mut()?;\n        default_toolchain.set_active_node(&self.version)?;\n\n        // If the user has a default version of `npm`, we shouldn't show the \"(with npm@X.Y.ZZZ)\" text in the success message\n        // Instead we should check if the bundled version is higher than the default and inform the user\n        // Note: The previous line ensures that there will be a default platform\n        if let Some(default_npm) = &default_toolchain.platform().unwrap().npm {\n            info_installed(&self); // includes node version\n\n            if node_version.npm > *default_npm {\n                info!(\"{} this version of Node includes {}, which is higher than your default version ({}).\n      To use the version included with Node, run `volta install npm@bundled`\",\n                    note_prefix(),\n                    tool_version(\"npm\", node_version.npm),\n                    default_npm.to_string()\n                );\n            }\n        } else {\n            info_installed(node_version); // includes node and npm version\n        }\n\n        check_shim_reachable(\"node\");\n\n        if let Ok(Some(project)) = session.project_platform() {\n            info_project_version(tool_version(\"node\", &project.node), &self);\n        }\n\n        Ok(())\n    }\n    fn pin(self: Box<Self>, session: &mut Session) -> Fallible<()> {\n        if session.project()?.is_some() {\n            let node_version = self.ensure_fetched(session)?;\n\n            // Note: We know this will succeed, since we checked above\n            let project = session.project_mut()?.unwrap();\n            project.pin_node(self.version.clone())?;\n\n            // If the user has a pinned version of `npm`, we shouldn't show the \"(with npm@X.Y.ZZZ)\" text in the success message\n            // Instead we should check if the bundled version is higher than the pinned and inform the user\n            // Note: The pin operation guarantees there will be a platform\n            if let Some(pinned_npm) = &project.platform().unwrap().npm {\n                info_pinned(self); // includes node version\n\n                if node_version.npm > *pinned_npm {\n                    info!(\"{} this version of Node includes {}, which is higher than your pinned version ({}).\n      To use the version included with Node, run `volta pin npm@bundled`\",\n                        note_prefix(),\n                        tool_version(\"npm\", node_version.npm),\n                        pinned_npm.to_string()\n                    );\n                }\n            } else {\n                info_pinned(node_version); // includes node and npm version\n            }\n\n            Ok(())\n        } else {\n            Err(ErrorKind::NotInPackage.into())\n        }\n    }\n}\n\nimpl Display for Node {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        f.write_str(&tool_version(\"node\", &self.version))\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_node_archive_basename() {\n        assert_eq!(\n            Node::archive_basename(&Version::parse(\"20.2.3\").unwrap()),\n            format!(\"node-v20.2.3-{}-{}\", NODE_DISTRO_OS, NODE_DISTRO_ARCH)\n        );\n    }\n\n    #[test]\n    fn test_node_archive_filename() {\n        assert_eq!(\n            Node::archive_filename(&Version::parse(\"20.2.3\").unwrap()),\n            format!(\n                \"node-v20.2.3-{}-{}.{}\",\n                NODE_DISTRO_OS, NODE_DISTRO_ARCH, NODE_DISTRO_EXTENSION\n            )\n        );\n    }\n\n    #[test]\n    #[cfg(all(target_os = \"macos\", target_arch = \"aarch64\"))]\n    fn test_fallback_node_archive_basename() {\n        assert_eq!(\n            Node::archive_basename(&Version::parse(\"15.2.3\").unwrap()),\n            format!(\n                \"node-v15.2.3-{}-{}\",\n                NODE_DISTRO_OS, NODE_DISTRO_ARCH_FALLBACK\n            )\n        );\n    }\n\n    #[test]\n    #[cfg(all(target_os = \"windows\", target_arch = \"aarch64\"))]\n    fn test_fallback_node_archive_basename() {\n        assert_eq!(\n            Node::archive_basename(&Version::parse(\"19.2.3\").unwrap()),\n            format!(\n                \"node-v19.2.3-{}-{}\",\n                NODE_DISTRO_OS, NODE_DISTRO_ARCH_FALLBACK\n            )\n        );\n    }\n\n    #[test]\n    #[cfg(all(target_os = \"macos\", target_arch = \"aarch64\"))]\n    fn test_fallback_node_archive_filename() {\n        assert_eq!(\n            Node::archive_filename(&Version::parse(\"15.2.3\").unwrap()),\n            format!(\n                \"node-v15.2.3-{}-{}.{}\",\n                NODE_DISTRO_OS, NODE_DISTRO_ARCH_FALLBACK, NODE_DISTRO_EXTENSION\n            )\n        );\n    }\n\n    #[test]\n    #[cfg(all(target_os = \"windows\", target_arch = \"aarch64\"))]\n    fn test_fallback_node_archive_filename() {\n        assert_eq!(\n            Node::archive_filename(&Version::parse(\"19.2.3\").unwrap()),\n            format!(\n                \"node-v19.2.3-{}-{}.{}\",\n                NODE_DISTRO_OS, NODE_DISTRO_ARCH_FALLBACK, NODE_DISTRO_EXTENSION\n            )\n        );\n    }\n}\n"
  },
  {
    "path": "crates/volta-core/src/tool/node/resolve.rs",
    "content": "//! Provides resolution of Node requirements into specific versions, using the NodeJS index\n\nuse std::fs::File;\nuse std::io::Write;\nuse std::time::{Duration, SystemTime};\n\nuse super::super::registry_fetch_error;\nuse super::metadata::{NodeEntry, NodeIndex, RawNodeIndex};\nuse crate::error::{Context, ErrorKind, Fallible};\nuse crate::fs::{create_staging_file, read_file};\nuse crate::hook::ToolHooks;\nuse crate::layout::volta_home;\nuse crate::session::Session;\nuse crate::style::progress_spinner;\nuse crate::tool::Node;\nuse crate::version::{VersionSpec, VersionTag};\nuse attohttpc::header::HeaderMap;\nuse attohttpc::Response;\nuse cfg_if::cfg_if;\nuse fs_utils::ensure_containing_dir_exists;\nuse headers::{CacheControl, Expires, HeaderMapExt};\nuse log::debug;\nuse node_semver::{Range, Version};\n\n// ISSUE (#86): Move public repository URLs to config file\ncfg_if! {\n    if #[cfg(feature = \"mock-network\")] {\n        // TODO: We need to reconsider our mocking strategy in light of mockito deprecating the\n        // SERVER_URL constant: Since our acceptance tests run the binary in a separate process,\n        // we can't use `mockito::server_url()`, which relies on shared memory.\n        #[allow(deprecated)]\n        const SERVER_URL: &str = mockito::SERVER_URL;\n        fn public_node_version_index() -> String {\n            format!(\"{}/node-dist/index.json\", SERVER_URL)\n        }\n    } else {\n        /// Returns the URL of the index of available Node versions on the public Node server.\n        fn public_node_version_index() -> String {\n            \"https://nodejs.org/dist/index.json\".to_string()\n        }\n    }\n}\n\npub fn resolve(matching: VersionSpec, session: &mut Session) -> Fallible<Version> {\n    let hooks = session.hooks()?.node();\n    match matching {\n        VersionSpec::Semver(requirement) => resolve_semver(requirement, hooks),\n        VersionSpec::Exact(version) => Ok(version),\n        VersionSpec::None | VersionSpec::Tag(VersionTag::Lts) => resolve_lts(hooks),\n        VersionSpec::Tag(VersionTag::Latest) => resolve_latest(hooks),\n        // Node doesn't have \"tagged\" versions (apart from 'latest' and 'lts'), so custom tags will always be an error\n        VersionSpec::Tag(VersionTag::Custom(tag)) => {\n            Err(ErrorKind::NodeVersionNotFound { matching: tag }.into())\n        }\n    }\n}\n\nfn resolve_latest(hooks: Option<&ToolHooks<Node>>) -> Fallible<Version> {\n    // NOTE: This assumes the registry always produces a list in sorted order\n    //       from newest to oldest. This should be specified as a requirement\n    //       when we document the plugin API.\n    let url = match hooks {\n        Some(&ToolHooks {\n            latest: Some(ref hook),\n            ..\n        }) => {\n            debug!(\"Using node.latest hook to determine node index URL\");\n            hook.resolve(\"index.json\")?\n        }\n        _ => public_node_version_index(),\n    };\n    let version_opt = match_node_version(&url, |_| true)?;\n\n    match version_opt {\n        Some(version) => {\n            debug!(\"Found latest node version ({}) from {}\", version, url);\n            Ok(version)\n        }\n        None => Err(ErrorKind::NodeVersionNotFound {\n            matching: \"latest\".into(),\n        }\n        .into()),\n    }\n}\n\nfn resolve_lts(hooks: Option<&ToolHooks<Node>>) -> Fallible<Version> {\n    let url = match hooks {\n        Some(&ToolHooks {\n            index: Some(ref hook),\n            ..\n        }) => {\n            debug!(\"Using node.index hook to determine node index URL\");\n            hook.resolve(\"index.json\")?\n        }\n        _ => public_node_version_index(),\n    };\n    let version_opt = match_node_version(&url, |&NodeEntry { lts, .. }| lts)?;\n\n    match version_opt {\n        Some(version) => {\n            debug!(\"Found newest LTS node version ({}) from {}\", version, url);\n            Ok(version)\n        }\n        None => Err(ErrorKind::NodeVersionNotFound {\n            matching: \"lts\".into(),\n        }\n        .into()),\n    }\n}\n\nfn resolve_semver(matching: Range, hooks: Option<&ToolHooks<Node>>) -> Fallible<Version> {\n    let url = match hooks {\n        Some(&ToolHooks {\n            index: Some(ref hook),\n            ..\n        }) => {\n            debug!(\"Using node.index hook to determine node index URL\");\n            hook.resolve(\"index.json\")?\n        }\n        _ => public_node_version_index(),\n    };\n    let version_opt = match_node_version(&url, |NodeEntry { version, .. }| {\n        matching.satisfies(version)\n    })?;\n\n    match version_opt {\n        Some(version) => {\n            debug!(\n                \"Found node@{} matching requirement '{}' from {}\",\n                version, matching, url\n            );\n            Ok(version)\n        }\n        None => Err(ErrorKind::NodeVersionNotFound {\n            matching: matching.to_string(),\n        }\n        .into()),\n    }\n}\n\nfn match_node_version(\n    url: &str,\n    predicate: impl Fn(&NodeEntry) -> bool,\n) -> Fallible<Option<Version>> {\n    let index: NodeIndex = resolve_node_versions(url)?.into();\n    let mut entries = index.entries.into_iter();\n    Ok(entries\n        .find(predicate)\n        .map(|NodeEntry { version, .. }| version))\n}\n\n/// Reads a public index from the Node cache, if it exists and hasn't expired.\nfn read_cached_opt(url: &str) -> Fallible<Option<RawNodeIndex>> {\n    let expiry_file = volta_home()?.node_index_expiry_file();\n    let expiry = read_file(expiry_file).with_context(|| ErrorKind::ReadNodeIndexExpiryError {\n        file: expiry_file.to_owned(),\n    })?;\n\n    if !expiry\n        .map(|date| httpdate::parse_http_date(&date))\n        .transpose()\n        .with_context(|| ErrorKind::ParseNodeIndexExpiryError)?\n        .is_some_and(|expiry_date| SystemTime::now() < expiry_date)\n    {\n        return Ok(None);\n    };\n\n    let index_file = volta_home()?.node_index_file();\n    let cached = read_file(index_file).with_context(|| ErrorKind::ReadNodeIndexCacheError {\n        file: index_file.to_owned(),\n    })?;\n\n    let Some(json) = cached\n        .as_ref()\n        .and_then(|content| content.strip_prefix(url))\n    else {\n        return Ok(None);\n    };\n\n    serde_json::de::from_str(json).with_context(|| ErrorKind::ParseNodeIndexCacheError)\n}\n\n/// Get the cache max-age of an HTTP response.\nfn max_age(headers: &HeaderMap) -> Duration {\n    const FOUR_HOURS: Duration = Duration::from_secs(4 * 60 * 60);\n    headers\n        .typed_get::<CacheControl>()\n        .and_then(|cache_control| cache_control.max_age())\n        .unwrap_or(FOUR_HOURS)\n}\n\nfn resolve_node_versions(url: &str) -> Fallible<RawNodeIndex> {\n    match read_cached_opt(url)? {\n        Some(serial) => {\n            debug!(\"Found valid cache of Node version index\");\n            Ok(serial)\n        }\n        None => {\n            debug!(\"Node index cache was not found or was invalid\");\n            let spinner = progress_spinner(format!(\"Fetching public registry: {}\", url));\n\n            let (_, headers, response) = attohttpc::get(url)\n                .send()\n                .and_then(Response::error_for_status)\n                .with_context(registry_fetch_error(\"Node\", url))?\n                .split();\n\n            let expires = headers\n                .typed_get::<Expires>()\n                .map(SystemTime::from)\n                .unwrap_or_else(|| SystemTime::now() + max_age(&headers));\n\n            let response_text = response\n                .text()\n                .with_context(registry_fetch_error(\"Node\", url))?;\n\n            let index: RawNodeIndex =\n                serde_json::de::from_str(&response_text).with_context(|| {\n                    ErrorKind::ParseNodeIndexError {\n                        from_url: url.to_string(),\n                    }\n                })?;\n\n            let cached = create_staging_file()?;\n\n            let mut cached_file: &File = cached.as_file();\n            writeln!(cached_file, \"{}\", url)\n                .and_then(|_| cached_file.write(response_text.as_bytes()))\n                .with_context(|| ErrorKind::WriteNodeIndexCacheError {\n                    file: cached.path().to_path_buf(),\n                })?;\n\n            let index_cache_file = volta_home()?.node_index_file();\n            ensure_containing_dir_exists(&index_cache_file).with_context(|| {\n                ErrorKind::ContainingDirError {\n                    path: index_cache_file.to_owned(),\n                }\n            })?;\n            cached.persist(index_cache_file).with_context(|| {\n                ErrorKind::WriteNodeIndexCacheError {\n                    file: index_cache_file.to_owned(),\n                }\n            })?;\n\n            let expiry = create_staging_file()?;\n            let mut expiry_file: &File = expiry.as_file();\n\n            write!(expiry_file, \"{}\", httpdate::fmt_http_date(expires)).with_context(|| {\n                ErrorKind::WriteNodeIndexExpiryError {\n                    file: expiry.path().to_path_buf(),\n                }\n            })?;\n\n            let index_expiry_file = volta_home()?.node_index_expiry_file();\n            ensure_containing_dir_exists(&index_expiry_file).with_context(|| {\n                ErrorKind::ContainingDirError {\n                    path: index_expiry_file.to_owned(),\n                }\n            })?;\n            expiry.persist(index_expiry_file).with_context(|| {\n                ErrorKind::WriteNodeIndexExpiryError {\n                    file: index_expiry_file.to_owned(),\n                }\n            })?;\n\n            spinner.finish_and_clear();\n            Ok(index)\n        }\n    }\n}\n"
  },
  {
    "path": "crates/volta-core/src/tool/npm/fetch.rs",
    "content": "//! Provides fetcher for npm distributions\n\nuse std::fs::{write, File};\nuse std::path::Path;\n\nuse super::super::download_tool_error;\nuse super::super::registry::public_registry_package;\nuse crate::error::{Context, ErrorKind, Fallible};\nuse crate::fs::{create_staging_dir, create_staging_file, rename, set_executable};\nuse crate::hook::ToolHooks;\nuse crate::layout::volta_home;\nuse crate::style::{progress_bar, tool_version};\nuse crate::tool::{self, Npm};\nuse crate::version::VersionSpec;\nuse archive::{Archive, Tarball};\nuse fs_utils::ensure_containing_dir_exists;\nuse log::debug;\nuse node_semver::Version;\n\npub fn fetch(version: &Version, hooks: Option<&ToolHooks<Npm>>) -> Fallible<()> {\n    let npm_dir = volta_home()?.npm_inventory_dir();\n    let cache_file = npm_dir.join(Npm::archive_filename(&version.to_string()));\n\n    let (archive, staging) = match load_cached_distro(&cache_file) {\n        Some(archive) => {\n            debug!(\n                \"Loading {} from cached archive at '{}'\",\n                tool_version(\"npm\", version),\n                cache_file.display()\n            );\n            (archive, None)\n        }\n        None => {\n            let staging = create_staging_file()?;\n            let remote_url = determine_remote_url(version, hooks)?;\n            let archive = fetch_remote_distro(version, &remote_url, staging.path())?;\n            (archive, Some(staging))\n        }\n    };\n\n    unpack_archive(archive, version)?;\n\n    if let Some(staging_file) = staging {\n        ensure_containing_dir_exists(&cache_file).with_context(|| {\n            ErrorKind::ContainingDirError {\n                path: cache_file.clone(),\n            }\n        })?;\n        staging_file\n            .persist(cache_file)\n            .with_context(|| ErrorKind::PersistInventoryError { tool: \"npm\".into() })?;\n    }\n\n    Ok(())\n}\n\n/// Unpack the npm archive into the image directory so that it is ready for use\nfn unpack_archive(archive: Box<dyn Archive>, version: &Version) -> Fallible<()> {\n    let temp = create_staging_dir()?;\n    debug!(\"Unpacking npm into '{}'\", temp.path().display());\n\n    let progress = progress_bar(\n        archive.origin(),\n        &tool_version(\"npm\", version),\n        archive.compressed_size(),\n    );\n    let version_string = version.to_string();\n\n    archive\n        .unpack(temp.path(), &mut |_, read| {\n            progress.inc(read as u64);\n        })\n        .with_context(|| ErrorKind::UnpackArchiveError {\n            tool: \"npm\".into(),\n            version: version_string.clone(),\n        })?;\n\n    let bin_path = temp.path().join(\"package\").join(\"bin\");\n    overwrite_launcher(&bin_path, \"npm\")?;\n    overwrite_launcher(&bin_path, \"npx\")?;\n\n    #[cfg(windows)]\n    {\n        overwrite_cmd_launcher(&bin_path, \"npm\")?;\n        overwrite_cmd_launcher(&bin_path, \"npx\")?;\n    }\n\n    let dest = volta_home()?.npm_image_dir(&version_string);\n    ensure_containing_dir_exists(&dest)\n        .with_context(|| ErrorKind::ContainingDirError { path: dest.clone() })?;\n\n    rename(temp.path().join(\"package\"), &dest).with_context(|| ErrorKind::SetupToolImageError {\n        tool: \"npm\".into(),\n        version: version_string.clone(),\n        dir: dest.clone(),\n    })?;\n\n    progress.finish_and_clear();\n\n    // Note: We write this after the progress bar is finished to avoid display bugs with re-renders of the progress\n    debug!(\"Installing npm in '{}'\", dest.display());\n\n    Ok(())\n}\n\n/// Return the archive if it is valid. It may have been corrupted or interrupted in the middle of\n/// downloading.\n/// ISSUE(#134) - verify checksum\nfn load_cached_distro(file: &Path) -> Option<Box<dyn Archive>> {\n    if file.is_file() {\n        let file = File::open(file).ok()?;\n        Tarball::load(file).ok()\n    } else {\n        None\n    }\n}\n\n/// Determine the remote URL to download from, using the hooks if avaialble\nfn determine_remote_url(version: &Version, hooks: Option<&ToolHooks<Npm>>) -> Fallible<String> {\n    let version_str = version.to_string();\n    match hooks {\n        Some(&ToolHooks {\n            distro: Some(ref hook),\n            ..\n        }) => {\n            debug!(\"Using npm.distro hook to determine download URL\");\n            let distro_file_name = Npm::archive_filename(&version_str);\n            hook.resolve(version, &distro_file_name)\n        }\n        _ => Ok(public_registry_package(\"npm\", &version_str)),\n    }\n}\n\n/// Fetch the distro archive from the internet\nfn fetch_remote_distro(\n    version: &Version,\n    url: &str,\n    staging_path: &Path,\n) -> Fallible<Box<dyn Archive>> {\n    debug!(\"Downloading {} from {}\", tool_version(\"npm\", version), url);\n    Tarball::fetch(url, staging_path).with_context(download_tool_error(\n        tool::Spec::Npm(VersionSpec::Exact(version.clone())),\n        url,\n    ))\n}\n\n/// Overwrite the launcher script\nfn overwrite_launcher(base_path: &Path, tool: &str) -> Fallible<()> {\n    let path = base_path.join(tool);\n    write(\n        &path,\n        // Note: Adapted from the existing npm/npx launcher, without unnecessary detection of Node location\n        format!(\n            r#\"#!/bin/sh\n(set -o igncr) 2>/dev/null && set -o igncr; # cygwin encoding fix\n\nbasedir=`dirname \"$0\"`\n\ncase `uname` in\n    *CYGWIN*) basedir=`cygpath -w \"$basedir\"`;;\nesac\n\nnode \"$basedir/{}-cli.js\" \"$@\"\n\"#,\n            tool\n        ),\n    )\n    .and_then(|_| set_executable(&path))\n    .with_context(|| ErrorKind::WriteLauncherError { tool: tool.into() })\n}\n\n/// Overwrite the CMD launcher\n#[cfg(windows)]\nfn overwrite_cmd_launcher(base_path: &Path, tool: &str) -> Fallible<()> {\n    write(\n        base_path.join(format!(\"{}.cmd\", tool)),\n        // Note: Adapted from the existing npm/npx cmd launcher, without unnecessary detection of Node location\n        format!(\n            r#\"@ECHO OFF\n\nnode \"%~dp0\\{}-cli.js\" %*\n\"#,\n            tool\n        ),\n    )\n    .with_context(|| ErrorKind::WriteLauncherError { tool: tool.into() })\n}\n"
  },
  {
    "path": "crates/volta-core/src/tool/npm/mod.rs",
    "content": "use std::fmt::{self, Display};\n\nuse super::node::load_default_npm_version;\nuse super::{\n    check_fetched, check_shim_reachable, debug_already_fetched, info_fetched, info_installed,\n    info_pinned, info_project_version, FetchStatus, Tool,\n};\nuse crate::error::{Context, ErrorKind, Fallible};\nuse crate::inventory::npm_available;\nuse crate::session::Session;\nuse crate::style::{success_prefix, tool_version};\nuse crate::sync::VoltaLock;\nuse log::info;\nuse node_semver::Version;\n\nmod fetch;\nmod resolve;\n\npub use resolve::resolve;\n\n/// The Tool implementation for fetching and installing npm\npub struct Npm {\n    pub(super) version: Version,\n}\n\nimpl Npm {\n    pub fn new(version: Version) -> Self {\n        Npm { version }\n    }\n\n    pub fn archive_basename(version: &str) -> String {\n        format!(\"npm-{}\", version)\n    }\n\n    pub fn archive_filename(version: &str) -> String {\n        format!(\"{}.tgz\", Npm::archive_basename(version))\n    }\n\n    pub(crate) fn ensure_fetched(&self, session: &mut Session) -> Fallible<()> {\n        match check_fetched(|| npm_available(&self.version))? {\n            FetchStatus::AlreadyFetched => {\n                debug_already_fetched(self);\n                Ok(())\n            }\n            FetchStatus::FetchNeeded(_lock) => fetch::fetch(&self.version, session.hooks()?.npm()),\n        }\n    }\n}\n\nimpl Tool for Npm {\n    fn fetch(self: Box<Self>, session: &mut Session) -> Fallible<()> {\n        self.ensure_fetched(session)?;\n\n        info_fetched(self);\n        Ok(())\n    }\n    fn install(self: Box<Self>, session: &mut Session) -> Fallible<()> {\n        // Acquire a lock on the Volta directory, if possible, to prevent concurrent changes\n        let _lock = VoltaLock::acquire();\n        self.ensure_fetched(session)?;\n\n        session\n            .toolchain_mut()?\n            .set_active_npm(Some(self.version.clone()))?;\n\n        info_installed(&self);\n        check_shim_reachable(\"npm\");\n\n        if let Ok(Some(project)) = session.project_platform() {\n            if let Some(npm) = &project.npm {\n                info_project_version(tool_version(\"npm\", npm), &self);\n            }\n        }\n        Ok(())\n    }\n    fn pin(self: Box<Self>, session: &mut Session) -> Fallible<()> {\n        if session.project()?.is_some() {\n            self.ensure_fetched(session)?;\n\n            // Note: We know this will succeed, since we checked above\n            let project = session.project_mut()?.unwrap();\n            project.pin_npm(Some(self.version.clone()))?;\n\n            info_pinned(self);\n            Ok(())\n        } else {\n            Err(ErrorKind::NotInPackage.into())\n        }\n    }\n}\n\nimpl Display for Npm {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        f.write_str(&tool_version(\"npm\", &self.version))\n    }\n}\n\n/// The Tool implementation for setting npm to the version bundled with Node\npub struct BundledNpm;\n\nimpl Tool for BundledNpm {\n    fn fetch(self: Box<Self>, _session: &mut Session) -> Fallible<()> {\n        info!(\"Bundled npm is included with Node, use `volta fetch node` to fetch Node\");\n        Ok(())\n    }\n\n    fn install(self: Box<Self>, session: &mut Session) -> Fallible<()> {\n        let toolchain = session.toolchain_mut()?;\n\n        toolchain.set_active_npm(None)?;\n\n        let bundled_version = match toolchain.platform() {\n            Some(platform) => {\n                let version = load_default_npm_version(&platform.node).with_context(|| {\n                    ErrorKind::NoBundledNpm {\n                        command: \"install\".into(),\n                    }\n                })?;\n                version.to_string()\n            }\n            None => {\n                return Err(ErrorKind::NoBundledNpm {\n                    command: \"install\".into(),\n                }\n                .into());\n            }\n        };\n\n        info!(\n            \"{} set bundled npm (currently {}) as default\",\n            success_prefix(),\n            bundled_version\n        );\n\n        Ok(())\n    }\n\n    fn pin(self: Box<Self>, session: &mut Session) -> Fallible<()> {\n        match session.project_mut()? {\n            Some(project) => {\n                project.pin_npm(None)?;\n\n                let bundled_version = match project.platform() {\n                    Some(platform) => {\n                        let version =\n                            load_default_npm_version(&platform.node).with_context(|| {\n                                ErrorKind::NoBundledNpm {\n                                    command: \"pin\".into(),\n                                }\n                            })?;\n                        version.to_string()\n                    }\n                    None => {\n                        return Err(ErrorKind::NoBundledNpm {\n                            command: \"pin\".into(),\n                        }\n                        .into());\n                    }\n                };\n\n                info!(\n                    \"{} set package.json to use bundled npm (currently {})\",\n                    success_prefix(),\n                    bundled_version\n                );\n\n                Ok(())\n            }\n            None => Err(ErrorKind::NotInPackage.into()),\n        }\n    }\n}\n\nimpl Display for BundledNpm {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        f.write_str(&tool_version(\"npm\", \"bundled\"))\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_npm_archive_basename() {\n        assert_eq!(Npm::archive_basename(\"1.2.3\"), \"npm-1.2.3\");\n    }\n\n    #[test]\n    fn test_npm_archive_filename() {\n        assert_eq!(Npm::archive_filename(\"1.2.3\"), \"npm-1.2.3.tgz\");\n    }\n}\n"
  },
  {
    "path": "crates/volta-core/src/tool/npm/resolve.rs",
    "content": "//! Provides resolution of npm Version requirements into specific versions\n\nuse super::super::registry::{\n    fetch_npm_registry, public_registry_index, PackageDetails, PackageIndex,\n};\nuse crate::error::{ErrorKind, Fallible};\nuse crate::hook::ToolHooks;\nuse crate::session::Session;\nuse crate::tool::Npm;\nuse crate::version::{VersionSpec, VersionTag};\nuse log::debug;\nuse node_semver::{Range, Version};\n\npub fn resolve(matching: VersionSpec, session: &mut Session) -> Fallible<Option<Version>> {\n    let hooks = session.hooks()?.npm();\n    match matching {\n        VersionSpec::Semver(requirement) => resolve_semver(requirement, hooks).map(Some),\n        VersionSpec::Exact(version) => Ok(Some(version)),\n        VersionSpec::None | VersionSpec::Tag(VersionTag::Latest) => {\n            resolve_tag(\"latest\", hooks).map(Some)\n        }\n        VersionSpec::Tag(VersionTag::Custom(tag)) if tag == \"bundled\" => Ok(None),\n        VersionSpec::Tag(tag) => resolve_tag(&tag.to_string(), hooks).map(Some),\n    }\n}\n\nfn fetch_npm_index(hooks: Option<&ToolHooks<Npm>>) -> Fallible<(String, PackageIndex)> {\n    let url = match hooks {\n        Some(&ToolHooks {\n            index: Some(ref hook),\n            ..\n        }) => {\n            debug!(\"Using npm.index hook to determine npm index URL\");\n            hook.resolve(\"npm\")?\n        }\n        _ => public_registry_index(\"npm\"),\n    };\n\n    fetch_npm_registry(url, \"npm\")\n}\n\nfn resolve_tag(tag: &str, hooks: Option<&ToolHooks<Npm>>) -> Fallible<Version> {\n    let (url, mut index) = fetch_npm_index(hooks)?;\n\n    match index.tags.remove(tag) {\n        Some(version) => {\n            debug!(\"Found npm@{} matching tag '{}' from {}\", version, tag, url);\n            Ok(version)\n        }\n        None => Err(ErrorKind::NpmVersionNotFound {\n            matching: tag.into(),\n        }\n        .into()),\n    }\n}\n\nfn resolve_semver(matching: Range, hooks: Option<&ToolHooks<Npm>>) -> Fallible<Version> {\n    let (url, index) = fetch_npm_index(hooks)?;\n\n    let details_opt = index\n        .entries\n        .into_iter()\n        .find(|PackageDetails { version, .. }| matching.satisfies(version));\n\n    match details_opt {\n        Some(details) => {\n            debug!(\n                \"Found npm@{} matching requirement '{}' from {}\",\n                details.version, matching, url\n            );\n            Ok(details.version)\n        }\n        None => Err(ErrorKind::NpmVersionNotFound {\n            matching: matching.to_string(),\n        }\n        .into()),\n    }\n}\n"
  },
  {
    "path": "crates/volta-core/src/tool/package/configure.rs",
    "content": "use std::path::PathBuf;\n\nuse super::manager::PackageManager;\nuse super::metadata::{BinConfig, PackageConfig, PackageManifest};\nuse crate::error::{ErrorKind, Fallible};\nuse crate::layout::volta_home;\nuse crate::platform::{Image, PlatformSpec};\nuse crate::shim;\nuse crate::tool::check_shim_reachable;\n\n/// Read the manifest for the package being installed\npub(super) fn parse_manifest(\n    package_name: &str,\n    staging_dir: PathBuf,\n    manager: PackageManager,\n) -> Fallible<PackageManifest> {\n    let mut package_dir = manager.source_dir(staging_dir);\n    package_dir.push(package_name);\n\n    PackageManifest::for_dir(package_name, &package_dir)\n}\n\n/// Generate configuration files and shims for the package and each of its bins\npub(super) fn write_config_and_shims(\n    name: &str,\n    manifest: &PackageManifest,\n    image: &Image,\n    manager: PackageManager,\n) -> Fallible<()> {\n    validate_bins(name, manifest)?;\n\n    let platform = PlatformSpec {\n        node: image.node.value.clone(),\n        npm: image.npm.clone().map(|s| s.value),\n        pnpm: image.pnpm.clone().map(|s| s.value),\n        yarn: image.yarn.clone().map(|s| s.value),\n    };\n\n    // Generate the shims and bin configs for each bin provided by the package\n    for bin_name in &manifest.bin {\n        shim::create(bin_name)?;\n        check_shim_reachable(bin_name);\n\n        BinConfig {\n            name: bin_name.clone(),\n            package: name.into(),\n            version: manifest.version.clone(),\n            platform: platform.clone(),\n            manager,\n        }\n        .write()?;\n    }\n\n    // Write the config for the package\n    PackageConfig {\n        name: name.into(),\n        version: manifest.version.clone(),\n        platform,\n        bins: manifest.bin.clone(),\n        manager,\n    }\n    .write()?;\n\n    Ok(())\n}\n\n/// Validate that we aren't attempting to install a bin that is already installed by\n/// another package.\nfn validate_bins(package_name: &str, manifest: &PackageManifest) -> Fallible<()> {\n    let home = volta_home()?;\n    for bin_name in &manifest.bin {\n        // Check for name conflicts with already-installed bins\n        // Some packages may install bins with the same name\n        if let Ok(config) = BinConfig::from_file(home.default_tool_bin_config(bin_name)) {\n            // The file exists, so there is a bin with this name\n            // That is okay iff it came from the package that is currently being installed\n            if package_name != config.package {\n                return Err(ErrorKind::BinaryAlreadyInstalled {\n                    bin_name: bin_name.into(),\n                    existing_package: config.package,\n                    new_package: package_name.into(),\n                }\n                .into());\n            }\n        }\n    }\n\n    Ok(())\n}\n"
  },
  {
    "path": "crates/volta-core/src/tool/package/install.rs",
    "content": "use std::path::PathBuf;\n\nuse super::manager::PackageManager;\nuse crate::command::create_command;\nuse crate::error::{Context, ErrorKind, Fallible};\nuse crate::platform::Image;\nuse crate::style::progress_spinner;\nuse log::debug;\n\n/// Use `npm install --global` to install the package\n///\n/// Sets the environment variable `npm_config_prefix` to redirect the install to the Volta\n/// data directory, taking advantage of the standard global install behavior with a custom\n/// location\npub(super) fn run_global_install(\n    package: String,\n    staging_dir: PathBuf,\n    platform_image: &Image,\n) -> Fallible<()> {\n    let mut command = create_command(\"npm\");\n    command.args([\n        \"install\",\n        \"--global\",\n        \"--loglevel=warn\",\n        \"--no-update-notifier\",\n        \"--no-audit\",\n    ]);\n    command.arg(&package);\n    command.env(\"PATH\", platform_image.path()?);\n    PackageManager::Npm.setup_global_command(&mut command, staging_dir);\n\n    debug!(\"Installing {} with command: {:?}\", package, command);\n    let spinner = progress_spinner(format!(\"Installing {}\", package));\n    let output_result = command\n        .output()\n        .with_context(|| ErrorKind::PackageInstallFailed {\n            package: package.clone(),\n        });\n    spinner.finish_and_clear();\n    let output = output_result?;\n\n    let stderr = String::from_utf8_lossy(&output.stderr);\n    debug!(\"[install stderr]\\n{}\", stderr);\n    debug!(\n        \"[install stdout]\\n{}\",\n        String::from_utf8_lossy(&output.stdout)\n    );\n\n    if output.status.success() {\n        Ok(())\n    } else if stderr.contains(\"code E404\") {\n        // npm outputs \"code E404\" as part of the error output when a package couldn't be found\n        // Detect that and show a nicer error message (since we likely know the problem in that case)\n        Err(ErrorKind::PackageNotFound { package }.into())\n    } else {\n        Err(ErrorKind::PackageInstallFailed { package }.into())\n    }\n}\n"
  },
  {
    "path": "crates/volta-core/src/tool/package/manager.rs",
    "content": "use std::ffi::OsStr;\nuse std::fs::File;\nuse std::path::{Path, PathBuf};\nuse std::process::Command;\n\nuse super::metadata::GlobalYarnManifest;\nuse crate::fs::read_dir_eager;\n\n/// The package manager used to install a given package\n#[derive(\n    Copy, Clone, serde::Serialize, serde::Deserialize, PartialOrd, Ord, PartialEq, Eq, Debug,\n)]\npub enum PackageManager {\n    Npm,\n    Pnpm,\n    Yarn,\n}\n\nimpl PackageManager {\n    /// Given the `package_root`, returns the directory where the source is stored for this\n    /// package manager. This will include the top-level `node_modules`, where appropriate.\n    pub fn source_dir(self, package_root: PathBuf) -> PathBuf {\n        let mut path = self.source_root(package_root);\n        path.push(\"node_modules\");\n\n        path\n    }\n\n    /// Given the `package_root`, returns the root of the source directory. This directory will\n    /// contain the top-level `node-modules`\n    #[cfg(unix)]\n    pub fn source_root(self, package_root: PathBuf) -> PathBuf {\n        let mut path = package_root;\n        match self {\n            // On Unix, the source is always within a `lib` subdirectory, with both npm and Yarn\n            PackageManager::Npm | PackageManager::Yarn => path.push(\"lib\"),\n            // pnpm puts the source node_modules directory in the global-dir\n            // plus a versioned subdirectory.\n            // FIXME: Here the subdirectory is hard-coded, I don't know if it's\n            // possible to retrieve it from pnpm dynamically.\n            PackageManager::Pnpm => path.push(\"5\"),\n        }\n\n        path\n    }\n\n    /// Given the `package_root`, returns the root of the source directory. This directory will\n    /// contain the top-level `node-modules`\n    #[cfg(windows)]\n    pub fn source_root(self, package_root: PathBuf) -> PathBuf {\n        match self {\n            // On Windows, npm puts the source node_modules directory in the root of the `prefix`\n            PackageManager::Npm => package_root,\n            // On Windows, we still tell yarn to use the `lib` subdirectory\n            PackageManager::Yarn => {\n                let mut path = package_root;\n                path.push(\"lib\");\n                path\n            }\n            // pnpm puts the source node_modules directory in the global-dir\n            // plus a versioned subdirectory.\n            // FIXME: Here the subdirectory is hard-coded, I don't know if it's\n            // possible to retrieve it from pnpm dynamically.\n            PackageManager::Pnpm => {\n                let mut path = package_root;\n                path.push(\"5\");\n                path\n            }\n        }\n    }\n\n    /// Given the `package_root`, returns the directory where binaries are stored for this package\n    /// manager.\n    #[cfg(unix)]\n    pub fn binary_dir(self, package_root: PathBuf) -> PathBuf {\n        // On Unix, the binaries are always within a `bin` subdirectory for both npm and Yarn\n        let mut path = package_root;\n        path.push(\"bin\");\n\n        path\n    }\n\n    /// Given the `package_root`, returns the directory where binaries are stored for this package\n    /// manager.\n    #[cfg(windows)]\n    pub fn binary_dir(self, package_root: PathBuf) -> PathBuf {\n        match self {\n            // On Windows, npm leaves the binaries at the root of the `prefix` directory\n            PackageManager::Npm => package_root,\n            // On Windows, Yarn still includes the `bin` subdirectory. pnpm by\n            // default generates binaries into the `PNPM_HOME` path\n            PackageManager::Yarn | PackageManager::Pnpm => {\n                let mut path = package_root;\n                path.push(\"bin\");\n                path\n            }\n        }\n    }\n\n    /// Modify a given `Command` to be set up for global installs, given the package root\n    pub fn setup_global_command(self, command: &mut Command, package_root: PathBuf) {\n        command.env(\"npm_config_prefix\", &package_root);\n\n        if let PackageManager::Yarn = self {\n            command.env(\"npm_config_global_folder\", self.source_root(package_root));\n        } else if let PackageManager::Pnpm = self {\n            // FIXME: Find out if there is a perfect way to intercept pnpm global\n            // installs by using environment variables or whatever.\n            // Using `--global-dir` and `--global-bin-dir` flags here is not enough,\n            // because pnpm generates _absolute path_ based symlinks, and this makes\n            // impossible to simply move installed packages from the staging directory\n            // to the final `image/packages/` destination.\n\n            // Specify the staging directory to store global package,\n            // see: https://pnpm.io/npmrc#global-dir\n            command.arg(\"--global-dir\").arg(&package_root);\n            // Specify the staging directory for the bin files of globally installed packages.\n            // See: https://pnpm.io/npmrc#global-bin-dir (>= 6.15.0)\n            // and https://github.com/volta-cli/rfcs/pull/46#discussion_r933296625\n            let global_bin_dir = self.binary_dir(package_root);\n            command.arg(\"--global-bin-dir\").arg(&global_bin_dir);\n            // pnpm requires the `global-bin-dir` to be in PATH, otherwise it\n            // will not trigger global installs. One can also use the `PNPM_HOME`\n            // environment variable, which is only available in pnpm v7+, to\n            // pass the check.\n            // See: https://github.com/volta-cli/rfcs/pull/46#discussion_r861943740\n            let mut new_path = global_bin_dir;\n            for (name, value) in command.get_envs() {\n                if name == \"PATH\" {\n                    if let Some(old_path) = value {\n                        #[cfg(unix)]\n                        let path_delimiter = OsStr::new(\":\");\n                        #[cfg(windows)]\n                        let path_delimiter = OsStr::new(\";\");\n                        new_path =\n                            PathBuf::from([new_path.as_os_str(), old_path].join(path_delimiter));\n                        break;\n                    }\n                }\n            }\n            command.env(\"PATH\", new_path);\n        }\n    }\n\n    /// Determine the name of the package that was installed into the `package_root`\n    ///\n    /// If there are none or more than one package installed, then we return None\n    pub(super) fn get_installed_package(self, package_root: PathBuf) -> Option<String> {\n        match self {\n            PackageManager::Npm => get_npm_package_name(self.source_dir(package_root)),\n            PackageManager::Pnpm | PackageManager::Yarn => {\n                get_pnpm_or_yarn_package_name(self.source_root(package_root))\n            }\n        }\n    }\n}\n\n/// Determine the package name for an npm global install\n///\n/// npm doesn't hoist the packages inside of `node_modules`, so the only directory will be the\n/// globally installed package.\nfn get_npm_package_name(mut source_dir: PathBuf) -> Option<String> {\n    let possible_name = get_single_directory_name(&source_dir)?;\n\n    // If the directory starts with `@`, that represents a scoped package, so we need to step\n    // a level deeper to determine the full package name (`@scope/package`)\n    if possible_name.starts_with('@') {\n        source_dir.push(&possible_name);\n        let package = get_single_directory_name(&source_dir)?;\n        Some(format!(\"{}/{}\", possible_name, package))\n    } else {\n        Some(possible_name)\n    }\n}\n\n/// Return the name of the single subdirectory (if any) to the given `parent_dir`\n///\n/// If there are more than one subdirectory, then this will return `None`\nfn get_single_directory_name(parent_dir: &Path) -> Option<String> {\n    let mut entries = read_dir_eager(parent_dir)\n        .ok()?\n        .filter_map(|(entry, metadata)| {\n            // If the entry is a symlink, _both_ is_dir() _and_ is_file() will be false. We want to\n            // include symlinks as well as directories in our search, since `npm link` uses\n            // symlinks internally, so we only exclude files from this search\n            if !metadata.is_file() {\n                Some(entry)\n            } else {\n                None\n            }\n        });\n\n    match (entries.next(), entries.next()) {\n        (Some(entry), None) => entry.file_name().into_string().ok(),\n        _ => None,\n    }\n}\n\n/// Determine the package name for a pnpm or Yarn global install\n///\n/// pnpm/Yarn creates a `package.json` file with the globally installed package as a dependency\nfn get_pnpm_or_yarn_package_name(source_root: PathBuf) -> Option<String> {\n    let package_file = source_root.join(\"package.json\");\n    let file = File::open(package_file).ok()?;\n    let manifest: GlobalYarnManifest = serde_json::de::from_reader(file).ok()?;\n    let mut dependencies = manifest.dependencies.into_iter();\n\n    match (dependencies.next(), dependencies.next()) {\n        // If there is exactly one dependency, we return it\n        (Some((key, _)), None) => Some(key),\n        // Otherwise, we can't determine the package name\n        _ => None,\n    }\n}\n"
  },
  {
    "path": "crates/volta-core/src/tool/package/metadata.rs",
    "content": "use std::collections::HashMap;\nuse std::fs::File;\nuse std::io;\nuse std::path::Path;\n\nuse super::manager::PackageManager;\nuse crate::error::{Context, ErrorKind, Fallible, VoltaError};\nuse crate::layout::volta_home;\nuse crate::platform::PlatformSpec;\nuse crate::version::{option_version_serde, version_serde};\nuse fs_utils::ensure_containing_dir_exists;\nuse node_semver::Version;\n\n/// Configuration information about an installed package\n///\n/// Will be stored in `<VOLTA_HOME>/tools/user/packages/<package>.json`\n#[derive(serde::Serialize, serde::Deserialize, PartialOrd, Ord, PartialEq, Eq)]\npub struct PackageConfig {\n    /// The package name\n    pub name: String,\n    /// The package version\n    #[serde(with = \"version_serde\")]\n    pub version: Version,\n    /// The platform used to install this package\n    #[serde(with = \"RawPlatformSpec\")]\n    pub platform: PlatformSpec,\n    /// The binaries installed by this package\n    pub bins: Vec<String>,\n    /// The package manager that was used to install this package\n    pub manager: PackageManager,\n}\n\nimpl PackageConfig {\n    /// Parse a `PackageConfig` instance from a config file\n    pub fn from_file<P>(file: P) -> Fallible<Self>\n    where\n        P: AsRef<Path>,\n    {\n        let config = File::open(&file).with_context(|| ErrorKind::ReadPackageConfigError {\n            file: file.as_ref().to_owned(),\n        })?;\n        serde_json::from_reader(config).with_context(|| ErrorKind::ParsePackageConfigError)\n    }\n\n    pub fn from_file_if_exists<P>(file: P) -> Fallible<Option<Self>>\n    where\n        P: AsRef<Path>,\n    {\n        match File::open(&file) {\n            Err(error) => {\n                if error.kind() == io::ErrorKind::NotFound {\n                    Ok(None)\n                } else {\n                    Err(VoltaError::from_source(\n                        error,\n                        ErrorKind::ReadPackageConfigError {\n                            file: file.as_ref().to_owned(),\n                        },\n                    ))\n                }\n            }\n            Ok(config) => serde_json::from_reader(config)\n                .with_context(|| ErrorKind::ParsePackageConfigError)\n                .map(Some),\n        }\n    }\n\n    /// Write this `PackageConfig` into the appropriate config file\n    pub fn write(self) -> Fallible<()> {\n        let config_file_path = volta_home()?.default_package_config_file(&self.name);\n\n        ensure_containing_dir_exists(&config_file_path).with_context(|| {\n            ErrorKind::ContainingDirError {\n                path: config_file_path.clone(),\n            }\n        })?;\n\n        let file = File::create(&config_file_path).with_context(|| {\n            ErrorKind::WritePackageConfigError {\n                file: config_file_path,\n            }\n        })?;\n        serde_json::to_writer_pretty(file, &self)\n            .with_context(|| ErrorKind::StringifyPackageConfigError)\n    }\n}\n\n/// Configuration information about a single installed binary from a package\n///\n/// Will be stored in <VOLTA_HOME>/tools/user/bins/<bin-name>.json\n#[derive(serde::Serialize, serde::Deserialize)]\npub struct BinConfig {\n    /// The binary name\n    pub name: String,\n    /// The package that installed the binary\n    pub package: String,\n    /// The package version\n    #[serde(with = \"version_serde\")]\n    pub version: Version,\n    /// The platform used to install this binary\n    #[serde(with = \"RawPlatformSpec\")]\n    pub platform: PlatformSpec,\n    /// The package manager used to install this binary\n    pub manager: PackageManager,\n}\n\nimpl BinConfig {\n    /// Parse a `BinConfig` instance from the given config file\n    pub fn from_file<P>(file: P) -> Fallible<Self>\n    where\n        P: AsRef<Path>,\n    {\n        let config = File::open(&file).with_context(|| ErrorKind::ReadBinConfigError {\n            file: file.as_ref().to_owned(),\n        })?;\n        serde_json::from_reader(config).with_context(|| ErrorKind::ParseBinConfigError)\n    }\n\n    pub fn from_file_if_exists<P>(file: P) -> Fallible<Option<Self>>\n    where\n        P: AsRef<Path>,\n    {\n        match File::open(&file) {\n            Err(error) => {\n                if error.kind() == io::ErrorKind::NotFound {\n                    Ok(None)\n                } else {\n                    Err(VoltaError::from_source(\n                        error,\n                        ErrorKind::ReadBinConfigError {\n                            file: file.as_ref().to_owned(),\n                        },\n                    ))\n                }\n            }\n            Ok(config) => serde_json::from_reader(config)\n                .with_context(|| ErrorKind::ParseBinConfigError)\n                .map(Some),\n        }\n    }\n\n    /// Write this `BinConfig` to the appropriate config file\n    pub fn write(self) -> Fallible<()> {\n        let config_file_path = volta_home()?.default_tool_bin_config(&self.name);\n\n        ensure_containing_dir_exists(&config_file_path).with_context(|| {\n            ErrorKind::ContainingDirError {\n                path: config_file_path.clone(),\n            }\n        })?;\n\n        let file =\n            File::create(&config_file_path).with_context(|| ErrorKind::WriteBinConfigError {\n                file: config_file_path,\n            })?;\n        serde_json::to_writer_pretty(file, &self)\n            .with_context(|| ErrorKind::StringifyBinConfigError)\n    }\n}\n\n#[derive(serde::Serialize, serde::Deserialize)]\n#[serde(remote = \"PlatformSpec\")]\nstruct RawPlatformSpec {\n    #[serde(with = \"version_serde\")]\n    node: Version,\n    #[serde(with = \"option_version_serde\")]\n    npm: Option<Version>,\n    // The magic:\n    // `serde(default)` to assign the pnpm field with a default value, this\n    // ensures a seamless migration is performed from the previous package\n    // platformspec which did not have a pnpm field despite the same layout.v3\n    #[serde(default)]\n    #[serde(with = \"option_version_serde\")]\n    pnpm: Option<Version>,\n    #[serde(with = \"option_version_serde\")]\n    yarn: Option<Version>,\n}\n\n/// The relevant information we need out of a package's `package.json` file\n///\n/// This includes the exact Version (since we can install using a range)\n/// and the list of bins provided by the package.\n#[derive(serde::Deserialize)]\npub struct PackageManifest {\n    /// The name of the package\n    pub name: String,\n    /// The version of the package\n    #[serde(deserialize_with = \"version_serde::deserialize\")]\n    pub version: Version,\n    /// The `bin` section, containing a map of binary names to locations\n    #[serde(default, deserialize_with = \"serde_bins::deserialize\")]\n    pub bin: Vec<String>,\n}\n\nimpl PackageManifest {\n    /// Parse the `package.json` for a given package directory\n    pub fn for_dir(package: &str, package_root: &Path) -> Fallible<Self> {\n        let package_file = package_root.join(\"package.json\");\n        let file =\n            File::open(package_file).with_context(|| ErrorKind::PackageManifestReadError {\n                package: package.into(),\n            })?;\n\n        let mut manifest: Self = serde_json::de::from_reader(file).with_context(|| {\n            ErrorKind::PackageManifestParseError {\n                package: package.into(),\n            }\n        })?;\n\n        // If the bin list contains only an empty string, that means `bin` was a string value,\n        // rather than a map. In that case, to match `npm`s behavior, we use the name of the package\n        // as the bin name.\n        // Note: For a scoped package, we should remove the scope and only use the package name\n        if manifest.bin == [\"\"] {\n            manifest.bin.pop();\n            manifest.bin.push(default_binary_name(&manifest.name));\n        }\n\n        Ok(manifest)\n    }\n}\n\n#[derive(serde::Deserialize)]\n/// Struct to read the `dependencies` out of Yarn's global manifest.\n///\n/// For global installs, yarn creates a `package.json` file in the `global-folder` and installs\n/// global packages as dependencies of that pseudo-package\npub(super) struct GlobalYarnManifest {\n    #[serde(default)]\n    pub dependencies: HashMap<String, String>,\n}\n\nmod serde_bins {\n    use std::fmt;\n\n    use serde::de::{Deserializer, Error, MapAccess, Visitor};\n\n    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>\n    where\n        D: Deserializer<'de>,\n    {\n        deserializer.deserialize_any(BinMapVisitor)\n    }\n\n    struct BinMapVisitor;\n\n    impl<'de> Visitor<'de> for BinMapVisitor {\n        type Value = Vec<String>;\n\n        fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n            f.write_str(\"string or map\")\n        }\n\n        // Handle String values with only the path\n        fn visit_str<E>(self, _path: &str) -> Result<Self::Value, E>\n        where\n            E: Error,\n        {\n            // Use an empty string as a placeholder for the binary name, since at this level we\n            // don't know the binary name for sure (npm uses the package name in this case)\n            Ok(vec![String::new()])\n        }\n\n        // Handle maps of Name -> Path\n        fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>\n        where\n            M: MapAccess<'de>,\n        {\n            let mut bins = Vec::new();\n            while let Some((name, _)) = access.next_entry::<String, String>()? {\n                // Bin names that include path separators are invalid, as they would then point to\n                // other locations on the filesystem. To match the behavior of npm & Yarn, we\n                // filter those values out of the list of bins.\n                if !name.contains(&['/', '\\\\'][..]) {\n                    bins.push(name);\n                }\n            }\n            Ok(bins)\n        }\n    }\n}\n\n/// Determine the default binary name from the package name\n///\n/// For non-scoped packages, this is just the package name\n/// For scoped packages, to match the behavior of the package managers, we remove the scope and use\n/// only the package part, e.g. `@microsoft/rush` would have a default name of `rush`\nfn default_binary_name(package_name: &str) -> String {\n    if package_name.starts_with('@') {\n        let mut chars = package_name.chars();\n\n        loop {\n            match chars.next() {\n                Some('/') | None => break,\n                _ => {}\n            }\n        }\n\n        let name = chars.as_str();\n        if name.is_empty() {\n            package_name.to_string()\n        } else {\n            name.to_string()\n        }\n    } else {\n        package_name.to_string()\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::default_binary_name;\n\n    #[test]\n    fn default_binary_uses_full_name_if_unscoped() {\n        assert_eq!(default_binary_name(\"my-package\"), \"my-package\");\n    }\n\n    #[test]\n    fn default_binary_removes_scope() {\n        assert_eq!(default_binary_name(\"@scope/my-package\"), \"my-package\");\n    }\n}\n"
  },
  {
    "path": "crates/volta-core/src/tool/package/mod.rs",
    "content": "use std::fmt::{self, Display};\nuse std::fs::create_dir_all;\nuse std::path::{Path, PathBuf};\nuse std::process::Command;\n\nuse super::Tool;\nuse crate::error::{Context, ErrorKind, Fallible};\nuse crate::fs::{remove_dir_if_exists, rename, symlink_dir};\nuse crate::layout::volta_home;\nuse crate::platform::{Image, PlatformSpec};\nuse crate::session::Session;\nuse crate::style::{success_prefix, tool_version};\nuse crate::sync::VoltaLock;\nuse crate::version::VersionSpec;\nuse fs_utils::ensure_containing_dir_exists;\nuse log::info;\nuse tempfile::{tempdir_in, TempDir};\n\nmod configure;\nmod install;\nmod manager;\nmod metadata;\nmod uninstall;\n\npub use manager::PackageManager;\npub use metadata::{BinConfig, PackageConfig, PackageManifest};\npub use uninstall::uninstall;\n\n/// The Tool implementation for installing 3rd-party global packages\npub struct Package {\n    name: String,\n    version: VersionSpec,\n    staging: TempDir,\n}\n\nimpl Package {\n    pub fn new(name: String, version: VersionSpec) -> Fallible<Self> {\n        let staging = setup_staging_directory(PackageManager::Npm, NeedsScope::No)?;\n\n        Ok(Package {\n            name,\n            version,\n            staging,\n        })\n    }\n\n    pub fn run_install(&self, platform_image: &Image) -> Fallible<()> {\n        install::run_global_install(\n            self.to_string(),\n            self.staging.path().to_owned(),\n            platform_image,\n        )\n    }\n\n    pub fn complete_install(self, image: &Image) -> Fallible<PackageManifest> {\n        let manager = PackageManager::Npm;\n        let manifest =\n            configure::parse_manifest(&self.name, self.staging.path().to_owned(), manager)?;\n\n        persist_install(&self.name, &self.version, self.staging.path())?;\n        link_package_to_shared_dir(&self.name, manager)?;\n        configure::write_config_and_shims(&self.name, &manifest, image, manager)?;\n\n        Ok(manifest)\n    }\n}\n\nimpl Tool for Package {\n    fn fetch(self: Box<Self>, _session: &mut Session) -> Fallible<()> {\n        Err(ErrorKind::CannotFetchPackage {\n            package: self.to_string(),\n        }\n        .into())\n    }\n\n    fn install(self: Box<Self>, session: &mut Session) -> Fallible<()> {\n        let _lock = VoltaLock::acquire();\n\n        let default_image = session\n            .default_platform()?\n            .map(PlatformSpec::as_default)\n            .ok_or(ErrorKind::NoPlatform)?\n            .checkout(session)?;\n\n        self.run_install(&default_image)?;\n        let manifest = self.complete_install(&default_image)?;\n\n        let bins = manifest.bin.join(\", \");\n\n        if bins.is_empty() {\n            info!(\n                \"{} installed {}\",\n                success_prefix(),\n                tool_version(manifest.name, manifest.version)\n            );\n        } else {\n            info!(\n                \"{} installed {} with executables: {}\",\n                success_prefix(),\n                tool_version(manifest.name, manifest.version),\n                bins\n            );\n        }\n\n        Ok(())\n    }\n\n    fn pin(self: Box<Self>, _session: &mut Session) -> Fallible<()> {\n        Err(ErrorKind::CannotPinPackage { package: self.name }.into())\n    }\n}\n\nimpl Display for Package {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match self.version {\n            VersionSpec::None => f.write_str(&self.name),\n            _ => f.write_str(&tool_version(&self.name, &self.version)),\n        }\n    }\n}\n\n/// Helper struct for direct installs through `npm i -g` or `yarn global add`\n///\n/// Provides methods to simplify installing into a staging directory and then moving that install\n/// into the proper location after it is complete.\n///\n/// Note: We don't always know the name of the package up-front, as the install could be from a\n/// tarball or a git coordinate. If we do know ahead of time, then we can skip looking it up\npub struct DirectInstall {\n    staging: TempDir,\n    manager: PackageManager,\n    name: Option<String>,\n}\n\nimpl DirectInstall {\n    pub fn new(manager: PackageManager) -> Fallible<Self> {\n        let staging = setup_staging_directory(manager, NeedsScope::No)?;\n\n        Ok(DirectInstall {\n            staging,\n            manager,\n            name: None,\n        })\n    }\n\n    pub fn with_name(manager: PackageManager, name: String) -> Fallible<Self> {\n        let staging = setup_staging_directory(manager, name.contains('/').into())?;\n\n        Ok(DirectInstall {\n            staging,\n            manager,\n            name: Some(name),\n        })\n    }\n\n    pub fn setup_command(&self, command: &mut Command) {\n        self.manager\n            .setup_global_command(command, self.staging.path().to_owned());\n    }\n\n    pub fn complete_install(self, image: &Image) -> Fallible<()> {\n        let DirectInstall {\n            staging,\n            name,\n            manager,\n        } = self;\n\n        let name = name\n            .or_else(|| manager.get_installed_package(staging.path().to_owned()))\n            .ok_or(ErrorKind::InstalledPackageNameError)?;\n        let manifest = configure::parse_manifest(&name, staging.path().to_owned(), manager)?;\n\n        persist_install(&name, &manifest.version, staging.path())?;\n        link_package_to_shared_dir(&name, manager)?;\n        configure::write_config_and_shims(&name, &manifest, image, manager)\n    }\n}\n\n/// Helper struct for direct in-place upgrades using `npm update -g` or `yarn global upgrade`\n///\n/// Upgrades the requested package directly in the image directory\npub struct InPlaceUpgrade {\n    package: String,\n    directory: PathBuf,\n    manager: PackageManager,\n}\n\nimpl InPlaceUpgrade {\n    pub fn new(package: String, manager: PackageManager) -> Fallible<Self> {\n        let directory = volta_home()?.package_image_dir(&package);\n\n        Ok(Self {\n            package,\n            directory,\n            manager,\n        })\n    }\n\n    /// Check for possible failure cases with the package to be upgraded\n    ///     - The package is not installed as a global\n    ///     - The package exists, but was installed with a different package manager\n    pub fn check_upgraded_package(&self) -> Fallible<()> {\n        let config =\n            PackageConfig::from_file(volta_home()?.default_package_config_file(&self.package))\n                .with_context(|| ErrorKind::UpgradePackageNotFound {\n                    package: self.package.clone(),\n                    manager: self.manager,\n                })?;\n\n        if config.manager != self.manager {\n            Err(ErrorKind::UpgradePackageWrongManager {\n                package: self.package.clone(),\n                manager: config.manager,\n            }\n            .into())\n        } else {\n            Ok(())\n        }\n    }\n\n    pub fn setup_command(&self, command: &mut Command) {\n        self.manager\n            .setup_global_command(command, self.directory.clone());\n    }\n\n    pub fn complete_upgrade(self, image: &Image) -> Fallible<()> {\n        let manifest = configure::parse_manifest(&self.package, self.directory, self.manager)?;\n\n        link_package_to_shared_dir(&self.package, self.manager)?;\n        configure::write_config_and_shims(&self.package, &manifest, image, self.manager)\n    }\n}\n\n#[derive(Clone, Copy, PartialEq)]\nenum NeedsScope {\n    Yes,\n    No,\n}\n\nimpl From<bool> for NeedsScope {\n    fn from(value: bool) -> Self {\n        if value {\n            NeedsScope::Yes\n        } else {\n            NeedsScope::No\n        }\n    }\n}\n\n/// Create the temporary staging directory we will use to install and ensure expected\n/// subdirectories exist within it\nfn setup_staging_directory(manager: PackageManager, needs_scope: NeedsScope) -> Fallible<TempDir> {\n    // Workaround to ensure relative symlinks continue to work.\n    // The final installed location of packages is:\n    //      $VOLTA_HOME/tools/image/packages/{name}/\n    // To ensure that the temp directory has the same amount of nesting, we use:\n    //      $VOLTA_HOME/tmp/image/packages/{tempdir}/\n    // This way any relative symlinks will have the same amount of nesting and will remain valid\n    // even when the directory is persisted.\n    // We also need to handle the case when the linked package has a scope, which requires another\n    // level of nesting\n    let mut staging_root = volta_home()?.tmp_dir().to_owned();\n    staging_root.push(\"image\");\n    staging_root.push(\"packages\");\n    if needs_scope == NeedsScope::Yes {\n        staging_root.push(\"scope\");\n    }\n    create_dir_all(&staging_root).with_context(|| ErrorKind::ContainingDirError {\n        path: staging_root.clone(),\n    })?;\n    let staging = tempdir_in(&staging_root).with_context(|| ErrorKind::CreateTempDirError {\n        in_dir: staging_root,\n    })?;\n\n    let source_dir = manager.source_dir(staging.path().to_owned());\n    ensure_containing_dir_exists(&source_dir)\n        .with_context(|| ErrorKind::ContainingDirError { path: source_dir })?;\n\n    let binary_dir = manager.binary_dir(staging.path().to_owned());\n    ensure_containing_dir_exists(&binary_dir)\n        .with_context(|| ErrorKind::ContainingDirError { path: binary_dir })?;\n\n    Ok(staging)\n}\n\nfn persist_install<V>(package_name: &str, package_version: V, staging_dir: &Path) -> Fallible<()>\nwhere\n    V: Display,\n{\n    let package_dir = volta_home()?.package_image_dir(package_name);\n\n    remove_dir_if_exists(&package_dir)?;\n\n    // Handle scoped packages (@vue/cli), which have an extra directory for the scope\n    ensure_containing_dir_exists(&package_dir).with_context(|| ErrorKind::ContainingDirError {\n        path: package_dir.to_owned(),\n    })?;\n\n    rename(staging_dir, &package_dir).with_context(|| ErrorKind::SetupToolImageError {\n        tool: package_name.into(),\n        version: package_version.to_string(),\n        dir: package_dir,\n    })?;\n\n    Ok(())\n}\n\nfn link_package_to_shared_dir(package_name: &str, manager: PackageManager) -> Fallible<()> {\n    let home = volta_home()?;\n    let mut source = manager.source_dir(home.package_image_dir(package_name));\n    source.push(package_name);\n\n    let target = home.shared_lib_dir(package_name);\n\n    remove_dir_if_exists(&target)?;\n\n    // Handle scoped packages (@vue/cli), which have an extra directory for the scope\n    ensure_containing_dir_exists(&target).with_context(|| ErrorKind::ContainingDirError {\n        path: target.clone(),\n    })?;\n\n    symlink_dir(source, target).with_context(|| ErrorKind::CreateSharedLinkError {\n        name: package_name.into(),\n    })\n}\n"
  },
  {
    "path": "crates/volta-core/src/tool/package/uninstall.rs",
    "content": "use super::metadata::{BinConfig, PackageConfig};\nuse crate::error::{Context, ErrorKind, Fallible};\nuse crate::fs::{\n    dir_entry_match, ok_if_not_found, read_dir_eager, remove_dir_if_exists, remove_file_if_exists,\n};\nuse crate::layout::volta_home;\nuse crate::shim;\nuse crate::style::success_prefix;\nuse crate::sync::VoltaLock;\nuse log::{info, warn};\n\n/// Uninstalls the specified package.\n///\n/// This removes:\n///\n/// - The JSON configuration files for both the package and its bins\n/// - The shims for the package bins\n/// - The package directory itself\npub fn uninstall(name: &str) -> Fallible<()> {\n    let home = volta_home()?;\n    // Acquire a lock on the Volta directory, if possible, to prevent concurrent changes\n    let _lock = VoltaLock::acquire();\n\n    // If the package config file exists, use that to remove any installed bins and shims\n    let package_config_file = home.default_package_config_file(name);\n\n    let package_found = match PackageConfig::from_file_if_exists(&package_config_file)? {\n        None => {\n            // there is no package config - check for orphaned binaries\n            let package_binary_list = binaries_from_package(name)?;\n            if !package_binary_list.is_empty() {\n                for bin_name in package_binary_list {\n                    remove_config_and_shim(&bin_name, name)?;\n                }\n                true\n            } else {\n                false\n            }\n        }\n        Some(package_config) => {\n            for bin_name in package_config.bins {\n                remove_config_and_shim(&bin_name, name)?;\n            }\n\n            remove_file_if_exists(package_config_file)?;\n            true\n        }\n    };\n\n    remove_shared_link_dir(name)?;\n\n    // Remove the package directory itself\n    let package_image_dir = home.package_image_dir(name);\n    remove_dir_if_exists(package_image_dir)?;\n\n    if package_found {\n        info!(\"{} package '{}' uninstalled\", success_prefix(), name);\n    } else {\n        warn!(\"No package '{}' found to uninstall\", name);\n    }\n\n    Ok(())\n}\n\n/// Remove a shim and its associated configuration file\nfn remove_config_and_shim(bin_name: &str, pkg_name: &str) -> Fallible<()> {\n    shim::delete(bin_name)?;\n    let config_file = volta_home()?.default_tool_bin_config(bin_name);\n    remove_file_if_exists(config_file)?;\n    info!(\n        \"Removed executable '{}' installed by '{}'\",\n        bin_name, pkg_name\n    );\n    Ok(())\n}\n\n/// Reads the contents of a directory and returns a Vec containing the names of\n/// all the binaries installed by the given package.\nfn binaries_from_package(package: &str) -> Fallible<Vec<String>> {\n    let bin_config_dir = volta_home()?.default_bin_dir();\n\n    dir_entry_match(bin_config_dir, |entry| {\n        let path = entry.path();\n        if let Ok(config) = BinConfig::from_file(path) {\n            if config.package == package {\n                return Some(config.name);\n            }\n        }\n        None\n    })\n    .or_else(ok_if_not_found)\n    .with_context(|| ErrorKind::ReadBinConfigDirError {\n        dir: bin_config_dir.to_owned(),\n    })\n}\n\n/// Remove the link to the package in the shared lib directory\n///\n/// For scoped packages, if the scope directory is now empty, it will also be removed\nfn remove_shared_link_dir(name: &str) -> Fallible<()> {\n    // Remove the link in the shared package directory, if it exists\n    let mut shared_lib_dir = volta_home()?.shared_lib_dir(name);\n    remove_dir_if_exists(&shared_lib_dir)?;\n\n    // For scoped packages, clean up the scope directory if it is now empty\n    if name.starts_with('@') {\n        shared_lib_dir.pop();\n\n        if let Ok(mut entries) = read_dir_eager(&shared_lib_dir) {\n            if entries.next().is_none() {\n                remove_dir_if_exists(&shared_lib_dir)?;\n            }\n        }\n    }\n\n    Ok(())\n}\n"
  },
  {
    "path": "crates/volta-core/src/tool/pnpm/fetch.rs",
    "content": "//! Provides fetcher for pnpm distributions\n\nuse std::fs::{write, File};\nuse std::path::Path;\n\nuse archive::{Archive, Tarball};\nuse fs_utils::ensure_containing_dir_exists;\nuse log::debug;\nuse node_semver::Version;\n\nuse crate::error::{Context, ErrorKind, Fallible};\nuse crate::fs::{create_staging_dir, create_staging_file, rename, set_executable};\nuse crate::hook::ToolHooks;\nuse crate::layout::volta_home;\nuse crate::style::{progress_bar, tool_version};\nuse crate::tool::registry::public_registry_package;\nuse crate::tool::{self, download_tool_error, Pnpm};\nuse crate::version::VersionSpec;\n\npub fn fetch(version: &Version, hooks: Option<&ToolHooks<Pnpm>>) -> Fallible<()> {\n    let pnpm_dir = volta_home()?.pnpm_inventory_dir();\n    let cache_file = pnpm_dir.join(Pnpm::archive_filename(&version.to_string()));\n\n    let (archive, staging) = match load_cached_distro(&cache_file) {\n        Some(archive) => {\n            debug!(\n                \"Loading {} from cached archive at '{}'\",\n                tool_version(\"pnpm\", version),\n                cache_file.display(),\n            );\n            (archive, None)\n        }\n        None => {\n            let staging = create_staging_file()?;\n            let remote_url = determine_remote_url(version, hooks)?;\n            let archive = fetch_remote_distro(version, &remote_url, staging.path())?;\n            (archive, Some(staging))\n        }\n    };\n\n    unpack_archive(archive, version)?;\n\n    if let Some(staging_file) = staging {\n        ensure_containing_dir_exists(&cache_file).with_context(|| {\n            ErrorKind::ContainingDirError {\n                path: cache_file.clone(),\n            }\n        })?;\n        staging_file\n            .persist(cache_file)\n            .with_context(|| ErrorKind::PersistInventoryError {\n                tool: \"pnpm\".into(),\n            })?;\n    }\n\n    Ok(())\n}\n\n/// Unpack the pnpm archive into the image directory so that it is ready for use\nfn unpack_archive(archive: Box<dyn Archive>, version: &Version) -> Fallible<()> {\n    let temp = create_staging_dir()?;\n    debug!(\"Unpacking pnpm into '{}'\", temp.path().display());\n\n    let progress = progress_bar(\n        archive.origin(),\n        &tool_version(\"pnpm\", version),\n        archive.compressed_size(),\n    );\n    let version_string = version.to_string();\n\n    archive\n        .unpack(temp.path(), &mut |_, read| {\n            progress.inc(read as u64);\n        })\n        .with_context(|| ErrorKind::UnpackArchiveError {\n            tool: \"pnpm\".into(),\n            version: version_string.clone(),\n        })?;\n\n    let bin_path = temp.path().join(\"package\").join(\"bin\");\n    write_launcher(&bin_path, \"pnpm\")?;\n    write_launcher(&bin_path, \"pnpx\")?;\n\n    #[cfg(windows)]\n    {\n        write_cmd_launcher(&bin_path, \"pnpm\")?;\n        write_cmd_launcher(&bin_path, \"pnpx\")?;\n    }\n\n    let dest = volta_home()?.pnpm_image_dir(&version_string);\n    ensure_containing_dir_exists(&dest)\n        .with_context(|| ErrorKind::ContainingDirError { path: dest.clone() })?;\n\n    rename(temp.path().join(\"package\"), &dest).with_context(|| ErrorKind::SetupToolImageError {\n        tool: \"pnpm\".into(),\n        version: version_string.clone(),\n        dir: dest.clone(),\n    })?;\n\n    progress.finish_and_clear();\n\n    // Note: We write this after the progress bar is finished to avoid display bugs with re-renders of the progress\n    debug!(\"Installing pnpm in '{}'\", dest.display());\n\n    Ok(())\n}\n\n/// Return the archive if it is valid. It may have been corrupted or interrupted in the middle of\n/// downloading.\n// ISSUE(#134) - verify checksum\nfn load_cached_distro(file: &Path) -> Option<Box<dyn Archive>> {\n    if file.is_file() {\n        let file = File::open(file).ok()?;\n        Tarball::load(file).ok()\n    } else {\n        None\n    }\n}\n\n/// Determine the remote URL to download from, using the hooks if avaialble\nfn determine_remote_url(version: &Version, hooks: Option<&ToolHooks<Pnpm>>) -> Fallible<String> {\n    let version_str = version.to_string();\n    match hooks {\n        Some(&ToolHooks {\n            distro: Some(ref hook),\n            ..\n        }) => {\n            debug!(\"Using pnpm.distro hook to determine download URL\");\n            let distro_file_name = Pnpm::archive_filename(&version_str);\n            hook.resolve(version, &distro_file_name)\n        }\n        _ => Ok(public_registry_package(\"pnpm\", &version_str)),\n    }\n}\n\n/// Fetch the distro archive from the internet\nfn fetch_remote_distro(\n    version: &Version,\n    url: &str,\n    staging_path: &Path,\n) -> Fallible<Box<dyn Archive>> {\n    debug!(\"Downloading {} from {}\", tool_version(\"pnpm\", version), url);\n    Tarball::fetch(url, staging_path).with_context(download_tool_error(\n        tool::Spec::Pnpm(VersionSpec::Exact(version.clone())),\n        url,\n    ))\n}\n\n/// Create executable launchers for the pnpm and pnpx binaries\nfn write_launcher(base_path: &Path, tool: &str) -> Fallible<()> {\n    let path = base_path.join(tool);\n    write(\n        &path,\n        format!(\n            r#\"#!/bin/sh\n(set -o igncr) 2>/dev/null && set -o igncr; # cygwin encoding fix\n\nbasedir=`dirname \"$0\"`\n\ncase `uname` in\n    *CYGWIN*) basedir=`cygpath -w \"$basedir\"`;;\nesac\n\nnode \"$basedir/{}.cjs\" \"$@\"\n\"#,\n            tool\n        ),\n    )\n    .and_then(|_| set_executable(&path))\n    .with_context(|| ErrorKind::WriteLauncherError { tool: tool.into() })\n}\n\n/// Create CMD executable launchers for the pnpm and pnpx binaries for Windows\n#[cfg(windows)]\nfn write_cmd_launcher(base_path: &Path, tool: &str) -> Fallible<()> {\n    write(\n        base_path.join(format!(\"{}.cmd\", tool)),\n        format!(\"@echo off\\nnode \\\"%~dp0\\\\{}.cjs\\\" %*\", tool),\n    )\n    .with_context(|| ErrorKind::WriteLauncherError { tool: tool.into() })\n}\n"
  },
  {
    "path": "crates/volta-core/src/tool/pnpm/mod.rs",
    "content": "use node_semver::Version;\nuse std::fmt::{self, Display};\n\nuse crate::error::{ErrorKind, Fallible};\nuse crate::inventory::pnpm_available;\nuse crate::session::Session;\nuse crate::style::tool_version;\nuse crate::sync::VoltaLock;\n\nuse super::{\n    check_fetched, check_shim_reachable, debug_already_fetched, info_fetched, info_installed,\n    info_pinned, info_project_version, FetchStatus, Tool,\n};\n\nmod fetch;\nmod resolve;\n\npub use resolve::resolve;\n\n/// The Tool implementation for fetching and installing pnpm\npub struct Pnpm {\n    pub(super) version: Version,\n}\n\nimpl Pnpm {\n    pub fn new(version: Version) -> Self {\n        Pnpm { version }\n    }\n\n    pub fn archive_basename(version: &str) -> String {\n        format!(\"pnpm-{}\", version)\n    }\n\n    pub fn archive_filename(version: &str) -> String {\n        format!(\"{}.tgz\", Pnpm::archive_basename(version))\n    }\n\n    pub(crate) fn ensure_fetched(&self, session: &mut Session) -> Fallible<()> {\n        match check_fetched(|| pnpm_available(&self.version))? {\n            FetchStatus::AlreadyFetched => {\n                debug_already_fetched(self);\n                Ok(())\n            }\n            FetchStatus::FetchNeeded(_lock) => fetch::fetch(&self.version, session.hooks()?.pnpm()),\n        }\n    }\n}\n\nimpl Tool for Pnpm {\n    fn fetch(self: Box<Self>, session: &mut Session) -> Fallible<()> {\n        self.ensure_fetched(session)?;\n\n        info_fetched(self);\n        Ok(())\n    }\n\n    fn install(self: Box<Self>, session: &mut Session) -> Fallible<()> {\n        // Acquire a lock on the Volta directory, if possible, to prevent concurrent changes\n        let _lock = VoltaLock::acquire();\n        self.ensure_fetched(session)?;\n\n        session\n            .toolchain_mut()?\n            .set_active_pnpm(Some(self.version.clone()))?;\n\n        info_installed(&self);\n        check_shim_reachable(\"pnpm\");\n\n        if let Ok(Some(project)) = session.project_platform() {\n            if let Some(pnpm) = &project.pnpm {\n                info_project_version(tool_version(\"pnpm\", pnpm), &self);\n            }\n        }\n        Ok(())\n    }\n\n    fn pin(self: Box<Self>, session: &mut Session) -> Fallible<()> {\n        if session.project()?.is_some() {\n            self.ensure_fetched(session)?;\n\n            // Note: We know this will succeed, since we checked above\n            let project = session.project_mut()?.unwrap();\n            project.pin_pnpm(Some(self.version.clone()))?;\n\n            info_pinned(self);\n            Ok(())\n        } else {\n            Err(ErrorKind::NotInPackage.into())\n        }\n    }\n}\n\nimpl Display for Pnpm {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        f.write_str(&tool_version(\"pnpm\", &self.version))\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_pnpm_archive_basename() {\n        assert_eq!(Pnpm::archive_basename(\"1.2.3\"), \"pnpm-1.2.3\");\n    }\n\n    #[test]\n    fn test_pnpm_archive_filename() {\n        assert_eq!(Pnpm::archive_filename(\"1.2.3\"), \"pnpm-1.2.3.tgz\");\n    }\n}\n"
  },
  {
    "path": "crates/volta-core/src/tool/pnpm/resolve.rs",
    "content": "use log::debug;\nuse node_semver::{Range, Version};\n\nuse crate::error::{ErrorKind, Fallible};\nuse crate::hook::ToolHooks;\nuse crate::session::Session;\nuse crate::tool::registry::{fetch_npm_registry, public_registry_index, PackageIndex};\nuse crate::tool::{PackageDetails, Pnpm};\nuse crate::version::{VersionSpec, VersionTag};\n\npub fn resolve(matching: VersionSpec, session: &mut Session) -> Fallible<Version> {\n    let hooks = session.hooks()?.pnpm();\n    match matching {\n        VersionSpec::Semver(requirement) => resolve_semver(requirement, hooks),\n        VersionSpec::Exact(version) => Ok(version),\n        VersionSpec::None | VersionSpec::Tag(VersionTag::Latest) => resolve_tag(\"latest\", hooks),\n        VersionSpec::Tag(tag) => resolve_tag(&tag.to_string(), hooks),\n    }\n}\n\nfn resolve_tag(tag: &str, hooks: Option<&ToolHooks<Pnpm>>) -> Fallible<Version> {\n    let (url, mut index) = fetch_pnpm_index(hooks)?;\n\n    match index.tags.remove(tag) {\n        Some(version) => {\n            debug!(\"Found pnpm@{} matching tag '{}' from {}\", version, tag, url);\n            Ok(version)\n        }\n        None => Err(ErrorKind::PnpmVersionNotFound {\n            matching: tag.into(),\n        }\n        .into()),\n    }\n}\n\nfn resolve_semver(matching: Range, hooks: Option<&ToolHooks<Pnpm>>) -> Fallible<Version> {\n    let (url, index) = fetch_pnpm_index(hooks)?;\n\n    let details_opt = index\n        .entries\n        .into_iter()\n        .find(|PackageDetails { version, .. }| matching.satisfies(version));\n\n    match details_opt {\n        Some(details) => {\n            debug!(\n                \"Found pnpm@{} matching requirement '{}' from {}\",\n                details.version, matching, url\n            );\n            Ok(details.version)\n        }\n        None => Err(ErrorKind::PnpmVersionNotFound {\n            matching: matching.to_string(),\n        }\n        .into()),\n    }\n}\n\n/// Fetch the index of available pnpm versions from the npm registry\nfn fetch_pnpm_index(hooks: Option<&ToolHooks<Pnpm>>) -> Fallible<(String, PackageIndex)> {\n    let url = match hooks {\n        Some(&ToolHooks {\n            index: Some(ref hook),\n            ..\n        }) => {\n            debug!(\"Using pnpm.index hook to determine pnpm index URL\");\n            hook.resolve(\"pnpm\")?\n        }\n        _ => public_registry_index(\"pnpm\"),\n    };\n\n    fetch_npm_registry(url, \"pnpm\")\n}\n"
  },
  {
    "path": "crates/volta-core/src/tool/registry.rs",
    "content": "use std::collections::HashMap;\nuse std::path::{Path, PathBuf};\n\nuse super::registry_fetch_error;\nuse crate::error::{Context, ErrorKind, Fallible};\nuse crate::fs::read_dir_eager;\nuse crate::style::progress_spinner;\nuse crate::version::{hashmap_version_serde, version_serde};\nuse attohttpc::header::ACCEPT;\nuse attohttpc::Response;\nuse cfg_if::cfg_if;\nuse node_semver::Version;\nuse serde::Deserialize;\n\n// Accept header needed to request the abbreviated metadata from the npm registry\n// See https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md\npub const NPM_ABBREVIATED_ACCEPT_HEADER: &str =\n    \"application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*\";\n\ncfg_if! {\n    if #[cfg(feature = \"mock-network\")] {\n        // TODO: We need to reconsider our mocking strategy in light of mockito deprecating the\n        // SERVER_URL constant: Since our acceptance tests run the binary in a separate process,\n        // we can't use `mockito::server_url()`, which relies on shared memory.\n        #[allow(deprecated)]\n        const SERVER_URL: &str = mockito::SERVER_URL;\n        pub fn public_registry_index(package: &str) -> String {\n            format!(\"{}/{}\", SERVER_URL, package)\n        }\n    } else {\n        pub fn public_registry_index(package: &str) -> String {\n            format!(\"https://registry.npmjs.org/{}\", package)\n        }\n    }\n}\n\n// fetch a registry that returns info in Npm format\npub fn fetch_npm_registry(url: String, name: &str) -> Fallible<(String, PackageIndex)> {\n    let spinner = progress_spinner(format!(\"Fetching npm registry: {}\", url));\n    let metadata: RawPackageMetadata = attohttpc::get(&url)\n        .header(ACCEPT, NPM_ABBREVIATED_ACCEPT_HEADER)\n        .send()\n        .and_then(Response::error_for_status)\n        .and_then(Response::json)\n        .with_context(registry_fetch_error(name, &url))?;\n\n    spinner.finish_and_clear();\n    Ok((url, metadata.into()))\n}\n\npub fn public_registry_package(package: &str, version: &str) -> String {\n    format!(\n        \"{}/-/{}-{}.tgz\",\n        public_registry_index(package),\n        package,\n        version\n    )\n}\n\n// need package and filename for namespaced tools like @yarnpkg/cli-dist, which is located at\n//   https://registry.npmjs.org/@yarnpkg/cli-dist/-/cli-dist-1.2.3.tgz\npub fn scoped_public_registry_package(scope: &str, package: &str, version: &str) -> String {\n    format!(\n        \"{}/{}/-/{}-{}.tgz\",\n        public_registry_index(scope),\n        package,\n        package,\n        version\n    )\n}\n\n/// Figure out the unpacked package directory name dynamically\n///\n/// Packages typically extract to a \"package\" directory, but not always\npub fn find_unpack_dir(in_dir: &Path) -> Fallible<PathBuf> {\n    let dirs: Vec<_> = read_dir_eager(in_dir)\n        .with_context(|| ErrorKind::PackageUnpackError)?\n        .collect();\n\n    // if there is only one directory, return that\n    if let [(entry, metadata)] = dirs.as_slice() {\n        if metadata.is_dir() {\n            return Ok(entry.path());\n        }\n    }\n    // there is more than just a single directory here, something is wrong\n    Err(ErrorKind::PackageUnpackError.into())\n}\n\n/// Details about a package in the npm Registry\n#[derive(Debug)]\npub struct PackageDetails {\n    pub(crate) version: Version,\n}\n\n/// Index of versions of a specific package from the npm Registry\npub struct PackageIndex {\n    pub tags: HashMap<String, Version>,\n    pub entries: Vec<PackageDetails>,\n}\n\n/// Package Metadata Response\n///\n/// See npm registry API doc:\n/// https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md\n#[derive(Deserialize, Debug)]\npub struct RawPackageMetadata {\n    pub name: String,\n    pub versions: HashMap<String, RawPackageVersionInfo>,\n    #[serde(\n        rename = \"dist-tags\",\n        deserialize_with = \"hashmap_version_serde::deserialize\"\n    )]\n    pub dist_tags: HashMap<String, Version>,\n}\n\n#[derive(Deserialize, Debug)]\npub struct RawPackageVersionInfo {\n    // there's a lot more in there, but right now just care about the version\n    #[serde(with = \"version_serde\")]\n    pub version: Version,\n    pub dist: RawDistInfo,\n}\n\n#[derive(Deserialize, Clone, Debug)]\npub struct RawDistInfo {\n    pub shasum: String,\n    pub tarball: String,\n}\n\nimpl From<RawPackageMetadata> for PackageIndex {\n    fn from(serial: RawPackageMetadata) -> PackageIndex {\n        let mut entries: Vec<PackageDetails> = serial\n            .versions\n            .into_values()\n            .map(|version_info| PackageDetails {\n                version: version_info.version,\n            })\n            .collect();\n\n        entries.sort_by(|a, b| b.version.cmp(&a.version));\n\n        PackageIndex {\n            tags: serial.dist_tags,\n            entries,\n        }\n    }\n}\n"
  },
  {
    "path": "crates/volta-core/src/tool/serial.rs",
    "content": "use std::cmp::Ordering;\n\nuse super::Spec;\nuse crate::error::{ErrorKind, Fallible};\nuse crate::version::{VersionSpec, VersionTag};\nuse once_cell::sync::Lazy;\nuse regex::Regex;\nuse validate_npm_package_name::{validate, Validity};\n\nstatic TOOL_SPEC_PATTERN: Lazy<Regex> = Lazy::new(|| {\n    Regex::new(\"^(?P<name>(?:@([^/]+?)[/])?([^/]+?))(@(?P<version>.+))?$\").expect(\"regex is valid\")\n});\nstatic HAS_VERSION: Lazy<Regex> = Lazy::new(|| Regex::new(r\"^[^\\s]+@\").expect(\"regex is valid\"));\n\n/// Methods for parsing a Spec out of string values\nimpl Spec {\n    pub fn from_str_and_version(tool_name: &str, version: VersionSpec) -> Self {\n        match tool_name {\n            \"node\" => Spec::Node(version),\n            \"npm\" => Spec::Npm(version),\n            \"pnpm\" => Spec::Pnpm(version),\n            \"yarn\" => Spec::Yarn(version),\n            package => Spec::Package(package.to_string(), version),\n        }\n    }\n\n    /// Try to parse a tool and version from a string like `<tool>[@<version>].\n    pub fn try_from_str(tool_spec: &str) -> Fallible<Self> {\n        let captures =\n            TOOL_SPEC_PATTERN\n                .captures(tool_spec)\n                .ok_or_else(|| ErrorKind::ParseToolSpecError {\n                    tool_spec: tool_spec.into(),\n                })?;\n\n        // Validate that the captured name is a valid NPM package name.\n        let name = &captures[\"name\"];\n        if let Validity::Invalid { errors, .. } = validate(name) {\n            return Err(ErrorKind::InvalidToolName {\n                name: name.into(),\n                errors,\n            }\n            .into());\n        }\n\n        let version = captures\n            .name(\"version\")\n            .map(|version| version.as_str().parse())\n            .transpose()?\n            .unwrap_or_default();\n\n        Ok(match name {\n            \"node\" => Spec::Node(version),\n            \"npm\" => Spec::Npm(version),\n            \"pnpm\" => Spec::Pnpm(version),\n            \"yarn\" => Spec::Yarn(version),\n            package => Spec::Package(package.into(), version),\n        })\n    }\n\n    /// Get a valid, sorted `Vec<Spec>` given a `Vec<String>`.\n    ///\n    /// Accounts for the following error conditions:\n    ///\n    /// - `volta install node 12`, where the user intended to install `node@12`\n    ///   but used syntax like in nodenv or nvm\n    /// - invalid version specs\n    ///\n    /// Returns a listed sorted so that if `node` is included in the list, it is\n    /// always first.\n    pub fn from_strings<T>(tool_strs: &[T], action: &str) -> Fallible<Vec<Spec>>\n    where\n        T: AsRef<str>,\n    {\n        Self::check_args(tool_strs, action)?;\n\n        let mut tools = tool_strs\n            .iter()\n            .map(|arg| Self::try_from_str(arg.as_ref()))\n            .collect::<Fallible<Vec<Spec>>>()?;\n\n        tools.sort_by(Self::sort_comparator);\n        Ok(tools)\n    }\n\n    /// Check the args for the bad patterns of\n    /// - `volta install <number>`\n    /// - `volta install <tool> <number>`\n    fn check_args<T>(args: &[T], action: &str) -> Fallible<()>\n    where\n        T: AsRef<str>,\n    {\n        let mut args = args.iter();\n\n        match (args.next(), args.next(), args.next()) {\n            // The case we are concerned with here is where we have `<number>`.\n            // That is, exactly one argument, which is a valid version specifier.\n            //\n            // - `volta install node@12` is allowed.\n            // - `volta install 12` is an error.\n            // - `volta install lts` is an error.\n            (Some(maybe_version), None, None) if is_version_like(maybe_version.as_ref()) => {\n                Err(ErrorKind::InvalidInvocationOfBareVersion {\n                    action: action.to_string(),\n                    version: maybe_version.as_ref().to_string(),\n                }\n                .into())\n            }\n            // The case we are concerned with here is where we have `<tool> <number>`.\n            // This is only interesting if there are exactly two args. Then we care\n            // whether the two items are a bare name (with no `@version`), followed\n            // by a valid version specifier (ignoring custom tags). That is:\n            //\n            // - `volta install node@lts latest` is allowed.\n            // - `volta install node latest` is an error.\n            // - `volta install node latest yarn` is allowed.\n            (Some(name), Some(maybe_version), None)\n                if !HAS_VERSION.is_match(name.as_ref())\n                    && is_version_like(maybe_version.as_ref()) =>\n            {\n                Err(ErrorKind::InvalidInvocation {\n                    action: action.to_string(),\n                    name: name.as_ref().to_string(),\n                    version: maybe_version.as_ref().to_string(),\n                }\n                .into())\n            }\n            _ => Ok(()),\n        }\n    }\n\n    /// Compare `Spec`s for sorting when converting from strings\n    ///\n    /// We want to preserve the original order as much as possible, so we treat tools in\n    /// the same tool category as equal. We still need to pull Node to the front of the\n    /// list, followed by Npm, pnpm, Yarn, and then Packages last.\n    fn sort_comparator(left: &Spec, right: &Spec) -> Ordering {\n        match (left, right) {\n            (Spec::Node(_), Spec::Node(_)) => Ordering::Equal,\n            (Spec::Node(_), _) => Ordering::Less,\n            (_, Spec::Node(_)) => Ordering::Greater,\n            (Spec::Npm(_), Spec::Npm(_)) => Ordering::Equal,\n            (Spec::Npm(_), _) => Ordering::Less,\n            (_, Spec::Npm(_)) => Ordering::Greater,\n            (Spec::Pnpm(_), Spec::Pnpm(_)) => Ordering::Equal,\n            (Spec::Pnpm(_), _) => Ordering::Less,\n            (_, Spec::Pnpm(_)) => Ordering::Greater,\n            (Spec::Yarn(_), Spec::Yarn(_)) => Ordering::Equal,\n            (Spec::Yarn(_), _) => Ordering::Less,\n            (_, Spec::Yarn(_)) => Ordering::Greater,\n            (Spec::Package(_, _), Spec::Package(_, _)) => Ordering::Equal,\n        }\n    }\n}\n\n/// Determine if a given string is \"version-like\".\n///\n/// This means it is either 'latest', 'lts', a Version, or a Version Range.\nfn is_version_like(value: &str) -> bool {\n    matches!(\n        value.parse(),\n        Ok(VersionSpec::Exact(_))\n            | Ok(VersionSpec::Semver(_))\n            | Ok(VersionSpec::Tag(VersionTag::Latest))\n            | Ok(VersionSpec::Tag(VersionTag::Lts))\n    )\n}\n\n#[cfg(test)]\nmod tests {\n    mod try_from_str {\n        use std::str::FromStr as _;\n\n        use super::super::super::Spec;\n        use crate::version::{VersionSpec, VersionTag};\n\n        const LTS: &str = \"lts\";\n        const LATEST: &str = \"latest\";\n        const MAJOR: &str = \"3\";\n        const MINOR: &str = \"3.0\";\n        const PATCH: &str = \"3.0.0\";\n        const BETA: &str = \"beta\";\n\n        /// Convenience macro for generating the <tool>@<version> string.\n        macro_rules! versioned_tool {\n            ($tool:expr, $version:expr) => {\n                format!(\"{}@{}\", $tool, $version)\n            };\n        }\n\n        #[test]\n        fn parses_bare_node() {\n            assert_eq!(\n                Spec::try_from_str(\"node\").expect(\"succeeds\"),\n                Spec::Node(VersionSpec::default())\n            );\n        }\n\n        #[test]\n        fn parses_node_with_valid_versions() {\n            let tool = \"node\";\n\n            assert_eq!(\n                Spec::try_from_str(&versioned_tool!(tool, MAJOR)).expect(\"succeeds\"),\n                Spec::Node(VersionSpec::from_str(MAJOR).expect(\"`VersionSpec` has its own tests\"))\n            );\n\n            assert_eq!(\n                Spec::try_from_str(&versioned_tool!(tool, MINOR)).expect(\"succeeds\"),\n                Spec::Node(VersionSpec::from_str(MINOR).expect(\"`VersionSpec` has its own tests\"))\n            );\n\n            assert_eq!(\n                Spec::try_from_str(&versioned_tool!(tool, PATCH)).expect(\"succeeds\"),\n                Spec::Node(VersionSpec::from_str(PATCH).expect(\"`VersionSpec` has its own tests\"))\n            );\n\n            assert_eq!(\n                Spec::try_from_str(&versioned_tool!(tool, LATEST)).expect(\"succeeds\"),\n                Spec::Node(VersionSpec::Tag(VersionTag::Latest))\n            );\n\n            assert_eq!(\n                Spec::try_from_str(&versioned_tool!(tool, LTS)).expect(\"succeeds\"),\n                Spec::Node(VersionSpec::Tag(VersionTag::Lts))\n            );\n        }\n\n        #[test]\n        fn parses_bare_yarn() {\n            assert_eq!(\n                Spec::try_from_str(\"yarn\").expect(\"succeeds\"),\n                Spec::Yarn(VersionSpec::default())\n            );\n        }\n\n        #[test]\n        fn parses_yarn_with_valid_versions() {\n            let tool = \"yarn\";\n\n            assert_eq!(\n                Spec::try_from_str(&versioned_tool!(tool, MAJOR)).expect(\"succeeds\"),\n                Spec::Yarn(VersionSpec::from_str(MAJOR).expect(\"`VersionSpec` has its own tests\"))\n            );\n\n            assert_eq!(\n                Spec::try_from_str(&versioned_tool!(tool, MINOR)).expect(\"succeeds\"),\n                Spec::Yarn(VersionSpec::from_str(MINOR).expect(\"`VersionSpec` has its own tests\"))\n            );\n\n            assert_eq!(\n                Spec::try_from_str(&versioned_tool!(tool, PATCH)).expect(\"succeeds\"),\n                Spec::Yarn(VersionSpec::from_str(PATCH).expect(\"`VersionSpec` has its own tests\"))\n            );\n\n            assert_eq!(\n                Spec::try_from_str(&versioned_tool!(tool, LATEST)).expect(\"succeeds\"),\n                Spec::Yarn(VersionSpec::Tag(VersionTag::Latest))\n            );\n        }\n\n        #[test]\n        fn parses_bare_packages() {\n            let package = \"ember-cli\";\n            assert_eq!(\n                Spec::try_from_str(package).expect(\"succeeds\"),\n                Spec::Package(package.into(), VersionSpec::default())\n            );\n        }\n\n        #[test]\n        fn parses_namespaced_packages() {\n            let package = \"@types/lodash\";\n            assert_eq!(\n                Spec::try_from_str(package).expect(\"succeeds\"),\n                Spec::Package(package.into(), VersionSpec::default())\n            );\n        }\n\n        #[test]\n        fn parses_bare_packages_with_valid_versions() {\n            let package = \"something-awesome\";\n\n            assert_eq!(\n                Spec::try_from_str(&versioned_tool!(package, MAJOR)).expect(\"succeeds\"),\n                Spec::Package(\n                    package.into(),\n                    VersionSpec::from_str(MAJOR).expect(\"`VersionSpec` has its own tests\")\n                )\n            );\n\n            assert_eq!(\n                Spec::try_from_str(&versioned_tool!(package, MINOR)).expect(\"succeeds\"),\n                Spec::Package(\n                    package.into(),\n                    VersionSpec::from_str(MINOR).expect(\"`VersionSpec` has its own tests\")\n                )\n            );\n\n            assert_eq!(\n                Spec::try_from_str(&versioned_tool!(package, PATCH)).expect(\"succeeds\"),\n                Spec::Package(\n                    package.into(),\n                    VersionSpec::from_str(PATCH).expect(\"`VersionSpec` has its own tests\")\n                )\n            );\n\n            assert_eq!(\n                Spec::try_from_str(&versioned_tool!(package, LATEST)).expect(\"succeeds\"),\n                Spec::Package(package.into(), VersionSpec::Tag(VersionTag::Latest))\n            );\n\n            assert_eq!(\n                Spec::try_from_str(&versioned_tool!(package, LTS)).expect(\"succeeds\"),\n                Spec::Package(package.into(), VersionSpec::Tag(VersionTag::Lts))\n            );\n\n            assert_eq!(\n                Spec::try_from_str(&versioned_tool!(package, BETA)).expect(\"succeeds\"),\n                Spec::Package(\n                    package.into(),\n                    VersionSpec::Tag(VersionTag::Custom(BETA.into()))\n                )\n            );\n        }\n\n        #[test]\n        fn parses_namespaced_packages_with_valid_versions() {\n            let package = \"@something/awesome\";\n\n            assert_eq!(\n                Spec::try_from_str(&versioned_tool!(package, MAJOR)).expect(\"succeeds\"),\n                Spec::Package(\n                    package.into(),\n                    VersionSpec::from_str(MAJOR).expect(\"`VersionSpec` has its own tests\")\n                )\n            );\n\n            assert_eq!(\n                Spec::try_from_str(&versioned_tool!(package, MINOR)).expect(\"succeeds\"),\n                Spec::Package(\n                    package.into(),\n                    VersionSpec::from_str(MINOR).expect(\"`VersionSpec` has its own tests\")\n                )\n            );\n\n            assert_eq!(\n                Spec::try_from_str(&versioned_tool!(package, PATCH)).expect(\"succeeds\"),\n                Spec::Package(\n                    package.into(),\n                    VersionSpec::from_str(PATCH).expect(\"`VersionSpec` has its own tests\")\n                )\n            );\n\n            assert_eq!(\n                Spec::try_from_str(&versioned_tool!(package, LATEST)).expect(\"succeeds\"),\n                Spec::Package(package.into(), VersionSpec::Tag(VersionTag::Latest))\n            );\n\n            assert_eq!(\n                Spec::try_from_str(&versioned_tool!(package, LTS)).expect(\"succeeds\"),\n                Spec::Package(package.into(), VersionSpec::Tag(VersionTag::Lts))\n            );\n\n            assert_eq!(\n                Spec::try_from_str(&versioned_tool!(package, BETA)).expect(\"succeeds\"),\n                Spec::Package(\n                    package.into(),\n                    VersionSpec::Tag(VersionTag::Custom(BETA.into()))\n                )\n            );\n        }\n    }\n\n    mod from_strings {\n        use super::super::*;\n        use std::str::FromStr;\n\n        static PIN: &str = \"pin\";\n\n        #[test]\n        fn special_cases_just_number() {\n            let version = \"1.2.3\";\n            let args: Vec<String> = vec![version.into()];\n\n            let err = Spec::from_strings(&args, PIN).unwrap_err();\n\n            assert_eq!(\n                err.kind(),\n                &ErrorKind::InvalidInvocationOfBareVersion {\n                    action: PIN.into(),\n                    version: version.into()\n                },\n                \"`volta <action> number` results in the correct error\"\n            );\n        }\n\n        #[test]\n        fn special_cases_tool_space_number() {\n            let name = \"potato\";\n            let version = \"1.2.3\";\n            let args: Vec<String> = vec![name.into(), version.into()];\n\n            let err = Spec::from_strings(&args, PIN).unwrap_err();\n\n            assert_eq!(\n                err.kind(),\n                &ErrorKind::InvalidInvocation {\n                    action: PIN.into(),\n                    name: name.into(),\n                    version: version.into()\n                },\n                \"`volta <action> tool number` results in the correct error\"\n            );\n        }\n\n        #[test]\n        fn leaves_other_scenarios_alone() {\n            let empty: Vec<&str> = Vec::new();\n            assert_eq!(\n                Spec::from_strings(&empty, PIN).expect(\"is ok\").len(),\n                empty.len(),\n                \"when there are no args\"\n            );\n\n            let only_one = [\"node\".to_owned()];\n            assert_eq!(\n                Spec::from_strings(&only_one, PIN).expect(\"is ok\").len(),\n                only_one.len(),\n                \"when there is only one arg\"\n            );\n\n            let one_with_explicit_verson = [\"10@latest\".to_owned()];\n            assert_eq!(\n                Spec::from_strings(&one_with_explicit_verson, PIN)\n                    .expect(\"is ok\")\n                    .len(),\n                only_one.len(),\n                \"when the sole arg is version-like but has an explicit version\"\n            );\n\n            let two_but_unmistakable = [\"12\".to_owned(), \"node\".to_owned()];\n            assert_eq!(\n                Spec::from_strings(&two_but_unmistakable, PIN)\n                    .expect(\"is ok\")\n                    .len(),\n                two_but_unmistakable.len(),\n                \"when there are two args but the order is not likely to be a mistake\"\n            );\n\n            let two_but_valid_first = [\"node@lts\".to_owned(), \"12\".to_owned()];\n            assert_eq!(\n                Spec::from_strings(&two_but_valid_first, PIN)\n                    .expect(\"is ok\")\n                    .len(),\n                two_but_valid_first.len(),\n                \"when there are two args but the first is a valid tool spec\"\n            );\n\n            let more_than_two_tools = [\"node\".to_owned(), \"12\".to_owned(), \"yarn\".to_owned()];\n            assert_eq!(\n                Spec::from_strings(&more_than_two_tools, PIN)\n                    .expect(\"is ok\")\n                    .len(),\n                more_than_two_tools.len(),\n                \"when there are more than two args\"\n            );\n        }\n\n        #[test]\n        fn sorts_node_npm_yarn_to_front() {\n            let multiple = [\n                \"ember-cli@3\".to_owned(),\n                \"yarn\".to_owned(),\n                \"npm@5\".to_owned(),\n                \"node@latest\".to_owned(),\n            ];\n            let expected = [\n                Spec::Node(VersionSpec::Tag(VersionTag::Latest)),\n                Spec::Npm(VersionSpec::from_str(\"5\").expect(\"requirement is valid\")),\n                Spec::Yarn(VersionSpec::default()),\n                Spec::Package(\n                    \"ember-cli\".to_owned(),\n                    VersionSpec::from_str(\"3\").expect(\"requirement is valid\"),\n                ),\n            ];\n            assert_eq!(Spec::from_strings(&multiple, PIN).expect(\"is ok\"), expected);\n        }\n\n        #[test]\n        fn keeps_package_order_unchanged() {\n            let packages_with_node = [\"typescript@latest\", \"ember-cli@3\", \"node@lts\", \"mocha\"];\n            let expected = [\n                Spec::Node(VersionSpec::Tag(VersionTag::Lts)),\n                Spec::Package(\n                    \"typescript\".to_owned(),\n                    VersionSpec::Tag(VersionTag::Latest),\n                ),\n                Spec::Package(\n                    \"ember-cli\".to_owned(),\n                    VersionSpec::from_str(\"3\").expect(\"requirement is valid\"),\n                ),\n                Spec::Package(\"mocha\".to_owned(), VersionSpec::default()),\n            ];\n\n            assert_eq!(\n                Spec::from_strings(&packages_with_node, PIN).expect(\"is ok\"),\n                expected\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "crates/volta-core/src/tool/yarn/fetch.rs",
    "content": "//! Provides fetcher for Yarn distributions\n\nuse std::fs::File;\nuse std::path::Path;\n\nuse super::super::download_tool_error;\nuse super::super::registry::{\n    find_unpack_dir, public_registry_package, scoped_public_registry_package,\n};\nuse crate::error::{Context, ErrorKind, Fallible};\nuse crate::fs::{create_staging_dir, create_staging_file, rename, set_executable};\nuse crate::hook::YarnHooks;\nuse crate::layout::volta_home;\nuse crate::style::{progress_bar, tool_version};\nuse crate::tool::{self, Yarn};\nuse crate::version::VersionSpec;\nuse archive::{Archive, Tarball};\nuse fs_utils::ensure_containing_dir_exists;\nuse log::debug;\nuse node_semver::Version;\n\npub fn fetch(version: &Version, hooks: Option<&YarnHooks>) -> Fallible<()> {\n    let yarn_dir = volta_home()?.yarn_inventory_dir();\n    let cache_file = yarn_dir.join(Yarn::archive_filename(&version.to_string()));\n\n    let (archive, staging) = match load_cached_distro(&cache_file) {\n        Some(archive) => {\n            debug!(\n                \"Loading {} from cached archive at '{}'\",\n                tool_version(\"yarn\", version),\n                cache_file.display(),\n            );\n            (archive, None)\n        }\n        None => {\n            let staging = create_staging_file()?;\n            let remote_url = determine_remote_url(version, hooks)?;\n            let archive = fetch_remote_distro(version, &remote_url, staging.path())?;\n            (archive, Some(staging))\n        }\n    };\n\n    unpack_archive(archive, version)?;\n\n    if let Some(staging_file) = staging {\n        ensure_containing_dir_exists(&cache_file).with_context(|| {\n            ErrorKind::ContainingDirError {\n                path: cache_file.clone(),\n            }\n        })?;\n        staging_file\n            .persist(cache_file)\n            .with_context(|| ErrorKind::PersistInventoryError {\n                tool: \"Yarn\".into(),\n            })?;\n    }\n\n    Ok(())\n}\n\n/// Unpack the yarn archive into the image directory so that it is ready for use\nfn unpack_archive(archive: Box<dyn Archive>, version: &Version) -> Fallible<()> {\n    let temp = create_staging_dir()?;\n    debug!(\"Unpacking yarn into '{}'\", temp.path().display());\n\n    let progress = progress_bar(\n        archive.origin(),\n        &tool_version(\"yarn\", version),\n        archive.compressed_size(),\n    );\n    let version_string = version.to_string();\n\n    archive\n        .unpack(temp.path(), &mut |_, read| {\n            progress.inc(read as u64);\n        })\n        .with_context(|| ErrorKind::UnpackArchiveError {\n            tool: \"Yarn\".into(),\n            version: version_string.clone(),\n        })?;\n\n    let unpack_dir = find_unpack_dir(temp.path())?;\n    // \"bin/yarn\" is not executable in the @yarnpkg/cli-dist package\n    ensure_bin_is_executable(&unpack_dir, \"yarn\")?;\n\n    let dest = volta_home()?.yarn_image_dir(&version_string);\n    ensure_containing_dir_exists(&dest)\n        .with_context(|| ErrorKind::ContainingDirError { path: dest.clone() })?;\n\n    rename(unpack_dir, &dest).with_context(|| ErrorKind::SetupToolImageError {\n        tool: \"Yarn\".into(),\n        version: version_string.clone(),\n        dir: dest.clone(),\n    })?;\n\n    progress.finish_and_clear();\n\n    // Note: We write this after the progress bar is finished to avoid display bugs with re-renders of the progress\n    debug!(\"Installing yarn in '{}'\", dest.display());\n\n    Ok(())\n}\n\n/// Return the archive if it is valid. It may have been corrupted or interrupted in the middle of\n/// downloading.\n// ISSUE(#134) - verify checksum\nfn load_cached_distro(file: &Path) -> Option<Box<dyn Archive>> {\n    if file.is_file() {\n        let file = File::open(file).ok()?;\n        Tarball::load(file).ok()\n    } else {\n        None\n    }\n}\n\n/// Determine the remote URL to download from, using the hooks if available\nfn determine_remote_url(version: &Version, hooks: Option<&YarnHooks>) -> Fallible<String> {\n    let version_str = version.to_string();\n    match hooks {\n        Some(&YarnHooks {\n            distro: Some(ref hook),\n            ..\n        }) => {\n            debug!(\"Using yarn.distro hook to determine download URL\");\n            let distro_file_name = Yarn::archive_filename(&version_str);\n            hook.resolve(version, &distro_file_name)\n        }\n        _ => {\n            if version.major >= 2 {\n                Ok(scoped_public_registry_package(\n                    \"@yarnpkg\",\n                    \"cli-dist\",\n                    &version_str,\n                ))\n            } else {\n                Ok(public_registry_package(\"yarn\", &version_str))\n            }\n        }\n    }\n}\n\n/// Fetch the distro archive from the internet\nfn fetch_remote_distro(\n    version: &Version,\n    url: &str,\n    staging_path: &Path,\n) -> Fallible<Box<dyn Archive>> {\n    debug!(\"Downloading {} from {}\", tool_version(\"yarn\", version), url);\n    Tarball::fetch(url, staging_path).with_context(download_tool_error(\n        tool::Spec::Yarn(VersionSpec::Exact(version.clone())),\n        url,\n    ))\n}\n\nfn ensure_bin_is_executable(unpack_dir: &Path, tool: &str) -> Fallible<()> {\n    let exec_path = unpack_dir.join(\"bin\").join(tool);\n    set_executable(&exec_path).with_context(|| ErrorKind::SetToolExecutable { tool: tool.into() })\n}\n"
  },
  {
    "path": "crates/volta-core/src/tool/yarn/metadata.rs",
    "content": "use std::collections::BTreeSet;\n\nuse crate::version::version_serde;\nuse node_semver::Version;\nuse serde::Deserialize;\n\n/// The public Yarn index.\npub struct YarnIndex {\n    pub(super) entries: BTreeSet<Version>,\n}\n\n#[derive(Deserialize)]\npub struct RawYarnIndex(Vec<RawYarnEntry>);\n\n#[derive(Deserialize)]\npub struct RawYarnEntry {\n    /// Yarn releases are given a tag name of the form \"v$version\" where $version\n    /// is the release's version string.\n    #[serde(with = \"version_serde\")]\n    pub tag_name: Version,\n\n    /// The GitHub API provides a list of assets. Some Yarn releases don't include\n    /// a tarball, so we don't support them and remove them from the set of available\n    /// Yarn versions.\n    pub assets: Vec<RawYarnAsset>,\n}\n\nimpl RawYarnEntry {\n    /// Is this entry a full release, i.e., does this entry's asset list include a\n    /// proper release tarball?\n    fn is_full_release(&self) -> bool {\n        let release_filename = &format!(\"yarn-v{}.tar.gz\", self.tag_name)[..];\n        self.assets\n            .iter()\n            .any(|raw_asset| raw_asset.name == release_filename)\n    }\n}\n\n#[derive(Deserialize)]\npub struct RawYarnAsset {\n    /// The filename of an asset included in a Yarn GitHub release.\n    pub name: String,\n}\n\nimpl From<RawYarnIndex> for YarnIndex {\n    fn from(raw: RawYarnIndex) -> YarnIndex {\n        let mut entries = BTreeSet::new();\n        for entry in raw.0 {\n            if entry.is_full_release() {\n                entries.insert(entry.tag_name);\n            }\n        }\n        YarnIndex { entries }\n    }\n}\n"
  },
  {
    "path": "crates/volta-core/src/tool/yarn/mod.rs",
    "content": "use std::fmt::{self, Display};\n\nuse super::{\n    check_fetched, check_shim_reachable, debug_already_fetched, info_fetched, info_installed,\n    info_pinned, info_project_version, FetchStatus, Tool,\n};\nuse crate::error::{ErrorKind, Fallible};\nuse crate::inventory::yarn_available;\nuse crate::session::Session;\nuse crate::style::tool_version;\nuse crate::sync::VoltaLock;\nuse node_semver::Version;\n\nmod fetch;\nmod metadata;\nmod resolve;\n\npub use resolve::resolve;\n\n/// The Tool implementation for fetching and installing Yarn\npub struct Yarn {\n    pub(super) version: Version,\n}\n\nimpl Yarn {\n    pub fn new(version: Version) -> Self {\n        Yarn { version }\n    }\n\n    pub fn archive_basename(version: &str) -> String {\n        format!(\"yarn-v{}\", version)\n    }\n\n    pub fn archive_filename(version: &str) -> String {\n        format!(\"{}.tar.gz\", Yarn::archive_basename(version))\n    }\n\n    pub(crate) fn ensure_fetched(&self, session: &mut Session) -> Fallible<()> {\n        match check_fetched(|| yarn_available(&self.version))? {\n            FetchStatus::AlreadyFetched => {\n                debug_already_fetched(self);\n                Ok(())\n            }\n            FetchStatus::FetchNeeded(_lock) => fetch::fetch(&self.version, session.hooks()?.yarn()),\n        }\n    }\n}\n\nimpl Tool for Yarn {\n    fn fetch(self: Box<Self>, session: &mut Session) -> Fallible<()> {\n        self.ensure_fetched(session)?;\n\n        info_fetched(self);\n        Ok(())\n    }\n    fn install(self: Box<Self>, session: &mut Session) -> Fallible<()> {\n        // Acquire a lock on the Volta directory, if possible, to prevent concurrent changes\n        let _lock = VoltaLock::acquire();\n        self.ensure_fetched(session)?;\n\n        session\n            .toolchain_mut()?\n            .set_active_yarn(Some(self.version.clone()))?;\n\n        info_installed(&self);\n        check_shim_reachable(\"yarn\");\n\n        if let Ok(Some(project)) = session.project_platform() {\n            if let Some(yarn) = &project.yarn {\n                info_project_version(tool_version(\"yarn\", yarn), &self);\n            }\n        }\n        Ok(())\n    }\n    fn pin(self: Box<Self>, session: &mut Session) -> Fallible<()> {\n        if session.project()?.is_some() {\n            self.ensure_fetched(session)?;\n\n            // Note: We know this will succeed, since we checked above\n            let project = session.project_mut()?.unwrap();\n            project.pin_yarn(Some(self.version.clone()))?;\n\n            info_pinned(self);\n            Ok(())\n        } else {\n            Err(ErrorKind::NotInPackage.into())\n        }\n    }\n}\n\nimpl Display for Yarn {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        f.write_str(&tool_version(\"yarn\", &self.version))\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_yarn_archive_basename() {\n        assert_eq!(Yarn::archive_basename(\"1.2.3\"), \"yarn-v1.2.3\");\n    }\n\n    #[test]\n    fn test_yarn_archive_filename() {\n        assert_eq!(Yarn::archive_filename(\"1.2.3\"), \"yarn-v1.2.3.tar.gz\");\n    }\n}\n"
  },
  {
    "path": "crates/volta-core/src/tool/yarn/resolve.rs",
    "content": "//! Provides resolution of Yarn requirements into specific versions\n\nuse super::super::registry::{\n    fetch_npm_registry, public_registry_index, PackageDetails, PackageIndex,\n};\nuse super::super::registry_fetch_error;\nuse super::metadata::{RawYarnIndex, YarnIndex};\nuse crate::error::{Context, ErrorKind, Fallible};\nuse crate::hook::{RegistryFormat, YarnHooks};\nuse crate::session::Session;\nuse crate::style::progress_spinner;\nuse crate::version::{parse_version, VersionSpec, VersionTag};\nuse attohttpc::Response;\nuse log::debug;\nuse node_semver::{Range, Version};\n\npub fn resolve(matching: VersionSpec, session: &mut Session) -> Fallible<Version> {\n    let hooks = session.hooks()?.yarn();\n    match matching {\n        VersionSpec::Semver(requirement) => resolve_semver(requirement, hooks),\n        VersionSpec::Exact(version) => Ok(version),\n        VersionSpec::None => resolve_tag(VersionTag::Latest, hooks),\n        VersionSpec::Tag(tag) => resolve_tag(tag, hooks),\n    }\n}\n\nfn resolve_tag(tag: VersionTag, hooks: Option<&YarnHooks>) -> Fallible<Version> {\n    // This triage is complicated because we need to maintain the legacy behavior of hooks\n    // First, if the tag is 'latest' and we have a 'latest' hook, we use the old behavior\n    // Next, if the tag is 'latest' and we _do not_ have a 'latest' hook, we use the new behavior\n    // Next, if the tag is _not_ 'latest' and we have an 'index' hook, we show an error since\n    //     the previous behavior did not support generic tags\n    // Finally, we don't have any relevant hooks, so we can use the new behavior\n    match (tag, hooks) {\n        (\n            VersionTag::Latest,\n            Some(&YarnHooks {\n                latest: Some(ref hook),\n                ..\n            }),\n        ) => {\n            debug!(\"Using yarn.latest hook to determine latest-version URL\");\n            // does yarn3 use latest-version? no\n            resolve_latest_legacy(hook.resolve(\"latest-version\")?)\n        }\n        (VersionTag::Latest, _) => resolve_custom_tag(VersionTag::Latest.to_string()),\n        (tag, Some(&YarnHooks { index: Some(_), .. })) => Err(ErrorKind::YarnVersionNotFound {\n            matching: tag.to_string(),\n        }\n        .into()),\n        (tag, _) => resolve_custom_tag(tag.to_string()),\n    }\n}\n\nfn resolve_semver(matching: Range, hooks: Option<&YarnHooks>) -> Fallible<Version> {\n    // For semver, the triage is less complicated: The previous behavior _always_ used\n    // the 'index' hook, so we can check for that to decide which behavior to use.\n    //\n    // If the user specifies a format for the registry, we use that. Otherwise Github format\n    // is the default legacy behavior.\n    if let Some(&YarnHooks {\n        index: Some(ref hook),\n        ..\n    }) = hooks\n    {\n        debug!(\"Using yarn.index hook to determine yarn index URL\");\n        match hook.format {\n            RegistryFormat::Github => resolve_semver_legacy(matching, hook.resolve(\"releases\")?),\n            RegistryFormat::Npm => resolve_semver_npm(matching, hook.resolve(\"\")?),\n        }\n    } else {\n        resolve_semver_from_registry(matching)\n    }\n}\n\nfn fetch_yarn_index(package: &str) -> Fallible<(String, PackageIndex)> {\n    let url = public_registry_index(package);\n    fetch_npm_registry(url, \"Yarn\")\n}\n\nfn resolve_custom_tag(tag: String) -> Fallible<Version> {\n    // first try yarn2+, which uses \"@yarnpkg/cli-dist\" instead of \"yarn\"\n    if let Ok((url, mut index)) = fetch_yarn_index(\"@yarnpkg/cli-dist\") {\n        if let Some(version) = index.tags.remove(&tag) {\n            debug!(\"Found yarn@{} matching tag '{}' from {}\", version, tag, url);\n            if version.major == 2 {\n                return Err(ErrorKind::Yarn2NotSupported.into());\n            }\n            return Ok(version);\n        }\n    }\n    debug!(\n        \"Did not find yarn matching tag '{}' from @yarnpkg/cli-dist\",\n        tag\n    );\n\n    let (url, mut index) = fetch_yarn_index(\"yarn\")?;\n    match index.tags.remove(&tag) {\n        Some(version) => {\n            debug!(\"Found yarn@{} matching tag '{}' from {}\", version, tag, url);\n            Ok(version)\n        }\n        None => Err(ErrorKind::YarnVersionNotFound { matching: tag }.into()),\n    }\n}\n\nfn resolve_latest_legacy(url: String) -> Fallible<Version> {\n    let response_text = attohttpc::get(&url)\n        .send()\n        .and_then(Response::error_for_status)\n        .and_then(Response::text)\n        .with_context(|| ErrorKind::YarnLatestFetchError {\n            from_url: url.clone(),\n        })?;\n\n    debug!(\"Found yarn latest version ({}) from {}\", response_text, url);\n    parse_version(response_text)\n}\n\nfn resolve_semver_from_registry(matching: Range) -> Fallible<Version> {\n    // first try yarn2+, which uses \"@yarnpkg/cli-dist\" instead of \"yarn\"\n    if let Ok((url, index)) = fetch_yarn_index(\"@yarnpkg/cli-dist\") {\n        let matching_entries: Vec<PackageDetails> = index\n            .entries\n            .into_iter()\n            .filter(|PackageDetails { version, .. }| matching.satisfies(version))\n            .collect();\n\n        if !matching_entries.is_empty() {\n            let details_opt = matching_entries\n                .iter()\n                .find(|PackageDetails { version, .. }| version.major >= 3);\n\n            match details_opt {\n                Some(details) => {\n                    debug!(\n                        \"Found yarn@{} matching requirement '{}' from {}\",\n                        details.version, matching, url\n                    );\n                    return Ok(details.version.clone());\n                }\n                None => {\n                    return Err(ErrorKind::Yarn2NotSupported.into());\n                }\n            }\n        }\n    }\n    debug!(\n        \"Did not find yarn matching requirement '{}' for @yarnpkg/cli-dist\",\n        matching\n    );\n\n    let (url, index) = fetch_yarn_index(\"yarn\")?;\n\n    let details_opt = index\n        .entries\n        .into_iter()\n        .find(|PackageDetails { version, .. }| matching.satisfies(version));\n\n    match details_opt {\n        Some(details) => {\n            debug!(\n                \"Found yarn@{} matching requirement '{}' from {}\",\n                details.version, matching, url\n            );\n            Ok(details.version)\n        }\n        // at this point Yarn is not found in either registry\n        None => Err(ErrorKind::YarnVersionNotFound {\n            matching: matching.to_string(),\n        }\n        .into()),\n    }\n}\n\nfn resolve_semver_legacy(matching: Range, url: String) -> Fallible<Version> {\n    let spinner = progress_spinner(format!(\"Fetching registry: {}\", url));\n    let releases: RawYarnIndex = attohttpc::get(&url)\n        .send()\n        .and_then(Response::error_for_status)\n        .and_then(Response::json)\n        .with_context(registry_fetch_error(\"Yarn\", &url))?;\n    let index = YarnIndex::from(releases);\n    let releases = index.entries;\n    spinner.finish_and_clear();\n    let version_opt = releases.into_iter().rev().find(|v| matching.satisfies(v));\n\n    match version_opt {\n        Some(version) => {\n            debug!(\n                \"Found yarn@{} matching requirement '{}' from {}\",\n                version, matching, url\n            );\n            Ok(version)\n        }\n        None => Err(ErrorKind::YarnVersionNotFound {\n            matching: matching.to_string(),\n        }\n        .into()),\n    }\n}\n\nfn resolve_semver_npm(matching: Range, url: String) -> Fallible<Version> {\n    let (url, index) = fetch_npm_registry(url, \"Yarn\")?;\n\n    let details_opt = index\n        .entries\n        .into_iter()\n        .find(|PackageDetails { version, .. }| matching.satisfies(version));\n\n    match details_opt {\n        Some(details) => {\n            debug!(\n                \"Found yarn@{} matching requirement '{}' from {}\",\n                details.version, matching, url\n            );\n            Ok(details.version)\n        }\n        None => Err(ErrorKind::YarnVersionNotFound {\n            matching: matching.to_string(),\n        }\n        .into()),\n    }\n}\n"
  },
  {
    "path": "crates/volta-core/src/toolchain/mod.rs",
    "content": "use std::fs::write;\n\nuse crate::error::{Context, ErrorKind, Fallible};\nuse crate::fs::touch;\nuse crate::layout::volta_home;\nuse crate::platform::PlatformSpec;\nuse log::debug;\nuse node_semver::Version;\nuse once_cell::unsync::OnceCell;\nuse readext::ReadExt;\n\npub mod serial;\n\n/// Lazily loaded toolchain\npub struct LazyToolchain {\n    toolchain: OnceCell<Toolchain>,\n}\n\nimpl LazyToolchain {\n    /// Creates a new `LazyToolchain`\n    pub fn init() -> Self {\n        LazyToolchain {\n            toolchain: OnceCell::new(),\n        }\n    }\n\n    /// Forces loading of the toolchain and returns an immutable reference to it\n    pub fn get(&self) -> Fallible<&Toolchain> {\n        self.toolchain.get_or_try_init(Toolchain::current)\n    }\n\n    /// Forces loading of the toolchain and returns a mutable reference to it\n    pub fn get_mut(&mut self) -> Fallible<&mut Toolchain> {\n        let _ = self.toolchain.get_or_try_init(Toolchain::current)?;\n        Ok(self.toolchain.get_mut().unwrap())\n    }\n}\n\npub struct Toolchain {\n    platform: Option<PlatformSpec>,\n}\n\nimpl Toolchain {\n    fn current() -> Fallible<Toolchain> {\n        let path = volta_home()?.default_platform_file();\n        let src = touch(path)\n            .and_then(|mut file| file.read_into_string())\n            .with_context(|| ErrorKind::ReadPlatformError {\n                file: path.to_owned(),\n            })?;\n\n        let platform: Option<PlatformSpec> = serial::Platform::try_from(src)?.into();\n        if platform.is_some() {\n            debug!(\"Found default configuration at '{}'\", path.display());\n        }\n        Ok(Toolchain { platform })\n    }\n\n    pub fn platform(&self) -> Option<&PlatformSpec> {\n        self.platform.as_ref()\n    }\n\n    /// Set the active Node version in the default platform file.\n    pub fn set_active_node(&mut self, node_version: &Version) -> Fallible<()> {\n        let mut dirty = false;\n\n        match self.platform.as_mut() {\n            Some(platform) => {\n                if platform.node != *node_version {\n                    platform.node = node_version.clone();\n                    dirty = true;\n                }\n            }\n            None => {\n                self.platform = Some(PlatformSpec {\n                    node: node_version.clone(),\n                    npm: None,\n                    pnpm: None,\n                    yarn: None,\n                });\n                dirty = true;\n            }\n        }\n\n        if dirty {\n            self.save()?;\n        }\n\n        Ok(())\n    }\n\n    /// Set the active Yarn version in the default platform file.\n    pub fn set_active_yarn(&mut self, yarn: Option<Version>) -> Fallible<()> {\n        if let Some(platform) = self.platform.as_mut() {\n            if platform.yarn != yarn {\n                platform.yarn = yarn;\n                self.save()?;\n            }\n        } else if yarn.is_some() {\n            return Err(ErrorKind::NoDefaultNodeVersion {\n                tool: \"Yarn\".into(),\n            }\n            .into());\n        }\n\n        Ok(())\n    }\n\n    /// Set the active pnpm version in the default platform file.\n    pub fn set_active_pnpm(&mut self, pnpm: Option<Version>) -> Fallible<()> {\n        if let Some(platform) = self.platform.as_mut() {\n            if platform.pnpm != pnpm {\n                platform.pnpm = pnpm;\n                self.save()?;\n            }\n        } else if pnpm.is_some() {\n            return Err(ErrorKind::NoDefaultNodeVersion {\n                tool: \"pnpm\".into(),\n            }\n            .into());\n        }\n\n        Ok(())\n    }\n\n    /// Set the active Npm version in the default platform file.\n    pub fn set_active_npm(&mut self, npm: Option<Version>) -> Fallible<()> {\n        if let Some(platform) = self.platform.as_mut() {\n            if platform.npm != npm {\n                platform.npm = npm;\n                self.save()?;\n            }\n        } else if npm.is_some() {\n            return Err(ErrorKind::NoDefaultNodeVersion { tool: \"npm\".into() }.into());\n        }\n\n        Ok(())\n    }\n\n    pub fn save(&self) -> Fallible<()> {\n        let path = volta_home()?.default_platform_file();\n        let result = match &self.platform {\n            Some(platform) => {\n                let src = serial::Platform::of(platform).into_json()?;\n                write(path, src)\n            }\n            None => write(path, \"{}\"),\n        };\n        result.with_context(|| ErrorKind::WritePlatformError {\n            file: path.to_owned(),\n        })\n    }\n}\n"
  },
  {
    "path": "crates/volta-core/src/toolchain/serial.rs",
    "content": "use crate::error::{Context, ErrorKind, Fallible, VoltaError};\nuse crate::platform::PlatformSpec;\nuse crate::version::{option_version_serde, version_serde};\nuse node_semver::Version;\nuse serde::{Deserialize, Serialize};\n\n#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]\npub struct NodeVersion {\n    #[serde(with = \"version_serde\")]\n    pub runtime: Version,\n    #[serde(with = \"option_version_serde\")]\n    pub npm: Option<Version>,\n}\n\n#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]\npub struct Platform {\n    #[serde(default)]\n    pub node: Option<NodeVersion>,\n    #[serde(default)]\n    #[serde(with = \"option_version_serde\")]\n    pub pnpm: Option<Version>,\n    #[serde(default)]\n    #[serde(with = \"option_version_serde\")]\n    pub yarn: Option<Version>,\n}\n\nimpl Platform {\n    pub fn of(source: &PlatformSpec) -> Self {\n        Platform {\n            node: Some(NodeVersion {\n                runtime: source.node.clone(),\n                npm: source.npm.clone(),\n            }),\n            pnpm: source.pnpm.clone(),\n            yarn: source.yarn.clone(),\n        }\n    }\n\n    /// Serialize the Platform to a JSON String\n    pub fn into_json(self) -> Fallible<String> {\n        serde_json::to_string_pretty(&self).with_context(|| ErrorKind::StringifyPlatformError)\n    }\n}\n\nimpl TryFrom<String> for Platform {\n    type Error = VoltaError;\n    fn try_from(src: String) -> Fallible<Self> {\n        let result = if src.is_empty() {\n            serde_json::de::from_str(\"{}\")\n        } else {\n            serde_json::de::from_str(&src)\n        };\n\n        result.with_context(|| ErrorKind::ParsePlatformError)\n    }\n}\n\nimpl From<Platform> for Option<PlatformSpec> {\n    fn from(platform: Platform) -> Option<PlatformSpec> {\n        let yarn = platform.yarn;\n        let pnpm = platform.pnpm;\n        platform.node.map(|node_version| PlatformSpec {\n            node: node_version.runtime,\n            npm: node_version.npm,\n            pnpm,\n            yarn,\n        })\n    }\n}\n\n#[cfg(test)]\npub mod tests {\n\n    use super::*;\n    use crate::platform;\n    use node_semver::Version;\n\n    // NOTE: serde_json is required with the \"preserve_order\" feature in Cargo.toml,\n    // so these tests will serialized/deserialize in a predictable order\n\n    const BASIC_JSON_STR: &str = r#\"{\n  \"node\": {\n    \"runtime\": \"4.5.6\",\n    \"npm\": \"7.8.9\"\n  },\n  \"pnpm\": \"3.2.1\",\n  \"yarn\": \"1.2.3\"\n}\"#;\n\n    #[test]\n    fn test_from_json() {\n        let json_str = BASIC_JSON_STR.to_string();\n        let platform = Platform::try_from(json_str).expect(\"could not parse JSON string\");\n        let expected_platform = Platform {\n            pnpm: Some(Version::parse(\"3.2.1\").expect(\"could not parse version\")),\n            yarn: Some(Version::parse(\"1.2.3\").expect(\"could not parse version\")),\n            node: Some(NodeVersion {\n                runtime: Version::parse(\"4.5.6\").expect(\"could not parse version\"),\n                npm: Some(Version::parse(\"7.8.9\").expect(\"could not parse version\")),\n            }),\n        };\n        assert_eq!(platform, expected_platform);\n    }\n\n    #[test]\n    fn test_from_json_empty_string() {\n        let json_str = \"\".to_string();\n        let platform = Platform::try_from(json_str).expect(\"could not parse JSON string\");\n        let expected_platform = Platform {\n            node: None,\n            pnpm: None,\n            yarn: None,\n        };\n        assert_eq!(platform, expected_platform);\n    }\n\n    #[test]\n    fn test_into_json() {\n        let platform_spec = platform::PlatformSpec {\n            pnpm: Some(Version::parse(\"3.2.1\").expect(\"could not parse version\")),\n            yarn: Some(Version::parse(\"1.2.3\").expect(\"could not parse version\")),\n            node: Version::parse(\"4.5.6\").expect(\"could not parse version\"),\n            npm: Some(Version::parse(\"7.8.9\").expect(\"could not parse version\")),\n        };\n        let json_str = Platform::of(&platform_spec)\n            .into_json()\n            .expect(\"could not serialize platform to JSON\");\n        let expected_json_str = BASIC_JSON_STR.to_string();\n        assert_eq!(json_str, expected_json_str);\n    }\n}\n"
  },
  {
    "path": "crates/volta-core/src/version/mod.rs",
    "content": "use std::fmt;\nuse std::str::FromStr;\n\nuse crate::error::{Context, ErrorKind, Fallible, VoltaError};\nuse node_semver::{Range, Version};\n\nmod serial;\n\n#[derive(Debug, Default)]\n#[cfg_attr(test, derive(PartialEq, Eq))]\npub enum VersionSpec {\n    /// No version specified (default)\n    #[default]\n    None,\n\n    /// SemVer Range\n    Semver(Range),\n\n    /// Exact Version\n    Exact(Version),\n\n    /// Arbitrary Version Tag\n    Tag(VersionTag),\n}\n\n#[derive(Debug)]\n#[cfg_attr(test, derive(PartialEq, Eq))]\npub enum VersionTag {\n    /// The 'latest' tag, a special case that exists for all packages\n    Latest,\n\n    /// The 'lts' tag, a special case for Node\n    Lts,\n\n    /// An arbitrary tag version\n    Custom(String),\n}\n\nimpl fmt::Display for VersionSpec {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match self {\n            VersionSpec::None => write!(f, \"<default>\"),\n            VersionSpec::Semver(req) => req.fmt(f),\n            VersionSpec::Exact(version) => version.fmt(f),\n            VersionSpec::Tag(tag) => tag.fmt(f),\n        }\n    }\n}\n\nimpl fmt::Display for VersionTag {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match self {\n            VersionTag::Latest => write!(f, \"latest\"),\n            VersionTag::Lts => write!(f, \"lts\"),\n            VersionTag::Custom(s) => s.fmt(f),\n        }\n    }\n}\n\nimpl FromStr for VersionSpec {\n    type Err = VoltaError;\n\n    fn from_str(s: &str) -> Fallible<Self> {\n        if let Ok(version) = parse_version(s) {\n            Ok(VersionSpec::Exact(version))\n        } else if let Ok(req) = parse_requirements(s) {\n            Ok(VersionSpec::Semver(req))\n        } else {\n            s.parse().map(VersionSpec::Tag)\n        }\n    }\n}\n\nimpl FromStr for VersionTag {\n    type Err = VoltaError;\n\n    fn from_str(s: &str) -> Fallible<Self> {\n        if s == \"latest\" {\n            Ok(VersionTag::Latest)\n        } else if s == \"lts\" {\n            Ok(VersionTag::Lts)\n        } else {\n            Ok(VersionTag::Custom(s.into()))\n        }\n    }\n}\n\npub fn parse_requirements(s: impl AsRef<str>) -> Fallible<Range> {\n    let s = s.as_ref();\n    serial::parse_requirements(s)\n        .with_context(|| ErrorKind::VersionParseError { version: s.into() })\n}\n\npub fn parse_version(s: impl AsRef<str>) -> Fallible<Version> {\n    let s = s.as_ref();\n    s.parse()\n        .with_context(|| ErrorKind::VersionParseError { version: s.into() })\n}\n\n// remove the leading 'v' from the version string, if present\nfn trim_version(s: &str) -> &str {\n    let s = s.trim();\n    match s.strip_prefix('v') {\n        Some(stripped) => stripped,\n        None => s,\n    }\n}\n\n// custom serialization and de-serialization for Version\n// because Version doesn't work with serde out of the box\npub mod version_serde {\n    use node_semver::Version;\n    use serde::de::{Error, Visitor};\n    use serde::{self, Deserializer, Serializer};\n    use std::fmt;\n\n    struct VersionVisitor;\n\n    impl Visitor<'_> for VersionVisitor {\n        type Value = Version;\n\n        fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {\n            formatter.write_str(\"string\")\n        }\n\n        // parse the version from the string\n        fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>\n        where\n            E: Error,\n        {\n            Version::parse(super::trim_version(value)).map_err(Error::custom)\n        }\n    }\n\n    pub fn serialize<S>(version: &Version, s: S) -> Result<S::Ok, S::Error>\n    where\n        S: Serializer,\n    {\n        s.serialize_str(&version.to_string())\n    }\n\n    pub fn deserialize<'de, D>(deserializer: D) -> Result<Version, D::Error>\n    where\n        D: Deserializer<'de>,\n    {\n        deserializer.deserialize_string(VersionVisitor)\n    }\n}\n\n// custom serialization and de-serialization for Option<Version>\n// because Version doesn't work with serde out of the box\npub mod option_version_serde {\n    use node_semver::Version;\n    use serde::de::Error;\n    use serde::{self, Deserialize, Deserializer, Serializer};\n\n    pub fn serialize<S>(version: &Option<Version>, s: S) -> Result<S::Ok, S::Error>\n    where\n        S: Serializer,\n    {\n        match version {\n            Some(v) => s.serialize_str(&v.to_string()),\n            None => s.serialize_none(),\n        }\n    }\n\n    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Version>, D::Error>\n    where\n        D: Deserializer<'de>,\n    {\n        let s: Option<String> = Option::deserialize(deserializer)?;\n        if let Some(v) = s {\n            return Ok(Some(\n                Version::parse(super::trim_version(&v)).map_err(Error::custom)?,\n            ));\n        }\n        Ok(None)\n    }\n}\n\n// custom deserialization for HashMap<String, Version>\n// because Version doesn't work with serde out of the box\npub mod hashmap_version_serde {\n    use super::version_serde;\n    use node_semver::Version;\n    use serde::{self, Deserialize, Deserializer};\n    use std::collections::HashMap;\n\n    #[derive(Deserialize)]\n    struct Wrapper(#[serde(deserialize_with = \"version_serde::deserialize\")] Version);\n\n    pub fn deserialize<'de, D>(deserializer: D) -> Result<HashMap<String, Version>, D::Error>\n    where\n        D: Deserializer<'de>,\n    {\n        let m = HashMap::<String, Wrapper>::deserialize(deserializer)?;\n        Ok(m.into_iter().map(|(k, Wrapper(v))| (k, v)).collect())\n    }\n}\n"
  },
  {
    "path": "crates/volta-core/src/version/serial.rs",
    "content": "use node_semver::{Range, SemverError};\n\n// NOTE: using `parse_compat` here because the semver crate defaults to\n// parsing in a cargo-compatible way. This is normally fine, except for\n// 2 cases (that I know about):\n//  * \"1.2.3\" parses as `^1.2.3` for cargo, but `=1.2.3` for Node\n//  * `>1.2.3 <2.0.0` serializes to \">1.2.3, <2.0.0\" for cargo (with the\n//    comma), but \">1.2.3 <2.0.0\" for Node (no comma, because Node parses\n//    commas differently)\n//\n// Because we are parsing the version requirements from the command line,\n// then serializing them to pass to `npm view`, they need to be handled in\n// a Node-compatible way (or we get the wrong version info returned).\npub fn parse_requirements(src: &str) -> Result<Range, SemverError> {\n    let src = src.trim().trim_start_matches('v');\n\n    Range::parse(src)\n}\n\n#[cfg(test)]\npub mod tests {\n\n    use crate::version::serial::parse_requirements;\n    use node_semver::Range;\n\n    #[test]\n    fn test_parse_requirements() {\n        assert_eq!(\n            parse_requirements(\"1.2.3\").unwrap(),\n            Range::parse(\"=1.2.3\").unwrap()\n        );\n        assert_eq!(\n            parse_requirements(\"v1.5\").unwrap(),\n            Range::parse(\"=1.5\").unwrap()\n        );\n        assert_eq!(\n            parse_requirements(\"=1.2.3\").unwrap(),\n            Range::parse(\"=1.2.3\").unwrap()\n        );\n        assert_eq!(\n            parse_requirements(\"^1.2\").unwrap(),\n            Range::parse(\"^1.2\").unwrap()\n        );\n        assert_eq!(\n            parse_requirements(\">=1.4\").unwrap(),\n            Range::parse(\">=1.4\").unwrap()\n        );\n        assert_eq!(\n            parse_requirements(\"8.11 - 8.17 || 10.* || >= 12\").unwrap(),\n            Range::parse(\"8.11 - 8.17 || 10.* || >= 12\").unwrap()\n        );\n    }\n}\n"
  },
  {
    "path": "crates/volta-layout/Cargo.toml",
    "content": "[package]\nname = \"volta-layout\"\nversion = \"0.1.1\"\nauthors = [\"Chuck Pierce <cpierce.grad@gmail.com>\"]\nedition = \"2021\"\n\n[dependencies]\nvolta-layout-macro = { path = \"../volta-layout-macro\" }\n"
  },
  {
    "path": "crates/volta-layout/src/lib.rs",
    "content": "#[macro_use]\nmod macros;\n\npub mod v0;\npub mod v1;\npub mod v2;\npub mod v3;\npub mod v4;\n\nfn executable(name: &str) -> String {\n    format!(\"{}{}\", name, std::env::consts::EXE_SUFFIX)\n}\n"
  },
  {
    "path": "crates/volta-layout/src/macros.rs",
    "content": "macro_rules! path_buf {\n    ($base:expr, $( $x:expr ), *) => {\n        {\n            let mut temp = $base;\n            $(\n                temp.push($x);\n            )*\n            temp\n        }\n    }\n}\n"
  },
  {
    "path": "crates/volta-layout/src/v0.rs",
    "content": "use std::path::PathBuf;\n\nuse super::executable;\nuse volta_layout_macro::layout;\n\nlayout! {\n    pub struct VoltaInstall {\n        \"shim[.exe]\": shim_executable;\n    }\n\n    pub struct VoltaHome {\n        \"cache\": cache_dir {\n            \"node\": node_cache_dir {\n                \"index.json\": node_index_file;\n                \"index.json.expires\": node_index_expiry_file;\n            }\n        }\n        \"bin\": shim_dir {}\n        \"log\": log_dir {}\n        \"tools\": tools_dir {\n            \"inventory\": inventory_dir {\n                \"node\": node_inventory_dir {}\n                \"packages\": package_inventory_dir {}\n                \"yarn\": yarn_inventory_dir {}\n            }\n            \"image\": image_dir {\n                \"node\": node_image_root_dir {}\n                \"yarn\": yarn_image_root_dir {}\n                \"packages\": package_image_root_dir {}\n            }\n            \"user\": default_toolchain_dir {\n                \"bins\": default_bin_dir {}\n                \"packages\": default_package_dir {}\n                \"platform.json\": default_platform_file;\n            }\n        }\n        \"tmp\": tmp_dir {}\n        \"hooks.json\": default_hooks_file;\n    }\n}\n\nimpl VoltaHome {\n    pub fn package_distro_file(&self, name: &str, version: &str) -> PathBuf {\n        path_buf!(\n            self.package_inventory_dir.clone(),\n            format!(\"{}-{}.tgz\", name, version)\n        )\n    }\n\n    pub fn package_distro_shasum(&self, name: &str, version: &str) -> PathBuf {\n        path_buf!(\n            self.package_inventory_dir.clone(),\n            format!(\"{}-{}.shasum\", name, version)\n        )\n    }\n\n    pub fn node_image_dir(&self, node: &str, npm: &str) -> PathBuf {\n        path_buf!(self.node_image_root_dir.clone(), node, npm)\n    }\n\n    pub fn yarn_image_dir(&self, version: &str) -> PathBuf {\n        path_buf!(self.yarn_image_root_dir.clone(), version)\n    }\n\n    pub fn yarn_image_bin_dir(&self, version: &str) -> PathBuf {\n        path_buf!(self.yarn_image_dir(version), \"bin\")\n    }\n\n    pub fn package_image_dir(&self, name: &str, version: &str) -> PathBuf {\n        path_buf!(self.package_image_root_dir.clone(), name, version)\n    }\n\n    pub fn default_package_config_file(&self, package_name: &str) -> PathBuf {\n        path_buf!(\n            self.default_package_dir.clone(),\n            format!(\"{}.json\", package_name)\n        )\n    }\n\n    pub fn default_tool_bin_config(&self, bin_name: &str) -> PathBuf {\n        path_buf!(self.default_bin_dir.clone(), format!(\"{}.json\", bin_name))\n    }\n\n    pub fn node_npm_version_file(&self, version: &str) -> PathBuf {\n        path_buf!(\n            self.node_inventory_dir.clone(),\n            format!(\"node-v{}-npm\", version)\n        )\n    }\n\n    pub fn shim_file(&self, toolname: &str) -> PathBuf {\n        path_buf!(self.shim_dir.clone(), executable(toolname))\n    }\n}\n\n#[cfg(windows)]\nimpl VoltaHome {\n    pub fn shim_git_bash_script_file(&self, toolname: &str) -> PathBuf {\n        path_buf!(self.shim_dir.clone(), toolname)\n    }\n\n    pub fn node_image_bin_dir(&self, node: &str, npm: &str) -> PathBuf {\n        self.node_image_dir(node, npm)\n    }\n}\n\n#[cfg(windows)]\nimpl VoltaInstall {\n    pub fn bin_dir(&self) -> PathBuf {\n        path_buf!(self.root.clone(), \"bin\")\n    }\n}\n\n#[cfg(unix)]\nimpl VoltaHome {\n    pub fn node_image_bin_dir(&self, node: &str, npm: &str) -> PathBuf {\n        path_buf!(self.node_image_dir(node, npm), \"bin\")\n    }\n}\n"
  },
  {
    "path": "crates/volta-layout/src/v1.rs",
    "content": "use std::path::PathBuf;\n\nuse super::executable;\nuse volta_layout_macro::layout;\n\nlayout! {\n    pub struct VoltaInstall {\n        \"volta-shim[.exe]\": shim_executable;\n        \"volta[.exe]\": main_executable;\n        \"volta-migrate[.exe]\": migrate_executable;\n    }\n\n    pub struct VoltaHome {\n        \"cache\": cache_dir {\n            \"node\": node_cache_dir {\n                \"index.json\": node_index_file;\n                \"index.json.expires\": node_index_expiry_file;\n            }\n        }\n        \"bin\": shim_dir {}\n        \"log\": log_dir {}\n        \"tools\": tools_dir {\n            \"inventory\": inventory_dir {\n                \"node\": node_inventory_dir {}\n                \"packages\": package_inventory_dir {}\n                \"yarn\": yarn_inventory_dir {}\n            }\n            \"image\": image_dir {\n                \"node\": node_image_root_dir {}\n                \"yarn\": yarn_image_root_dir {}\n                \"packages\": package_image_root_dir {}\n            }\n            \"user\": default_toolchain_dir {\n                \"bins\": default_bin_dir {}\n                \"packages\": default_package_dir {}\n                \"platform.json\": default_platform_file;\n            }\n        }\n        \"tmp\": tmp_dir {}\n        \"hooks.json\": default_hooks_file;\n        \"layout.v1\": layout_file;\n    }\n}\n\nimpl VoltaHome {\n    pub fn package_distro_file(&self, name: &str, version: &str) -> PathBuf {\n        path_buf!(\n            self.package_inventory_dir.clone(),\n            format!(\"{}-{}.tgz\", name, version)\n        )\n    }\n\n    pub fn package_distro_shasum(&self, name: &str, version: &str) -> PathBuf {\n        path_buf!(\n            self.package_inventory_dir.clone(),\n            format!(\"{}-{}.shasum\", name, version)\n        )\n    }\n\n    pub fn node_image_dir(&self, node: &str, npm: &str) -> PathBuf {\n        path_buf!(self.node_image_root_dir.clone(), node, npm)\n    }\n\n    pub fn yarn_image_dir(&self, version: &str) -> PathBuf {\n        path_buf!(self.yarn_image_root_dir.clone(), version)\n    }\n\n    pub fn yarn_image_bin_dir(&self, version: &str) -> PathBuf {\n        path_buf!(self.yarn_image_dir(version), \"bin\")\n    }\n\n    pub fn package_image_dir(&self, name: &str, version: &str) -> PathBuf {\n        path_buf!(self.package_image_root_dir.clone(), name, version)\n    }\n\n    pub fn default_package_config_file(&self, package_name: &str) -> PathBuf {\n        path_buf!(\n            self.default_package_dir.clone(),\n            format!(\"{}.json\", package_name)\n        )\n    }\n\n    pub fn default_tool_bin_config(&self, bin_name: &str) -> PathBuf {\n        path_buf!(self.default_bin_dir.clone(), format!(\"{}.json\", bin_name))\n    }\n\n    pub fn node_npm_version_file(&self, version: &str) -> PathBuf {\n        path_buf!(\n            self.node_inventory_dir.clone(),\n            format!(\"node-v{}-npm\", version)\n        )\n    }\n\n    pub fn shim_file(&self, toolname: &str) -> PathBuf {\n        path_buf!(self.shim_dir.clone(), executable(toolname))\n    }\n}\n\n#[cfg(windows)]\nimpl VoltaHome {\n    pub fn shim_git_bash_script_file(&self, toolname: &str) -> PathBuf {\n        path_buf!(self.shim_dir.clone(), toolname)\n    }\n\n    pub fn node_image_bin_dir(&self, node: &str, npm: &str) -> PathBuf {\n        self.node_image_dir(node, npm)\n    }\n}\n\n#[cfg(unix)]\nimpl VoltaHome {\n    pub fn node_image_bin_dir(&self, node: &str, npm: &str) -> PathBuf {\n        path_buf!(self.node_image_dir(node, npm), \"bin\")\n    }\n}\n"
  },
  {
    "path": "crates/volta-layout/src/v2.rs",
    "content": "use std::path::PathBuf;\n\nuse super::executable;\nuse volta_layout_macro::layout;\n\npub use crate::v1::VoltaInstall;\n\nlayout! {\n    pub struct VoltaHome {\n        \"cache\": cache_dir {\n            \"node\": node_cache_dir {\n                \"index.json\": node_index_file;\n                \"index.json.expires\": node_index_expiry_file;\n            }\n        }\n        \"bin\": shim_dir {}\n        \"log\": log_dir {}\n        \"tools\": tools_dir {\n            \"inventory\": inventory_dir {\n                \"node\": node_inventory_dir {}\n                \"npm\": npm_inventory_dir {}\n                \"packages\": package_inventory_dir {}\n                \"yarn\": yarn_inventory_dir {}\n            }\n            \"image\": image_dir {\n                \"node\": node_image_root_dir {}\n                \"npm\": npm_image_root_dir {}\n                \"yarn\": yarn_image_root_dir {}\n                \"packages\": package_image_root_dir {}\n            }\n            \"user\": default_toolchain_dir {\n                \"bins\": default_bin_dir {}\n                \"packages\": default_package_dir {}\n                \"platform.json\": default_platform_file;\n            }\n        }\n        \"tmp\": tmp_dir {}\n        \"hooks.json\": default_hooks_file;\n        \"layout.v2\": layout_file;\n    }\n}\n\nimpl VoltaHome {\n    pub fn package_distro_file(&self, name: &str, version: &str) -> PathBuf {\n        path_buf!(\n            self.package_inventory_dir.clone(),\n            format!(\"{}-{}.tgz\", name, version)\n        )\n    }\n\n    pub fn package_distro_shasum(&self, name: &str, version: &str) -> PathBuf {\n        path_buf!(\n            self.package_inventory_dir.clone(),\n            format!(\"{}-{}.shasum\", name, version)\n        )\n    }\n\n    pub fn node_image_dir(&self, node: &str) -> PathBuf {\n        path_buf!(self.node_image_root_dir.clone(), node)\n    }\n\n    pub fn npm_image_dir(&self, npm: &str) -> PathBuf {\n        path_buf!(self.npm_image_root_dir.clone(), npm)\n    }\n\n    pub fn npm_image_bin_dir(&self, npm: &str) -> PathBuf {\n        path_buf!(self.npm_image_dir(npm), \"bin\")\n    }\n\n    pub fn yarn_image_dir(&self, version: &str) -> PathBuf {\n        path_buf!(self.yarn_image_root_dir.clone(), version)\n    }\n\n    pub fn yarn_image_bin_dir(&self, version: &str) -> PathBuf {\n        path_buf!(self.yarn_image_dir(version), \"bin\")\n    }\n\n    pub fn package_image_dir(&self, name: &str, version: &str) -> PathBuf {\n        path_buf!(self.package_image_root_dir.clone(), name, version)\n    }\n\n    pub fn default_package_config_file(&self, package_name: &str) -> PathBuf {\n        path_buf!(\n            self.default_package_dir.clone(),\n            format!(\"{}.json\", package_name)\n        )\n    }\n\n    pub fn default_tool_bin_config(&self, bin_name: &str) -> PathBuf {\n        path_buf!(self.default_bin_dir.clone(), format!(\"{}.json\", bin_name))\n    }\n\n    pub fn node_npm_version_file(&self, version: &str) -> PathBuf {\n        path_buf!(\n            self.node_inventory_dir.clone(),\n            format!(\"node-v{}-npm\", version)\n        )\n    }\n\n    pub fn shim_file(&self, toolname: &str) -> PathBuf {\n        path_buf!(self.shim_dir.clone(), executable(toolname))\n    }\n}\n\n#[cfg(windows)]\nimpl VoltaHome {\n    pub fn shim_git_bash_script_file(&self, toolname: &str) -> PathBuf {\n        path_buf!(self.shim_dir.clone(), toolname)\n    }\n\n    pub fn node_image_bin_dir(&self, node: &str) -> PathBuf {\n        self.node_image_dir(node)\n    }\n}\n\n#[cfg(unix)]\nimpl VoltaHome {\n    pub fn node_image_bin_dir(&self, node: &str) -> PathBuf {\n        path_buf!(self.node_image_dir(node), \"bin\")\n    }\n}\n"
  },
  {
    "path": "crates/volta-layout/src/v3.rs",
    "content": "use std::path::PathBuf;\n\nuse super::executable;\nuse volta_layout_macro::layout;\n\npub use crate::v1::VoltaInstall;\n\nlayout! {\n    pub struct VoltaHome {\n        \"cache\": cache_dir {\n            \"node\": node_cache_dir {\n                \"index.json\": node_index_file;\n                \"index.json.expires\": node_index_expiry_file;\n            }\n        }\n        \"bin\": shim_dir {}\n        \"log\": log_dir {}\n        \"tools\": tools_dir {\n            \"inventory\": inventory_dir {\n                \"node\": node_inventory_dir {}\n                \"npm\": npm_inventory_dir {}\n                \"pnpm\": pnpm_inventory_dir {}\n                \"yarn\": yarn_inventory_dir {}\n            }\n            \"image\": image_dir {\n                \"node\": node_image_root_dir {}\n                \"npm\": npm_image_root_dir {}\n                \"pnpm\": pnpm_image_root_dir {}\n                \"yarn\": yarn_image_root_dir {}\n                \"packages\": package_image_root_dir {}\n            }\n            \"shared\": shared_lib_root {}\n            \"user\": default_toolchain_dir {\n                \"bins\": default_bin_dir {}\n                \"packages\": default_package_dir {}\n                \"platform.json\": default_platform_file;\n            }\n        }\n        \"tmp\": tmp_dir {}\n        \"hooks.json\": default_hooks_file;\n        \"layout.v3\": layout_file;\n    }\n}\n\nimpl VoltaHome {\n    pub fn node_image_dir(&self, node: &str) -> PathBuf {\n        path_buf!(self.node_image_root_dir.clone(), node)\n    }\n\n    pub fn npm_image_dir(&self, npm: &str) -> PathBuf {\n        path_buf!(self.npm_image_root_dir.clone(), npm)\n    }\n\n    pub fn npm_image_bin_dir(&self, npm: &str) -> PathBuf {\n        path_buf!(self.npm_image_dir(npm), \"bin\")\n    }\n\n    pub fn pnpm_image_dir(&self, version: &str) -> PathBuf {\n        path_buf!(self.pnpm_image_root_dir.clone(), version)\n    }\n\n    pub fn pnpm_image_bin_dir(&self, version: &str) -> PathBuf {\n        path_buf!(self.pnpm_image_dir(version), \"bin\")\n    }\n\n    pub fn yarn_image_dir(&self, version: &str) -> PathBuf {\n        path_buf!(self.yarn_image_root_dir.clone(), version)\n    }\n\n    pub fn yarn_image_bin_dir(&self, version: &str) -> PathBuf {\n        path_buf!(self.yarn_image_dir(version), \"bin\")\n    }\n\n    pub fn package_image_dir(&self, name: &str) -> PathBuf {\n        path_buf!(self.package_image_root_dir.clone(), name)\n    }\n\n    pub fn default_package_config_file(&self, package_name: &str) -> PathBuf {\n        path_buf!(\n            self.default_package_dir.clone(),\n            format!(\"{}.json\", package_name)\n        )\n    }\n\n    pub fn default_tool_bin_config(&self, bin_name: &str) -> PathBuf {\n        path_buf!(self.default_bin_dir.clone(), format!(\"{}.json\", bin_name))\n    }\n\n    pub fn node_npm_version_file(&self, version: &str) -> PathBuf {\n        path_buf!(\n            self.node_inventory_dir.clone(),\n            format!(\"node-v{}-npm\", version)\n        )\n    }\n\n    pub fn shim_file(&self, toolname: &str) -> PathBuf {\n        path_buf!(self.shim_dir.clone(), executable(toolname))\n    }\n\n    pub fn shared_lib_dir(&self, library: &str) -> PathBuf {\n        path_buf!(self.shared_lib_root.clone(), library)\n    }\n}\n\n#[cfg(windows)]\nimpl VoltaHome {\n    pub fn shim_git_bash_script_file(&self, toolname: &str) -> PathBuf {\n        path_buf!(self.shim_dir.clone(), toolname)\n    }\n\n    pub fn node_image_bin_dir(&self, node: &str) -> PathBuf {\n        self.node_image_dir(node)\n    }\n}\n\n#[cfg(unix)]\nimpl VoltaHome {\n    pub fn node_image_bin_dir(&self, node: &str) -> PathBuf {\n        path_buf!(self.node_image_dir(node), \"bin\")\n    }\n}\n"
  },
  {
    "path": "crates/volta-layout/src/v4.rs",
    "content": "use std::path::PathBuf;\n\nuse volta_layout_macro::layout;\n\npub use crate::v1::VoltaInstall;\n\nlayout! {\n    pub struct VoltaHome {\n        \"cache\": cache_dir {\n            \"node\": node_cache_dir {\n                \"index.json\": node_index_file;\n                \"index.json.expires\": node_index_expiry_file;\n            }\n        }\n        \"bin\": shim_dir {}\n        \"log\": log_dir {}\n        \"tools\": tools_dir {\n            \"inventory\": inventory_dir {\n                \"node\": node_inventory_dir {}\n                \"npm\": npm_inventory_dir {}\n                \"pnpm\": pnpm_inventory_dir {}\n                \"yarn\": yarn_inventory_dir {}\n            }\n            \"image\": image_dir {\n                \"node\": node_image_root_dir {}\n                \"npm\": npm_image_root_dir {}\n                \"pnpm\": pnpm_image_root_dir {}\n                \"yarn\": yarn_image_root_dir {}\n                \"packages\": package_image_root_dir {}\n            }\n            \"shared\": shared_lib_root {}\n            \"user\": default_toolchain_dir {\n                \"bins\": default_bin_dir {}\n                \"packages\": default_package_dir {}\n                \"platform.json\": default_platform_file;\n            }\n        }\n        \"tmp\": tmp_dir {}\n        \"hooks.json\": default_hooks_file;\n        \"layout.v4\": layout_file;\n    }\n}\n\nimpl VoltaHome {\n    pub fn node_image_dir(&self, node: &str) -> PathBuf {\n        path_buf!(self.node_image_root_dir.clone(), node)\n    }\n\n    pub fn npm_image_dir(&self, npm: &str) -> PathBuf {\n        path_buf!(self.npm_image_root_dir.clone(), npm)\n    }\n\n    pub fn npm_image_bin_dir(&self, npm: &str) -> PathBuf {\n        path_buf!(self.npm_image_dir(npm), \"bin\")\n    }\n\n    pub fn pnpm_image_dir(&self, version: &str) -> PathBuf {\n        path_buf!(self.pnpm_image_root_dir.clone(), version)\n    }\n\n    pub fn pnpm_image_bin_dir(&self, version: &str) -> PathBuf {\n        path_buf!(self.pnpm_image_dir(version), \"bin\")\n    }\n\n    pub fn yarn_image_dir(&self, version: &str) -> PathBuf {\n        path_buf!(self.yarn_image_root_dir.clone(), version)\n    }\n\n    pub fn yarn_image_bin_dir(&self, version: &str) -> PathBuf {\n        path_buf!(self.yarn_image_dir(version), \"bin\")\n    }\n\n    pub fn package_image_dir(&self, name: &str) -> PathBuf {\n        path_buf!(self.package_image_root_dir.clone(), name)\n    }\n\n    pub fn default_package_config_file(&self, package_name: &str) -> PathBuf {\n        path_buf!(\n            self.default_package_dir.clone(),\n            format!(\"{}.json\", package_name)\n        )\n    }\n\n    pub fn default_tool_bin_config(&self, bin_name: &str) -> PathBuf {\n        path_buf!(self.default_bin_dir.clone(), format!(\"{}.json\", bin_name))\n    }\n\n    pub fn node_npm_version_file(&self, version: &str) -> PathBuf {\n        path_buf!(\n            self.node_inventory_dir.clone(),\n            format!(\"node-v{}-npm\", version)\n        )\n    }\n\n    pub fn shim_file(&self, toolname: &str) -> PathBuf {\n        // On Windows, shims are created as `<name>.cmd` since they\n        // are thin scripts that use `volta run` to execute the command\n        #[cfg(windows)]\n        let toolname = format!(\"{}{}\", toolname, \".cmd\");\n\n        path_buf!(self.shim_dir.clone(), toolname)\n    }\n\n    pub fn shared_lib_dir(&self, library: &str) -> PathBuf {\n        path_buf!(self.shared_lib_root.clone(), library)\n    }\n}\n\n#[cfg(windows)]\nimpl VoltaHome {\n    pub fn shim_git_bash_script_file(&self, toolname: &str) -> PathBuf {\n        path_buf!(self.shim_dir.clone(), toolname)\n    }\n\n    pub fn node_image_bin_dir(&self, node: &str) -> PathBuf {\n        self.node_image_dir(node)\n    }\n}\n\n#[cfg(unix)]\nimpl VoltaHome {\n    pub fn node_image_bin_dir(&self, node: &str) -> PathBuf {\n        path_buf!(self.node_image_dir(node), \"bin\")\n    }\n}\n"
  },
  {
    "path": "crates/volta-layout-macro/Cargo.toml",
    "content": "[package]\nname = \"volta-layout-macro\"\nversion = \"0.1.0\"\nauthors = [\"David Herman <david.herman@gmail.com>\"]\nedition = \"2021\"\n\n[lib]\nproc-macro = true\n\n[dependencies]\nsyn = \"1.0.5\"\nquote = \"1.0.2\"\nproc-macro2 = \"1.0.2\"\n"
  },
  {
    "path": "crates/volta-layout-macro/src/ast.rs",
    "content": "use crate::ir::{Entry, Ir};\nuse proc_macro2::TokenStream;\nuse std::collections::HashMap;\nuse syn::parse::{self, Parse, ParseStream};\nuse syn::punctuated::Punctuated;\nuse syn::{braced, Attribute, Ident, LitStr, Token, Visibility};\n\npub(crate) type Result<T> = ::std::result::Result<T, TokenStream>;\n\n/// Abstract syntax tree (AST) for the surface syntax of the `layout!` macro.\n///\n/// The surface syntax of the `layout!` macro takes the form:\n///\n/// ```text,no_run\n/// Attribute* Visibility \"struct\" Ident Directory\n/// ```\n///\n/// This AST gets lowered by the `flatten` method to a vector of intermediate\n/// representation (IR) trees. See the `Ir` type for details.\npub(crate) struct Ast {\n    decls: Vec<LayoutStruct>,\n}\n\nimpl Parse for Ast {\n    fn parse(input: ParseStream) -> parse::Result<Self> {\n        let mut decls = Vec::new();\n        while !input.is_empty() {\n            let decl = input.call(LayoutStruct::parse)?;\n            decls.push(decl);\n        }\n        Ok(Ast { decls })\n    }\n}\n\nimpl Ast {\n    /// Compiles (macro-expands) the AST.\n    pub(crate) fn compile(self) -> TokenStream {\n        self.decls\n            .into_iter()\n            .map(|decl| match decl.flatten() {\n                Ok(ir) => ir.codegen(),\n                Err(err) => err,\n            })\n            .collect()\n    }\n}\n\n/// Represents a single type LayoutStruct in the AST, which takes the form:\n///\n/// ```text,no_run\n/// Attribute* Visibility \"struct\" Ident Directory\n/// ```\n///\n/// This AST gets lowered by the `flatten` method to a flat list of entries,\n/// organized by entry type. See the `Ir` type for details.\npub(crate) struct LayoutStruct {\n    attrs: Vec<Attribute>,\n    visibility: Visibility,\n    name: Ident,\n    directory: Directory,\n}\n\nimpl Parse for LayoutStruct {\n    fn parse(input: ParseStream) -> parse::Result<Self> {\n        let attrs: Vec<Attribute> = input.call(Attribute::parse_outer)?;\n        let visibility: Visibility = input.parse()?;\n        input.parse::<Token![struct]>()?;\n        let name: Ident = input.parse()?;\n        let directory: Directory = input.parse()?;\n        Ok(LayoutStruct {\n            attrs,\n            visibility,\n            name,\n            directory,\n        })\n    }\n}\n\nimpl LayoutStruct {\n    /// Lowers the AST to a flattened intermediate representation.\n    fn flatten(self) -> Result<Ir> {\n        let mut results = Ir {\n            name: self.name,\n            attrs: self.attrs,\n            visibility: self.visibility,\n            dirs: vec![],\n            files: vec![],\n            exes: vec![],\n        };\n\n        self.directory.flatten(&mut results, vec![])?;\n\n        Ok(results)\n    }\n}\n\n/// Represents a directory entry in the AST, which can recursively contain\n/// more entries.\n///\n/// The surface syntax of a directory takes the form:\n///\n/// ```text,no_run\n/// {\n///     (FieldPrefix)FieldContents*\n/// }\n/// ```\nstruct Directory {\n    entries: Punctuated<FieldPrefix, FieldContents>,\n}\n\nimpl Parse for Directory {\n    fn parse(input: ParseStream) -> parse::Result<Self> {\n        let content;\n        braced!(content in input);\n        Ok(Directory {\n            entries: content.parse_terminated(FieldPrefix::parse)?,\n        })\n    }\n}\n\nenum EntryKind {\n    Exe,\n    File,\n    Dir,\n}\n\nimpl Directory {\n    /// Lowers the directory to a flattened intermediate representation.\n    fn flatten(self, results: &mut Ir, context: Vec<LitStr>) -> Result<()> {\n        let mut visited_entries = HashMap::new();\n\n        for pair in self.entries.into_pairs() {\n            let (prefix, punc) = pair.into_tuple();\n\n            let mut entry = Entry {\n                name: prefix.name,\n                context: context.clone(),\n                filename: prefix.filename.clone(),\n            };\n\n            match punc {\n                Some(FieldContents::Dir(dir)) => {\n                    let filename = prefix.filename.value();\n\n                    if filename.ends_with(\".exe\") || filename.ends_with(\"[.exe]\") {\n                        let error = syn::Error::new(\n                            prefix.filename.span(),\n                            \"the `.exe` extension is not allowed for directory names\",\n                        );\n                        return Err(error.to_compile_error());\n                    }\n\n                    if let Some(kind) = visited_entries.get(&filename) {\n                        let message = match kind {\n                            EntryKind::Exe => {\n                                format!(\"filename `{}` is a duplicate of `{}` executable on non-Windows operating systems\", filename, filename)\n                            }\n                            _ => {\n                                format!(\"duplicate filename `{}`\", filename)\n                            }\n                        };\n                        let error = syn::Error::new(prefix.filename.span(), message);\n                        return Err(error.to_compile_error());\n                    }\n\n                    visited_entries.insert(filename.clone(), EntryKind::Dir);\n\n                    results.dirs.push(entry);\n                    let mut sub_context = context.clone();\n                    sub_context.push(prefix.filename);\n                    dir.flatten(results, sub_context)?;\n                }\n                _ => {\n                    let filename = prefix.filename.value();\n                    if filename.ends_with(\"[.exe]\") {\n                        let filename = &filename[0..filename.len() - 6];\n\n                        if let Some(kind) = visited_entries.get(filename) {\n                            let message = match kind {\n                                EntryKind::Exe => {\n                                    format!(\"duplicate filename `{}.exe`\", filename)\n                                }\n                                EntryKind::File => {\n                                    format!(\"executable `{}` (on non-Windows operating systems) is a duplicate of `{}` filename\", filename, filename)\n                                }\n                                EntryKind::Dir => {\n                                    format!(\"executable `{}` (on non-Windows operating systems) is a duplicate of `{}` directory name\", filename, filename)\n                                }\n                            };\n                            let error = syn::Error::new(prefix.filename.span(), message);\n                            return Err(error.to_compile_error());\n                        }\n\n                        visited_entries.insert(filename.to_string(), EntryKind::Exe);\n                        entry.filename = LitStr::new(filename, prefix.filename.span());\n                        results.exes.push(entry);\n                    } else {\n                        if let Some(kind) = visited_entries.get(&filename) {\n                            let message = match kind {\n                                EntryKind::Exe => {\n                                    format!(\"filename `{}` is a duplicate of `{}` executable on non-Windows operating systems\", filename, filename)\n                                }\n                                _ => {\n                                    format!(\"duplicate filename `{}`\", filename)\n                                }\n                            };\n                            let error = syn::Error::new(prefix.filename.span(), message);\n                            return Err(error.to_compile_error());\n                        }\n\n                        visited_entries.insert(filename, EntryKind::File);\n                        results.files.push(entry);\n                    }\n                }\n            }\n        }\n        Ok(())\n    }\n}\n\n/// AST for the common prefix of a single field in a `layout!` struct declaration,\n/// which is of the form:\n///\n/// ```text,no_run\n/// LitStr \":\" Ident\n/// ```\n///\n/// This is followed either by a semicolon (`;`), indicating that the field is a\n/// file, or a braced directory entry, indicating that the field is a directory.\n///\n/// If the `LitStr` contains the suffix `\"[.exe]\"` it is treated specially as an\n/// executable file, whose suffix (or lack thereof) is determined by the current\n/// operating system (using the `std::env::consts::EXE_SUFFIX` constant).\nstruct FieldPrefix {\n    filename: LitStr,\n    name: Ident,\n}\n\nimpl Parse for FieldPrefix {\n    fn parse(input: ParseStream) -> parse::Result<Self> {\n        let filename = input.parse()?;\n        input.parse::<Token![:]>()?;\n        let name = input.parse()?;\n        Ok(FieldPrefix { filename, name })\n    }\n}\n\n/// AST for the suffix of a field in a `layout!` struct declaration.\nenum FieldContents {\n    /// A file field suffix, which consists of a single semicolon (`;`).\n    File(Token![;]),\n\n    /// A directory field suffix, which consists of a braced directory.\n    Dir(Directory),\n}\n\nimpl Parse for FieldContents {\n    fn parse(input: ParseStream) -> parse::Result<Self> {\n        let lookahead = input.lookahead1();\n        Ok(if lookahead.peek(Token![;]) {\n            let semi = input.parse()?;\n            FieldContents::File(semi)\n        } else {\n            let directory = input.parse()?;\n            FieldContents::Dir(directory)\n        })\n    }\n}\n"
  },
  {
    "path": "crates/volta-layout-macro/src/ir.rs",
    "content": "// The `proc_macro2` crate is a polyfill for advanced functionality of Rust's\n// procedural macros, not all of which have shipped in stable Rust. It's used by\n// the `syn` and `quote` crates to produce a shimmed version of the standard\n// `TokenStream` type. So internally that's the type we have to use for the\n// implementation of our macro. The actual front-end for the macro takes this\n// shimmed `TokenStream` type and converts it to the built-in `TokenStream` type\n// required by the Rust macro system.\nuse proc_macro2::TokenStream;\nuse quote::quote;\nuse syn::{Attribute, Ident, LitStr, Visibility};\n\n// These seem to be leaked implementation details of the `quote` macro that have\n// to be imported by users. You can ignore them; they simply pacify the compiler.\n#[allow(unused_imports)]\nuse quote::{pounded_var_names, quote_each_token, quote_spanned};\n\n/// The intermediate representation (IR) of a struct type defined by the `layout!`\n/// macro, which contains the flattened directory entries, organized into three\n/// categories:\n///\n/// - Directories\n/// - Executable files\n/// - Other files\npub(crate) struct Ir {\n    pub(crate) name: Ident,\n    pub(crate) attrs: Vec<Attribute>,\n    pub(crate) visibility: Visibility,\n    pub(crate) dirs: Vec<Entry>,\n    pub(crate) files: Vec<Entry>,\n    pub(crate) exes: Vec<Entry>,\n}\n\nimpl Ir {\n    fn dir_names(&self) -> impl Iterator<Item = &Ident> {\n        self.dirs.iter().map(|entry| &entry.name)\n    }\n\n    fn file_names(&self) -> impl Iterator<Item = &Ident> {\n        self.files.iter().map(|entry| &entry.name)\n    }\n\n    fn exe_names(&self) -> impl Iterator<Item = &Ident> {\n        self.exes.iter().map(|entry| &entry.name)\n    }\n\n    fn field_names(&self) -> impl Iterator<Item = &Ident> {\n        let dir_names = self.dir_names();\n        let file_names = self.file_names();\n        let exe_names = self.exe_names();\n        dir_names.chain(file_names).chain(exe_names)\n    }\n\n    fn to_struct_decl(&self) -> TokenStream {\n        let name = &self.name;\n\n        let attrs = self.attrs.iter();\n        let visibility = self.visibility.clone();\n\n        let field_names = self.field_names().map(|field_name| {\n            // Use the field name's span for good duplicate-field-name error messages.\n            quote_spanned! {field_name.span()=>\n                #field_name : ::std::path::PathBuf ,\n            }\n        });\n\n        quote! {\n            #(#attrs)* #visibility struct #name {\n                #(#field_names)*\n                root: ::std::path::PathBuf,\n            }\n        }\n    }\n\n    fn to_create_method(&self) -> TokenStream {\n        let name = &self.name;\n        let dir_names = self.dir_names();\n\n        quote! {\n            impl #name {\n                /// Creates all subdirectories in this directory layout.\n                pub fn create(&self) -> ::std::io::Result<()> {\n                    #(::std::fs::create_dir_all(self.#dir_names())?;)*\n                    ::std::result::Result::Ok(())\n                }\n            }\n        }\n    }\n\n    fn to_item_methods(&self) -> TokenStream {\n        let name = &self.name;\n\n        let methods = self.field_names().map(|field_name| {\n            // Markdown-formatted field name for the doc comment.\n            let markdown_field_name = format!(\"`{}`\", field_name);\n            let markdown_field_name = LitStr::new(&markdown_field_name, field_name.span());\n\n            // Use the field name's span for good duplicate-method-name error messages.\n            quote_spanned! {field_name.span()=>\n                #[doc = \"Returns the \"]\n                #[doc = #markdown_field_name]\n                #[doc = \" path.\"]\n                pub fn #field_name(&self) -> &::std::path::Path { &self.#field_name }\n            }\n        });\n\n        quote! {\n            impl #name {\n                #(#methods)*\n\n                 /// Returns the root path for this directory layout.\n                pub fn root(&self) -> &::std::path::Path { &self.root }\n            }\n        }\n    }\n\n    fn to_ctor(&self) -> TokenStream {\n        let name = &self.name;\n        let root = Ident::new(\"root\", self.name.span());\n\n        let dir_names = self.dir_names();\n        let dir_inits = self.dirs.iter().map(|entry| entry.to_normal_init(&root));\n\n        let file_names = self.file_names();\n        let file_inits = self.files.iter().map(|entry| entry.to_normal_init(&root));\n\n        let exe_names = self.exe_names();\n        let exe_inits = self.exes.iter().map(|entry| entry.to_exe_init(&root));\n\n        let all_names = dir_names.chain(file_names).chain(exe_names);\n        let all_inits = dir_inits.chain(file_inits).chain(exe_inits);\n\n        let markdown_struct_name = format!(\"`{}`\", name);\n        let markdown_struct_name = LitStr::new(&markdown_struct_name, name.span());\n\n        quote! {\n            impl #name {\n                #[doc = \"Constructs a new instance of the \"]\n                #[doc = #markdown_struct_name]\n                #[doc = \" layout, rooted at `root`.\"]\n                pub fn new(#root: ::std::path::PathBuf) -> Self {\n                    Self {\n                        #(#all_names: #all_inits),* ,\n                        #root: #root\n                    }\n                }\n            }\n        }\n    }\n\n    pub(crate) fn codegen(&self) -> TokenStream {\n        let struct_decl = self.to_struct_decl();\n        let ctor = self.to_ctor();\n        let item_methods = self.to_item_methods();\n        let create_method = self.to_create_method();\n\n        quote! {\n            #struct_decl\n            #ctor\n            #item_methods\n            #create_method\n        }\n    }\n}\n\npub(crate) struct Entry {\n    pub(crate) name: Ident,\n    pub(crate) context: Vec<LitStr>,\n    pub(crate) filename: LitStr,\n}\n\nimpl Entry {\n    fn to_normal_init(&self, root: &Ident) -> TokenStream {\n        let name = &self.name;\n        let path_items = self.context.iter();\n        let name_replicated = self.context.iter().map(|_| name);\n        let filename = &self.filename;\n\n        quote! {\n            {\n                let mut #name = #root.clone();\n                #(#name_replicated.push(#path_items);)*\n                #name.push(#filename);\n                #name\n            }\n        }\n    }\n\n    fn to_exe_init(&self, root: &Ident) -> TokenStream {\n        let name = &self.name;\n        let path_items = self.context.iter();\n        let name_replicated = self.context.iter().map(|_| name);\n        let filename = &self.filename;\n\n        quote! {\n            {\n                let mut #name = #root.clone();\n                #(#name_replicated.push(#path_items);)*\n                #name.push(::std::format!(\"{}{}\", #filename, ::std::env::consts::EXE_SUFFIX));\n                #name\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "crates/volta-layout-macro/src/lib.rs",
    "content": "#![recursion_limit = \"128\"]\n\nextern crate proc_macro;\n\nmod ast;\nmod ir;\n\nuse crate::ast::Ast;\nuse proc_macro::TokenStream;\nuse syn::parse_macro_input;\n\n/// A macro for defining Volta directory layout hierarchies.\n///\n/// The syntax of `layout!` takes the form:\n///\n/// ```text,no_run\n/// layout! {\n///     LayoutStruct*\n/// }\n/// ```\n///\n/// The syntax of a `LayoutStruct` takes the form:\n///\n/// ```text,no_run\n/// Attribute* Visibility \"struct\" Ident Directory\n/// ```\n///\n/// The syntax of a `Directory` takes the form:\n///\n/// ```text,no_run\n/// {\n///     (FieldPrefix)FieldContents*\n/// }\n/// ```\n///\n/// The syntax of a `FieldPrefix` takes the form:\n///\n/// ```text,no_run\n/// LitStr \":\" Ident\n/// ```\n///\n/// The syntax of a `FieldContents` is either:\n///\n/// ```text,no_run\n/// \";\"\n/// ```\n///\n/// or:\n///\n/// ```text,no_run\n/// Directory\n/// ```\n#[proc_macro]\npub fn layout(input: TokenStream) -> TokenStream {\n    let ast = parse_macro_input!(input as Ast);\n    let expanded = ast.compile();\n    TokenStream::from(expanded)\n}\n"
  },
  {
    "path": "crates/volta-migrate/Cargo.toml",
    "content": "[package]\nname = \"volta-migrate\"\nversion = \"0.1.0\"\nauthors = [\"Charles Pierce <cpierce.grad@gmail.com>\"]\nedition = \"2021\"\n\n[dependencies]\nvolta-core = { path = \"../volta-core\" }\nvolta-layout = { path = \"../volta-layout\" }\nlog = { version = \"0.4\", features = [\"std\"] }\ntempfile = \"3.14.0\"\nnode-semver = \"2\"\nserde_json = { version = \"1.0.135\", features = [\"preserve_order\"] }\nserde = { version = \"1.0.217\", features = [\"derive\"] }\nwalkdir = \"2.5.0\"\n"
  },
  {
    "path": "crates/volta-migrate/src/empty.rs",
    "content": "use std::path::PathBuf;\n\n/// Represents an Empty (or uninitialized) Volta layout, one that has never been used by any prior version\n///\n/// This is the easiest to migrate from, as we simply need to create the current layout within the .volta\n/// directory\npub struct Empty {\n    pub home: PathBuf,\n}\n\nimpl Empty {\n    pub fn new(home: PathBuf) -> Self {\n        Empty { home }\n    }\n}\n"
  },
  {
    "path": "crates/volta-migrate/src/lib.rs",
    "content": "//! Provides types for modeling the current state of the Volta directory and for migrating between versions\n//!\n//! A new layout should be represented by its own struct (as in the existing v0 or v1 modules)\n//! Migrations between types should be represented by `TryFrom` implementations between the layout types\n//! (see v1.rs for examples)\n//!\n//! NOTE: Since the layout file is written once the migration is complete, all migration implementations\n//! need to be aware that they may be partially applied (if something fails in the process) and should be\n//! able to re-start gracefully from an interrupted migration\n\nuse std::path::Path;\n\nmod empty;\nmod v0;\nmod v1;\nmod v2;\nmod v3;\nmod v4;\n\nuse v0::V0;\nuse v1::V1;\nuse v2::V2;\nuse v3::V3;\nuse v4::V4;\n\nuse log::{debug, info};\nuse volta_core::error::Fallible;\nuse volta_core::layout::volta_home;\n#[cfg(unix)]\nuse volta_core::layout::volta_install;\nuse volta_core::shim::regenerate_shims_for_dir;\nuse volta_core::sync::VoltaLock;\n\n/// Represents the state of the Volta directory at every point in the migration process\n///\n/// Migrations should be applied sequentially, migrating from V0 to V1 to ... as needed, cycling\n/// through the possible MigrationState values.\nenum MigrationState {\n    Empty(empty::Empty),\n    V0(Box<V0>),\n    V1(Box<V1>),\n    V2(Box<V2>),\n    V3(Box<V3>),\n    V4(Box<V4>),\n}\n\n/// Macro to simplify the boilerplate associated with detecting a tagged state.\n///\n/// Should be passed a series of tuples, each of which contains (in this order):\n///\n/// * The layout version (module name from `volta_layout` crate, e.g. `v1`)\n/// * The `MigrationState` variant name (e.g. `V1`)\n/// * The migration object itself (e.g. `V1` from the v1 module in _this_ crate)\n///\n/// The tuples should be in reverse chronological order, so that the newest is first, e.g.:\n///\n/// detect_tagged!((v3, V3, V3), (v2, V2, V2), (v1, V1, V1));\nmacro_rules! detect_tagged {\n    ($(($layout:ident, $variant:ident, $migration:ident)),*) => {\n        impl MigrationState {\n            fn detect_tagged_state(home: &::std::path::Path) -> Option<Self> {\n                None\n                $(\n                    .or_else(|| detect::$layout(home))\n                )*\n            }\n        }\n\n        mod detect {\n            $(\n                pub(super) fn $layout(home: &::std::path::Path) -> Option<super::MigrationState> {\n                    let volta_home = volta_layout::$layout::VoltaHome::new(home.to_owned());\n                    if volta_home.layout_file().exists() {\n                        Some(super::MigrationState::$variant(Box::new(super::$migration::new(home.to_owned()))))\n                    } else {\n                        None\n                    }\n                }\n            )*\n        }\n    }\n}\n\ndetect_tagged!((v4, V4, V4), (v3, V3, V3), (v2, V2, V2), (v1, V1, V1));\n\nimpl MigrationState {\n    fn current() -> Fallible<Self> {\n        // First look for a tagged version (V1+). If that can't be found, then go through the triage\n        // for detecting a legacy version\n\n        let home = volta_home()?;\n\n        match MigrationState::detect_tagged_state(home.root()) {\n            Some(state) => Ok(state),\n            None => MigrationState::detect_legacy_state(home.root()),\n        }\n    }\n\n    #[allow(clippy::unnecessary_wraps)] // Needs to be Fallible for Unix\n    fn detect_legacy_state(home: &Path) -> Fallible<Self> {\n        /*\n        Triage for determining the legacy layout version:\n        - Does Volta Home exist?\n            - If yes (Windows) then V0\n            - If yes (Unix) then check if Volta Install is outside shim_dir?\n                - If yes, then V0\n                - If no, then check if $VOLTA_HOME/load.sh exists? If yes then V0\n        - Else Empty\n\n        The extra logic on Unix is necessary because Unix installs can be either inside or outside $VOLTA_HOME\n        If it is inside, then the directory necessarily must exist, so we can't use that as a determination.\n        If it is outside (and for Windows which is always outside), then if $VOLTA_HOME exists, it must be from a\n        previous, V0 installation.\n        */\n\n        let volta_home = home.to_owned();\n\n        if volta_home.exists() {\n            #[cfg(windows)]\n            return Ok(MigrationState::V0(Box::new(V0::new(volta_home))));\n\n            #[cfg(unix)]\n            {\n                let install = volta_install()?;\n                if install.root().starts_with(&volta_home) {\n                    // Installed inside $VOLTA_HOME, so need to look for `load.sh` as a marker\n                    if volta_home.join(\"load.sh\").exists() {\n                        return Ok(MigrationState::V0(Box::new(V0::new(volta_home))));\n                    }\n                } else {\n                    // Installed outside of $VOLTA_HOME, so it must exist from a previous V0 install\n                    return Ok(MigrationState::V0(Box::new(V0::new(volta_home))));\n                }\n            }\n        }\n\n        Ok(MigrationState::Empty(empty::Empty::new(volta_home)))\n    }\n}\n\npub fn run_migration() -> Fallible<()> {\n    // Acquire an exclusive lock on the Volta directory, to ensure that no other migrations are running.\n    // If this fails, however, we still need to run the migration\n    match VoltaLock::acquire() {\n        Ok(_lock) => {\n            // The lock was acquired, so we can be confident that no other migrations are running\n            detect_and_migrate()\n        }\n        Err(_) => {\n            debug!(\"Unable to acquire lock on Volta directory! Running migration anyway.\");\n            detect_and_migrate()\n        }\n    }\n}\n\nfn detect_and_migrate() -> Fallible<()> {\n    info!(\"Updating your Volta directory. This may take a few moments...\");\n    let mut state = MigrationState::current()?;\n\n    // To keep the complexity of writing a new migration from continuously increasing, each new\n    // layout version only needs to implement a migration from 2 states: Empty and the previously\n    // latest version. We then apply the migrations sequentially here: V0 -> V1 -> ... -> VX\n    loop {\n        state = match state {\n            MigrationState::Empty(e) => MigrationState::V3(Box::new(e.try_into()?)),\n            MigrationState::V0(zero) => MigrationState::V1(Box::new((*zero).try_into()?)),\n            MigrationState::V1(one) => MigrationState::V2(Box::new((*one).try_into()?)),\n            MigrationState::V2(two) => MigrationState::V3(Box::new((*two).try_into()?)),\n            MigrationState::V3(three) => MigrationState::V4(Box::new((*three).try_into()?)),\n            MigrationState::V4(_) => {\n                break;\n            }\n        };\n    }\n\n    regenerate_shims_for_dir(volta_home()?.shim_dir())?;\n\n    Ok(())\n}\n"
  },
  {
    "path": "crates/volta-migrate/src/v0.rs",
    "content": "use std::path::PathBuf;\n\nuse volta_layout::v0::VoltaHome;\n\n/// Represents a V0 Volta layout (from before v0.7.0)\n///\n/// This needs some migration work to move up to V1, so we keep a reference to the V0 layout\n/// struct to allow for easy comparison between versions\npub struct V0 {\n    pub home: VoltaHome,\n}\n\nimpl V0 {\n    pub fn new(home: PathBuf) -> Self {\n        V0 {\n            home: VoltaHome::new(home),\n        }\n    }\n}\n"
  },
  {
    "path": "crates/volta-migrate/src/v1.rs",
    "content": "#[cfg(unix)]\nuse std::fs::remove_file;\nuse std::fs::File;\nuse std::path::PathBuf;\n\nuse super::empty::Empty;\nuse super::v0::V0;\nuse log::debug;\nuse volta_core::error::{Context, ErrorKind, Fallible, VoltaError};\n#[cfg(unix)]\nuse volta_core::fs::{read_dir_eager, remove_file_if_exists};\nuse volta_layout::v1;\n\n/// Represents a V1 Volta Layout (used by Volta v0.7.0 - v0.7.2)\n///\n/// Holds a reference to the V1 layout struct to support potential future migrations\npub struct V1 {\n    pub home: v1::VoltaHome,\n}\n\nimpl V1 {\n    pub fn new(home: PathBuf) -> Self {\n        V1 {\n            home: v1::VoltaHome::new(home),\n        }\n    }\n\n    /// Write the layout file to mark migration to V1 as complete\n    ///\n    /// Should only be called once all other migration steps are finished, so that we don't\n    /// accidentally mark an incomplete migration as completed\n    fn complete_migration(home: v1::VoltaHome) -> Fallible<Self> {\n        debug!(\"Writing layout marker file\");\n        File::create(home.layout_file()).with_context(|| ErrorKind::CreateLayoutFileError {\n            file: home.layout_file().to_owned(),\n        })?;\n\n        Ok(V1 { home })\n    }\n}\n\nimpl TryFrom<Empty> for V1 {\n    type Error = VoltaError;\n\n    fn try_from(old: Empty) -> Fallible<V1> {\n        debug!(\"New Volta installation detected, creating fresh layout\");\n\n        let home = v1::VoltaHome::new(old.home);\n        home.create().with_context(|| ErrorKind::CreateDirError {\n            dir: home.root().to_owned(),\n        })?;\n\n        V1::complete_migration(home)\n    }\n}\n\nimpl TryFrom<V0> for V1 {\n    type Error = VoltaError;\n\n    fn try_from(old: V0) -> Fallible<V1> {\n        debug!(\"Existing Volta installation detected, migrating from V0 layout\");\n\n        let new_home = v1::VoltaHome::new(old.home.root().to_owned());\n        new_home\n            .create()\n            .with_context(|| ErrorKind::CreateDirError {\n                dir: new_home.root().to_owned(),\n            })?;\n\n        #[cfg(unix)]\n        {\n            debug!(\"Removing unnecessary 'load.*' files\");\n            let root_contents =\n                read_dir_eager(new_home.root()).with_context(|| ErrorKind::ReadDirError {\n                    dir: new_home.root().to_owned(),\n                })?;\n            for (entry, _) in root_contents {\n                let path = entry.path();\n                if let Some(stem) = path.file_stem() {\n                    if stem == \"load\" && path.is_file() {\n                        remove_file(&path)\n                            .with_context(|| ErrorKind::DeleteFileError { file: path })?;\n                    }\n                }\n            }\n\n            debug!(\"Removing old Volta binaries\");\n\n            let old_volta_bin = new_home.root().join(\"volta\");\n            remove_file_if_exists(old_volta_bin)?;\n\n            let old_shim_bin = new_home.root().join(\"shim\");\n            remove_file_if_exists(old_shim_bin)?;\n        }\n\n        V1::complete_migration(new_home)\n    }\n}\n"
  },
  {
    "path": "crates/volta-migrate/src/v2.rs",
    "content": "use std::fs::{read_to_string, write, File};\nuse std::io;\nuse std::path::{Path, PathBuf};\n\nuse super::empty::Empty;\nuse super::v1::V1;\nuse log::debug;\nuse node_semver::Version;\nuse tempfile::tempdir_in;\nuse volta_core::error::{Context, ErrorKind, Fallible, VoltaError};\nuse volta_core::fs::{read_dir_eager, remove_dir_if_exists, remove_file_if_exists, rename};\nuse volta_core::tool::load_default_npm_version;\nuse volta_core::toolchain::serial::Platform;\nuse volta_core::version::parse_version;\nuse volta_layout::{v1, v2};\n\n/// Represents a V2 Volta Layout (used by Volta v0.7.3 and above)\n///\n/// Holds a reference to the V2 layout struct to support potential future migrations\npub struct V2 {\n    pub home: v2::VoltaHome,\n}\n\nimpl V2 {\n    pub fn new(home: PathBuf) -> Self {\n        V2 {\n            home: v2::VoltaHome::new(home),\n        }\n    }\n\n    /// Write the layout file to mark migration to V2 as complete\n    ///\n    /// Should only be called once all other migration steps are finished, so that we don't\n    /// accidentally mark an incomplete migration as completed\n    fn complete_migration(home: v2::VoltaHome) -> Fallible<Self> {\n        debug!(\"Writing layout marker file\");\n        File::create(home.layout_file()).with_context(|| ErrorKind::CreateLayoutFileError {\n            file: home.layout_file().to_owned(),\n        })?;\n\n        Ok(V2 { home })\n    }\n}\n\nimpl TryFrom<Empty> for V2 {\n    type Error = VoltaError;\n\n    fn try_from(old: Empty) -> Fallible<V2> {\n        debug!(\"New Volta installation detected, creating fresh layout\");\n\n        let home = v2::VoltaHome::new(old.home);\n        home.create().with_context(|| ErrorKind::CreateDirError {\n            dir: home.root().to_owned(),\n        })?;\n\n        V2::complete_migration(home)\n    }\n}\n\nimpl TryFrom<V1> for V2 {\n    type Error = VoltaError;\n\n    fn try_from(old: V1) -> Fallible<V2> {\n        debug!(\"Migrating from V1 layout\");\n\n        let new_home = v2::VoltaHome::new(old.home.root().to_owned());\n        new_home\n            .create()\n            .with_context(|| ErrorKind::CreateDirError {\n                dir: new_home.root().to_owned(),\n            })?;\n\n        // Perform the core of the migration\n        clear_default_npm(old.home.default_platform_file())?;\n        shift_node_images(&old.home, &new_home)?;\n\n        // Complete the migration, writing the V2 layout file\n        let layout = V2::complete_migration(new_home)?;\n\n        // Remove the V1 layout file, since we're now on V2 (do this after writing the V2 so that we know the migration succeeded)\n        let old_layout_file = old.home.layout_file();\n        remove_file_if_exists(old_layout_file)?;\n        Ok(layout)\n    }\n}\n\n/// Clear npm from the default `platform.json` file if it is set to the same value as that bundled with Node\n///\n/// This will ensure that we don't treat the default npm from a prior version of Volta as a \"custom\" npm that\n/// the user explicitly requested\nfn clear_default_npm(platform_file: &Path) -> Fallible<()> {\n    let platform_json = match read_to_string(platform_file) {\n        Ok(json) => json,\n        Err(error) => {\n            if error.kind() == io::ErrorKind::NotFound {\n                return Ok(());\n            } else {\n                return Err(VoltaError::from_source(\n                    error,\n                    ErrorKind::ReadPlatformError {\n                        file: platform_file.to_path_buf(),\n                    },\n                ));\n            }\n        }\n    };\n    let mut existing_platform = Platform::try_from(platform_json)?;\n\n    if let Some(ref mut node_version) = &mut existing_platform.node {\n        if let Some(npm) = &node_version.npm {\n            if let Ok(default_npm) = load_default_npm_version(&node_version.runtime) {\n                if *npm == default_npm {\n                    node_version.npm = None;\n                    write(platform_file, existing_platform.into_json()?).with_context(|| {\n                        ErrorKind::WritePlatformError {\n                            file: platform_file.to_owned(),\n                        }\n                    })?;\n                }\n            }\n        }\n    }\n\n    Ok(())\n}\n\n/// Move all Node images up one directory, removing the default npm version directory\n///\n/// In the V1 layout, we kept all node images in /<node_version>/<npm_version>/, however we will be\n/// storing custom npm versions in a separate image directory, so there is no need to maintain the\n/// bundled npm version in the file structure any more. This also will make it slightly easier to access\n/// the Node image, as we no longer will need to look up the bundled npm version every time.\nfn shift_node_images(old_home: &v1::VoltaHome, new_home: &v2::VoltaHome) -> Fallible<()> {\n    let temp_dir =\n        tempdir_in(new_home.tmp_dir()).with_context(|| ErrorKind::CreateTempDirError {\n            in_dir: new_home.tmp_dir().to_owned(),\n        })?;\n    let node_installs = read_dir_eager(old_home.node_image_root_dir())\n        .with_context(|| ErrorKind::ReadDirError {\n            dir: old_home.node_image_root_dir().to_owned(),\n        })?\n        .filter_map(|(entry, metadata)| {\n            if metadata.is_dir() {\n                parse_version(entry.file_name().to_string_lossy()).ok()\n            } else {\n                None\n            }\n        });\n\n    for node_version in node_installs {\n        remove_npm_version_from_node_image_dir(old_home, new_home, node_version, temp_dir.path())?;\n    }\n\n    Ok(())\n}\n\n/// Move a single node image up a directory, if it currently has the npm version in its path\nfn remove_npm_version_from_node_image_dir(\n    old_home: &v1::VoltaHome,\n    new_home: &v2::VoltaHome,\n    node_version: Version,\n    temp_dir: &Path,\n) -> Fallible<()> {\n    let node_string = node_version.to_string();\n    let npm_version = load_default_npm_version(&node_version)?;\n    let old_install = old_home.node_image_dir(&node_string, &npm_version.to_string());\n\n    if old_install.exists() {\n        let temp_image = temp_dir.join(&node_string);\n        let new_install = new_home.node_image_dir(&node_string);\n        rename(&old_install, &temp_image).with_context(|| ErrorKind::SetupToolImageError {\n            tool: \"Node\".into(),\n            version: node_string.clone(),\n            dir: temp_image.clone(),\n        })?;\n        remove_dir_if_exists(&new_install)?;\n        rename(&temp_image, &new_install).with_context(|| ErrorKind::SetupToolImageError {\n            tool: \"Node\".into(),\n            version: node_string,\n            dir: temp_image,\n        })?;\n    }\n    Ok(())\n}\n"
  },
  {
    "path": "crates/volta-migrate/src/v3/config.rs",
    "content": "use std::fs::File;\nuse std::path::Path;\n\nuse node_semver::Version;\nuse volta_core::platform::PlatformSpec;\nuse volta_core::version::{option_version_serde, version_serde};\n\n#[derive(serde::Deserialize)]\npub struct LegacyPackageConfig {\n    pub name: String,\n    #[serde(with = \"version_serde\")]\n    pub version: Version,\n    pub platform: LegacyPlatform,\n    pub bins: Vec<String>,\n}\n\n#[derive(serde::Deserialize)]\npub struct LegacyPlatform {\n    pub node: NodeVersion,\n    #[serde(with = \"option_version_serde\")]\n    pub yarn: Option<Version>,\n}\n\n#[derive(serde::Deserialize)]\npub struct NodeVersion {\n    #[serde(with = \"version_serde\")]\n    pub runtime: Version,\n    #[serde(with = \"option_version_serde\")]\n    pub npm: Option<Version>,\n}\n\nimpl LegacyPackageConfig {\n    pub fn from_file(config_file: &Path) -> Option<Self> {\n        let file = File::open(config_file).ok()?;\n\n        serde_json::from_reader(file).ok()\n    }\n}\n\nimpl From<LegacyPlatform> for PlatformSpec {\n    fn from(config_platform: LegacyPlatform) -> Self {\n        PlatformSpec {\n            node: config_platform.node.runtime,\n            npm: config_platform.node.npm,\n            // LegacyPlatform (layout.v2) doesn't have a pnpm field\n            pnpm: None,\n            yarn: config_platform.yarn,\n        }\n    }\n}\n"
  },
  {
    "path": "crates/volta-migrate/src/v3.rs",
    "content": "use std::fs::File;\nuse std::path::{Path, PathBuf};\n\nuse crate::empty::Empty;\nuse crate::v2::V2;\nuse log::{debug, warn};\nuse volta_core::error::{Context, ErrorKind, Fallible, VoltaError};\nuse volta_core::fs::{remove_dir_if_exists, remove_file_if_exists};\nuse volta_core::platform::PlatformSpec;\nuse volta_core::session::Session;\nuse volta_core::tool::{Package, PackageConfig};\nuse volta_core::version::VersionSpec;\nuse volta_layout::{v2, v3};\nuse walkdir::WalkDir;\n\nmod config;\n\nuse config::LegacyPackageConfig;\n\n/// Represents a V3 Volta layout (used by Volta v0.9.0 and above)\n///\n/// Holds a reference to the V3 layout struct to support future migrations\npub struct V3 {\n    pub home: v3::VoltaHome,\n}\n\nimpl V3 {\n    pub fn new(home: PathBuf) -> Self {\n        V3 {\n            home: v3::VoltaHome::new(home),\n        }\n    }\n\n    /// Write the layout file to mark migration to V2 as complete\n    ///\n    /// Should only be called once all other migration steps are finished, so that we don't\n    /// accidentally mark an incomplete migration as completed\n    fn complete_migration(home: v3::VoltaHome) -> Fallible<Self> {\n        debug!(\"Writing layout marker file\");\n        File::create(home.layout_file()).with_context(|| ErrorKind::CreateLayoutFileError {\n            file: home.layout_file().to_owned(),\n        })?;\n\n        Ok(V3 { home })\n    }\n}\n\nimpl TryFrom<Empty> for V3 {\n    type Error = VoltaError;\n\n    fn try_from(old: Empty) -> Fallible<Self> {\n        debug!(\"New Volta installation detected, creating fresh layout\");\n\n        let home = v3::VoltaHome::new(old.home);\n        home.create().with_context(|| ErrorKind::CreateDirError {\n            dir: home.root().to_owned(),\n        })?;\n\n        V3::complete_migration(home)\n    }\n}\n\nimpl TryFrom<V2> for V3 {\n    type Error = VoltaError;\n\n    fn try_from(old: V2) -> Fallible<Self> {\n        debug!(\"Migrating from V2 layout\");\n\n        let new_home = v3::VoltaHome::new(old.home.root().to_owned());\n        new_home\n            .create()\n            .with_context(|| ErrorKind::CreateDirError {\n                dir: new_home.root().to_owned(),\n            })?;\n\n        // Migrate installed packages to the new workflow\n        migrate_packages(&old.home)?;\n\n        // Remove the package inventory directory, as we no longer cache package tarballs\n        remove_dir_if_exists(old.home.package_inventory_dir())?;\n\n        // Complete the migration, writing the V3 layout file\n        let layout = V3::complete_migration(new_home)?;\n\n        // Remove the V2 layout file, since we're now on V3 (do this after writing the V3 file so that we know the migration succeeded)\n        remove_file_if_exists(old.home.layout_file())?;\n\n        Ok(layout)\n    }\n}\n\nfn migrate_packages(old_home: &v2::VoltaHome) -> Fallible<()> {\n    let packages = get_installed_packages(old_home);\n    let mut session = Session::init();\n\n    for package in packages {\n        migrate_single_package(package, &mut session)?;\n    }\n\n    Ok(())\n}\n\n/// Determine a list of all installed packages that are using the legacy package config\nfn get_installed_packages(old_home: &v2::VoltaHome) -> Vec<LegacyPackageConfig> {\n    WalkDir::new(old_home.default_package_dir())\n        .max_depth(2)\n        .into_iter()\n        .filter_map(|res| match res {\n            Ok(entry) => {\n                if entry.file_type().is_file() {\n                    let config = LegacyPackageConfig::from_file(entry.path());\n\n                    // If unable to parse the config file and this isn't an already-migrated\n                    // package, then show debug information and a warning for the user.\n                    if config.is_none() && !is_migrated_config(entry.path()) {\n                        debug!(\"Unable to parse config file: {}\", entry.path().display());\n                        if let Some(name) = entry.path().file_stem() {\n                            let name = name.to_string_lossy();\n                            warn!(\n                                \"Could not migrate {}. Please run `volta install {0}` to migrate the package manually.\",\n                                name\n                            );\n                        }\n                    }\n\n                    config\n                } else {\n                    None\n                }\n            }\n            Err(error) => {\n                debug!(\"Error reading directory entry: {}\", error);\n                None\n            }\n        })\n        .collect()\n}\n\n/// Determine if a package has already been migrated by attempting to read the V3 PackageConfig\nfn is_migrated_config(config_path: &Path) -> bool {\n    PackageConfig::from_file(config_path).is_ok()\n}\n\n/// Migrate a single package to the new workflow\n///\n/// Note: This relies on the package install logic in `volta_core`. If that logic changes, then\n/// the end result may not be a valid V3 layout any more, and this migration will need to be\n/// updated. Specifically, the invariants we rely on are:\n///\n/// - Package image directory is in the same location\n/// - Package config files are in the same location and the same format\n/// - Binary config files are in the same location and the same format\n///\n/// If any of those are violated, this migration may be invalid and need to be reworked / scrapped\nfn migrate_single_package(config: LegacyPackageConfig, session: &mut Session) -> Fallible<()> {\n    let tool = Package::new(config.name, VersionSpec::Exact(config.version))?;\n\n    let platform: PlatformSpec = config.platform.into();\n    let image = platform.as_binary().checkout(session)?;\n\n    // Run the global install command\n    tool.run_install(&image)?;\n    // Overwrite the config files and image directory\n    tool.complete_install(&image)?;\n\n    Ok(())\n}\n"
  },
  {
    "path": "crates/volta-migrate/src/v4.rs",
    "content": "use std::fs::File;\r\nuse std::path::PathBuf;\r\n\r\nuse super::empty::Empty;\r\nuse super::v3::V3;\r\nuse log::debug;\r\nuse volta_core::error::{Context, ErrorKind, Fallible, VoltaError};\r\n#[cfg(windows)]\r\nuse volta_core::fs::read_dir_eager;\r\nuse volta_core::fs::remove_file_if_exists;\r\nuse volta_layout::v4;\r\n\r\n/// Represents a V4 Volta Layout (used by Volta v2.0.0 and above)\r\n///\r\n/// Holds a reference to the V4 layout struct to support potential future migrations\r\npub struct V4 {\r\n    pub home: v4::VoltaHome,\r\n}\r\n\r\nimpl V4 {\r\n    pub fn new(home: PathBuf) -> Self {\r\n        V4 {\r\n            home: v4::VoltaHome::new(home),\r\n        }\r\n    }\r\n\r\n    /// Write the layout file to mark migration to V4 as complete\r\n    ///\r\n    /// Should only be called once all other migration steps are finished, so that we don't\r\n    /// accidentally mark an incomplete migration as completed\r\n    fn complete_migration(home: v4::VoltaHome) -> Fallible<Self> {\r\n        debug!(\"Writing layout marker file\");\r\n        File::create(home.layout_file()).with_context(|| ErrorKind::CreateLayoutFileError {\r\n            file: home.layout_file().to_owned(),\r\n        })?;\r\n\r\n        Ok(V4 { home })\r\n    }\r\n}\r\n\r\nimpl TryFrom<Empty> for V4 {\r\n    type Error = VoltaError;\r\n\r\n    fn try_from(old: Empty) -> Fallible<V4> {\r\n        debug!(\"New Volta installation detected, creating fresh layout\");\r\n\r\n        let home = v4::VoltaHome::new(old.home);\r\n        home.create().with_context(|| ErrorKind::CreateDirError {\r\n            dir: home.root().to_owned(),\r\n        })?;\r\n\r\n        V4::complete_migration(home)\r\n    }\r\n}\r\n\r\nimpl TryFrom<V3> for V4 {\r\n    type Error = VoltaError;\r\n\r\n    fn try_from(old: V3) -> Fallible<V4> {\r\n        debug!(\"Migrating from V3 layout\");\r\n\r\n        let new_home = v4::VoltaHome::new(old.home.root().to_owned());\r\n        new_home\r\n            .create()\r\n            .with_context(|| ErrorKind::CreateDirError {\r\n                dir: new_home.root().to_owned(),\r\n            })?;\r\n\r\n        // Perform the core of the migration\r\n        #[cfg(windows)]\r\n        {\r\n            migrate_shims(&new_home)?;\r\n            migrate_shared_directory(&new_home)?;\r\n        }\r\n\r\n        // Complete the migration, writing the V4 layout file\r\n        let layout = V4::complete_migration(new_home)?;\r\n\r\n        // Remove the V3 layout file, since we're now on V4 (do this after writing the V4 so that we know the migration succeeded)\r\n        let old_layout_file = old.home.layout_file();\r\n        remove_file_if_exists(old_layout_file)?;\r\n        Ok(layout)\r\n    }\r\n}\r\n\r\n/// Migrate Windows shims to use the new non-symlink approach. Previously, shims were created in\r\n/// the same way as on Unix: With symlinks to the `volta-shim` executable. Now, we use scripts that\r\n/// call `volta run` to execute the underlying tool. This allows us to avoid needing developer\r\n/// mode, making Volta more broadly usable for Windows devs.\r\n///\r\n/// To migrate the shims, we read the shim directory looking for symlinks, remove those, and then\r\n/// file stem (name without extension) to generate new shims.\r\n#[cfg(windows)]\r\nfn migrate_shims(new_home: &v4::VoltaHome) -> Fallible<()> {\r\n    use std::ffi::OsStr;\r\n\r\n    let entries = read_dir_eager(new_home.shim_dir()).with_context(|| ErrorKind::ReadDirError {\r\n        dir: new_home.shim_dir().to_owned(),\r\n    })?;\r\n\r\n    for (entry, metadata) in entries {\r\n        if metadata.is_symlink() {\r\n            let path = entry.path();\r\n            remove_file_if_exists(&path)?;\r\n\r\n            if let Some(shim_name) = path.file_stem().and_then(OsStr::to_str) {\r\n                volta_core::shim::create(shim_name)?;\r\n            }\r\n        }\r\n    }\r\n\r\n    Ok(())\r\n}\r\n\r\n/// Migrate Windows shared directory to use junctions rather than directory symlinks. Similar to\r\n/// the shims, we previously used symlinks to create the shared global package directory, which\r\n/// requires developer mode. By using junctions, we can avoid that requirement entirely.\r\n///\r\n/// To migrate the directories, we read the shim directory, determine the target of each symlink,\r\n/// delete the link, and then create a junction (using volta_core::fs::symlink_dir which delegates\r\n/// to `junction` internally)\r\n#[cfg(windows)]\r\nfn migrate_shared_directory(new_home: &v4::VoltaHome) -> Fallible<()> {\r\n    use std::fs::read_link;\r\n    use volta_core::fs::{remove_dir_if_exists, symlink_dir};\r\n\r\n    let entries =\r\n        read_dir_eager(new_home.shared_lib_root()).with_context(|| ErrorKind::ReadDirError {\r\n            dir: new_home.shared_lib_root().to_owned(),\r\n        })?;\r\n\r\n    for (entry, metadata) in entries {\r\n        if metadata.is_symlink() {\r\n            let path = entry.path();\r\n            let source = read_link(&path).with_context(|| ErrorKind::ReadDirError {\r\n                dir: new_home.shared_lib_root().to_owned(),\r\n            })?;\r\n\r\n            remove_dir_if_exists(&path)?;\r\n            symlink_dir(source, path).with_context(|| ErrorKind::CreateSharedLinkError {\r\n                name: entry.file_name().to_string_lossy().to_string(),\r\n            })?;\r\n        }\r\n    }\r\n\r\n    Ok(())\r\n}\r\n"
  },
  {
    "path": "dev/package.json",
    "content": "{\n  \"name\": \"example\",\n  \"version\": \"1.0\",\n  \"description\": \"an example Node project using volta\",\n  \"author\": \"Dave Herman <david.herman@gmail.com>\",\n  \"main\": \"lib/index.js\",\n  \"devDependencies\": {\n    \"ember-cli\": \"2.18.1\"\n  },\n  \"volta\": {\n    \"node\": \"6.11.1\",\n    \"yarn\": \"1.7.0\"\n  }\n}\n"
  },
  {
    "path": "dev/rpm/build-rpm.sh",
    "content": "#!/usr/bin/env bash\n# Build an RPM package for Volta\n\n# using the directions from https://rpm-packaging-guide.github.io/\n\n# exit on error\nset -e\n\n# only argument is the version number\nrelease_version=\"${1:?Must specify the release version, like \\`build-rpm 1.2.3\\`}\"\narchive_filename=\"v${release_version}.tar.gz\"\n\n# make sure these packages are installed\n# (https://rpm-packaging-guide.github.io/#prerequisites)\nsudo yum install gcc rpm-build rpm-devel rpmlint make python bash coreutils diffutils patch rpmdevtools\n\n# set up the directory layout for the RPM packaging workspace\n# (https://rpm-packaging-guide.github.io/#rpm-packaging-workspace)\nrpmdev-setuptree\n\n# create a tarball of the repo for the specified version\n# using prefix because the rpmbuild process expects a 'volta-<version>' directory\n# (https://rpm-packaging-guide.github.io/#putting-source-code-into-tarball)\ngit archive --format=tar.gz --output=$archive_filename --prefix=\"volta-${release_version}/\" HEAD\n\n# move the archive to the SOURCES dir, after cleaning it up\n# (https://rpm-packaging-guide.github.io/#working-with-spec-files)\nrm -rf \"$HOME/rmpbuild/SOURCES/\"*\nmv \"$archive_filename\" \"$HOME/rpmbuild/SOURCES/\"\n\n# copy the .spec file to SPECS dir\ncp dev/rpm/volta.spec \"$HOME/rpmbuild/SPECS/\"\n\n# build it!\n# (https://rpm-packaging-guide.github.io/#binary-rpms)\nrpmbuild -bb \"$HOME/rpmbuild/SPECS/volta.spec\"\n# (there will be a lot of output)\n\n# then install it and verify everything worked...\necho \"\"\necho \"Build finished!\"\necho \"\"\necho \"Run this to install:\"\necho \"  \\`sudo yum install ~/rpmbuild/RPMS/x86_64/volta-${release_version}-1.el7.x86_64.rpm\\`\"\necho \"\"\necho \"Then run this to uninstall after verifying:\"\necho \"  \\`sudo yum erase volta-${release_version}-1.el7.x86_64\\`\"\n"
  },
  {
    "path": "dev/rpm/volta.spec",
    "content": "Name:           volta\nVersion:        0.8.2\nRelease:        1%{?dist}\nSummary:        The JavaScript Launcher ⚡\n\nLicense:        BSD 2-CLAUSE\nURL:            https://%{name}.sh\nSource0:        https://github.com/volta-cli/volta/archive/v%{version}.tar.gz\n\n# cargo is required, but installing from RPM is failing with libcrypto dep error\n# so you will have to install cargo manually to build this\n#BuildRequires:  cargo\n\n# because these are built with openssl\nRequires:       openssl\n\n\n%description\nVolta’s job is to manage your JavaScript command-line tools, such as node, npm, yarn, or executables shipped as part of JavaScript packages. Similar to package managers, Volta keeps track of which project (if any) you’re working on based on your current directory. The tools in your Volta toolchain automatically detect when you’re in a project that’s using a particular version of the tools, and take care of routing to the right version of the tools for you.\n\n\n%prep\n# this unpacks the tarball to the build root\n%setup -q\n\n\n%build\n# build the release binaries\n# NOTE: build expects to `cd` into a volta-<version> directory\ncargo build --release\n\n\n# this installs into a chroot directory resembling the user's root directory\n%install\n# BUILDROOT/usr/bin\n%define volta_install_dir %{buildroot}/%{_bindir}\n# setup the /usr/bin/volta-lib/ directory\nrm -rf %{buildroot}\nmkdir -p %{volta_install_dir}\n# install everything into into /usr/bin/, so it's on the PATH\ninstall -m 0755 target/release/%{name} %{volta_install_dir}/%{name}\ninstall -m 0755 target/release/volta-shim %{volta_install_dir}/volta-shim\ninstall -m 0755 target/release/volta-migrate %{volta_install_dir}/volta-migrate\n\n\n# files installed by this package\n%files\n%license LICENSE\n%{_bindir}/%{name}\n%{_bindir}/volta-shim\n%{_bindir}/volta-migrate\n\n\n# this runs before install\n%pre\n# make sure the /usr/bin/volta/ dir does not exist, from prev RPM installs (or this will fail)\nprintf '\\033[1;32m%12s\\033[0m %s\\n' \"Running\" \"Volta pre-install...\" 1>&2\nrm -rf %{_bindir}/%{name}\n\n\n# this runs after install, and sets up VOLTA_HOME and the shell integration\n%post\nprintf '\\033[1;32m%12s\\033[0m %s\\n' \"Running\" \"Volta post-install setup...\" 1>&2\n# run this as the user who invoked sudo (not as root, because we're writing to $HOME)\n/bin/su -c \"%{_bindir}/volta setup\" - $SUDO_USER\n\n\n%changelog\n* Tue Oct 22 2019 Charles Pierce <cpierce.grad@gmail.com> - 0.6.5-1\n- Update to use 'volta setup' as the postinstall script\n* Mon Jun 03 2019 Michael Stewart <mikrostew@gmail.com> - 0.5.3-1\n- First volta package\n"
  },
  {
    "path": "dev/unix/SHASUMS256.txt",
    "content": "fbdc4b8cb33fb6d19e5f07b22423265943d34e7e5c3d5a1efcecc9621854f9cb  volta-install.sh\n"
  },
  {
    "path": "dev/unix/boot-install.sh",
    "content": "#!/usr/bin/env bash\n\n# This is the bootstrap Unix installer served by `https://get.volta.sh`.\n# Its responsibility is to query the system to determine what OS (and in the\n# case of Linux, what OpenSSL version) the system has, and then proceed to\n# fetch and install the appropriate build of Volta.\n\nvolta_get_latest_release() {\n  curl --silent https://volta.sh/latest-version\n}\n\nvolta_eprintf() {\n  command printf \"$1\\n\" 1>&2\n}\n\nvolta_info() {\n  local ACTION\n  local DETAILS\n  ACTION=\"$1\"\n  DETAILS=\"$2\"\n  command printf '\\033[1;32m%12s\\033[0m %s\\n' \"${ACTION}\" \"${DETAILS}\" 1>&2\n}\n\nvolta_error() {\n  command printf '\\033[1;31mError\\033[0m: ' 1>&2\n  volta_eprintf \"$1\"\n}\n\nvolta_warning() {\n  command printf '\\033[1;33mWarning\\033[0m: ' 1>&2\n  volta_eprintf \"$1\"\n  volta_eprintf ''\n}\n\nvolta_request() {\n  command printf \"\\033[1m$1\\033[0m\" 1>&2\n  volta_eprintf ''\n}\n\nlegacy_install_dir() {\n  printf \"%s\" \"${NOTION_HOME:-\"$HOME/.notion\"}\"\n}\n\n# Check for a legacy installation from when the tool was named Notion.\nvolta_check_legacy_installation() {\n  local LEGACY_INSTALL_DIR=\"$(legacy_install_dir)\"\n  if [[ -d \"$LEGACY_INSTALL_DIR\" ]]; then\n      volta_eprintf \"\"\n      volta_error \"You have an existing Notion install, which can't be automatically upgraded to Volta.\"\n      volta_request \"       Please delete $LEGACY_INSTALL_DIR and try again.\"\n      volta_eprintf \"\"\n      volta_eprintf \"(We plan to implement automatic upgrades in the future. Thanks for bearing with us!)\"\n      volta_eprintf \"\"\n      exit 1\n  fi\n}\n\nvolta_install_dir() {\n  printf %s \"${VOLTA_HOME:-\"$HOME/.volta\"}\"\n}\n\n# Check for an existing installation that needs to be removed.\nvolta_check_existing_installation() {\n  local LATEST_VERSION=\"$1\"\n  local INSTALL_DIR=\"$(volta_install_dir)\"\n  local VOLTA_BIN=\"${INSTALL_DIR}/volta\"\n\n  if [[ -n \"$INSTALL_DIR\" && -x \"$VOLTA_BIN\" ]]; then\n    local PREV_VOLTA_VERSION\n    # Some 0.1.* builds would eagerly validate package.json even for benign commands,\n    # so just to be safe we'll ignore errors and consider those to be 0.1 as well.\n    PREV_VOLTA_VERSION=\"$( ($VOLTA_BIN --version 2>/dev/null || echo 0.1) | sed -E 's/^.*([0-9]+\\.[0-9]+\\.[0-9]+).*$/\\1/')\"\n    if [ \"$PREV_VOLTA_VERSION\" == \"$LATEST_VERSION\" ]; then\n      volta_eprintf \"\"\n      volta_eprintf \"Latest version $LATEST_VERSION already installed\"\n      exit 0\n    fi\n    if [[ \"$PREV_VOLTA_VERSION\" == 0.1* || \"$PREV_VOLTA_VERSION\" == 0.2* || \"$PREV_VOLTA_VERSION\" == 0.3* ]]; then\n      volta_eprintf \"\"\n      volta_error \"Your Volta installation is out of date and can't be automatically upgraded.\"\n      volta_request \"       Please delete or move $INSTALL_DIR and try again.\"\n      volta_eprintf \"\"\n      volta_eprintf \"(We plan to implement automatic upgrades in the future. Thanks for bearing with us!)\"\n      volta_eprintf \"\"\n      exit 1\n    fi\n  fi\n}\n\n# determines the major and minor version of OpenSSL on the system\nvolta_get_openssl_version() {\n  local LIB\n  local LIBNAME\n  local FULLVERSION\n  local MAJOR\n  local MINOR\n\n  # By default, we'll guess OpenSSL 1.0.1.\n  LIB=\"$(openssl version 2>/dev/null || echo 'OpenSSL 1.0.1')\"\n\n  LIBNAME=\"$(echo $LIB | awk '{print $1;}')\"\n\n  if [[ \"$LIBNAME\" != \"OpenSSL\" ]]; then\n    volta_error \"Your system SSL library ($LIBNAME) is not currently supported on this OS.\"\n    volta_eprintf \"\"\n    exit 1\n  fi\n\n  FULLVERSION=\"$(echo $LIB | awk '{print $2;}')\"\n  MAJOR=\"$(echo ${FULLVERSION} | cut -d. -f1)\"\n  MINOR=\"$(echo ${FULLVERSION} | cut -d. -f2)\"\n\n  # If we have version 1.0.x, check for RHEL / CentOS style OpenSSL SONAME (.so.10)\n  if [[ \"${MAJOR}.${MINOR}\" == \"1.0\" && -f \"/usr/lib64/libcrypto.so.10\" ]]; then\n    echo \"rhel\"\n  else\n    echo \"${MAJOR}.${MINOR}\"\n  fi\n}\n\nVOLTA_LATEST_VERSION=$(volta_get_latest_release)\n\nvolta_info 'Checking' \"for existing Volta installation\"\nvolta_check_legacy_installation\nvolta_check_existing_installation \"$VOLTA_LATEST_VERSION\"\n\n\ncase $(uname) in\n    Linux)\n        VOLTA_OS=\"linux-openssl-$(volta_get_openssl_version)\"\n        VOLTA_PRETTY_OS=Linux\n        ;;\n    Darwin)\n        VOLTA_OS=macos\n        VOLTA_PRETTY_OS=macOS\n        ;;\n    *)\n        volta_error \"The current operating system does not appear to be supported by Volta.\"\n        volta_eprintf \"\"\n        exit 1\nesac\n\nVOLTA_INSTALLER=\"https://github.com/volta-cli/volta/releases/download/v${VOLTA_LATEST_VERSION}/volta-${VOLTA_LATEST_VERSION}-${VOLTA_OS}.sh\"\n\nvolta_info 'Fetching' \"${VOLTA_PRETTY_OS} installer\"\n\ncurl -#SLf ${VOLTA_INSTALLER} | bash\nSTATUS=$?\n\nexit $STATUS\n"
  },
  {
    "path": "dev/unix/build.sh",
    "content": "#!/usr/bin/env bash\n\nscript_dir=\"$(dirname \"$0\")\"\n\nusage() {\n  cat <<END_USAGE\nbuild.sh: generate volta's generic unix installation script\n\nusage: build.sh [target]\n  [target]   build artifacts to use ('release' or 'debug', defaults to 'release')\n\nThe output file is saved as $script_dir/install.sh.\nEND_USAGE\n}\n\nif [ -z \"$1\" ]; then\n  target_dir='release'\nelif [[ \"$1\" =~ (debug|release) ]]; then\n  target_dir=\"$1\"\nelse\n  usage\n  exit 1\nfi\n\nencode_base64_sed_command() {\n  command printf \"s|<PLACEHOLDER_$2_PAYLOAD>|\" > $1.base64.txt\n  cat $3 | base64 - | tr -d '\\n' >> $1.base64.txt\n  command printf \"|\\n\" >> $1.base64.txt\n}\n\nencode_expand_sed_command() {\n  # This atrocity is a combination of:\n  # - https://unix.stackexchange.com/questions/141387/sed-replace-string-with-file-contents\n  # - https://serverfault.com/questions/391360/remove-line-break-using-awk\n  # - https://stackoverflow.com/questions/1421478/how-do-i-use-a-new-line-replacement-in-a-bsd-sed\n  command printf \"s|<PLACEHOLDER_$2_PAYLOAD>|$(sed 's/|/\\\\|/g' $3 | awk '{printf \"%s\\\\\\n\",$0} END {print \"\"}' )\\\\\\n|\\n\" > $1.expand.txt\n}\n\nbuild_dir=\"$script_dir/../../target/$target_dir\"\nshell_dir=\"$script_dir/../../shell\"\n\nencode_base64_sed_command volta VOLTA \"$build_dir/volta\"\nencode_base64_sed_command shim SHIM \"$build_dir/shim\"\nencode_expand_sed_command bash_launcher BASH_LAUNCHER \"$shell_dir/unix/load.sh\"\nencode_expand_sed_command fish_launcher FISH_LAUNCHER \"$shell_dir/unix/load.fish\"\n\nsed -f volta.base64.txt \\\n    -f shim.base64.txt \\\n    -f bash_launcher.expand.txt \\\n    -f fish_launcher.expand.txt \\\n    < \"$script_dir/install.sh.in\" > \"$script_dir/install.sh\"\n\nchmod 755 \"$script_dir/install.sh\"\n\nrm volta.base64.txt \\\n   shim.base64.txt \\\n   bash_launcher.expand.txt \\\n   fish_launcher.expand.txt\n"
  },
  {
    "path": "dev/unix/install.sh.in",
    "content": "#!/usr/bin/env bash\n\n{\n\nvolta_unpack_volta() {\n  base64 --decode <<'END_BINARY_PAYLOAD'\n<PLACEHOLDER_VOLTA_PAYLOAD>\nEND_BINARY_PAYLOAD\n}\n\nvolta_unpack_shim() {\n  base64 --decode <<'END_BINARY_PAYLOAD'\n<PLACEHOLDER_SHIM_PAYLOAD>\nEND_BINARY_PAYLOAD\n}\n\nvolta_unpack_bash_launcher() {\n  cat <<'END_TEXT_PAYLOAD'\n<PLACEHOLDER_BASH_LAUNCHER_PAYLOAD>\nEND_TEXT_PAYLOAD\n}\n\nvolta_unpack_fish_launcher() {\n  cat <<'END_TEXT_PAYLOAD'\n<PLACEHOLDER_FISH_LAUNCHER_PAYLOAD>\nEND_TEXT_PAYLOAD\n}\n\nvolta_install_dir() {\n  printf %s \"${VOLTA_HOME:-\"$HOME/.volta\"}\"\n}\n\nvolta_create_tree() {\n  local INSTALL_DIR\n\n  INSTALL_DIR=\"$(volta_install_dir)\"\n\n  mkdir -p \"${INSTALL_DIR}\"\n\n  # ~/\n  #     .volta/\n  #         cache/\n  #             node/\n  #         tools/\n  #             inventory/\n  #                 node/\n  #                 packages/\n  #                 yarn/\n  #             image/\n  #                 node/\n  #                 yarn/\n  #             user/\n  #         bin/\n  #         tmp/\n\n  mkdir -p \"${INSTALL_DIR}\"/cache/node\n  mkdir -p \"${INSTALL_DIR}\"/tools/inventory/node\n  mkdir -p \"${INSTALL_DIR}\"/tools/inventory/packages\n  mkdir -p \"${INSTALL_DIR}\"/tools/inventory/yarn\n  mkdir -p \"${INSTALL_DIR}\"/tools/image/node\n  mkdir -p \"${INSTALL_DIR}\"/tools/image/yarn\n  mkdir -p \"${INSTALL_DIR}\"/tools/user\n  mkdir -p \"${INSTALL_DIR}\"/bin\n  mkdir -p \"${INSTALL_DIR}\"/tmp\n}\n\nvolta_create_binaries() {\n  local INSTALL_DIR\n\n  INSTALL_DIR=\"$(volta_install_dir)\"\n\n  volta_unpack_volta        > \"${INSTALL_DIR}\"/volta\n  volta_unpack_shim          > \"${INSTALL_DIR}\"/shim\n  volta_unpack_bash_launcher > \"${INSTALL_DIR}\"/load.sh\n  volta_unpack_fish_launcher > \"${INSTALL_DIR}\"/load.fish\n\n  # Remove any existing binaries for tools so that the symlinks can be installed\n  # using -f so there is no error if the files don't exist\n  rm -f \"${INSTALL_DIR}\"/bin/node\n  rm -f \"${INSTALL_DIR}\"/bin/npm\n  rm -f \"${INSTALL_DIR}\"/bin/npx\n  rm -f \"${INSTALL_DIR}\"/bin/yarn\n\n  for FILE_NAME in \"${INSTALL_DIR}\"/bin/*; do\n    if [ -e \"${FILE_NAME}\" ] && ! [ -d \"${FILE_NAME}\" ]; then\n      rm -f \"${FILE_NAME}\"\n      ln -s \"${INSTALL_DIR}\"/shim \"${FILE_NAME}\"\n    fi\n  done\n\n  ln -s \"${INSTALL_DIR}\"/shim \"${INSTALL_DIR}\"/bin/node\n  ln -s \"${INSTALL_DIR}\"/shim \"${INSTALL_DIR}\"/bin/npm\n  ln -s \"${INSTALL_DIR}\"/shim \"${INSTALL_DIR}\"/bin/npx\n  ln -s \"${INSTALL_DIR}\"/shim \"${INSTALL_DIR}\"/bin/yarn\n  ln -s \"${INSTALL_DIR}\"/shim \"${INSTALL_DIR}\"/bin/yarnpkg\n\n  chmod 755 \"${INSTALL_DIR}/\"/volta \"${INSTALL_DIR}/bin\"/* \"${INSTALL_DIR}\"/shim\n}\n\nvolta_try_profile() {\n  if [ -z \"${1-}\" ] || [ ! -f \"${1}\" ]; then\n    return 1\n  fi\n  echo \"${1}\"\n}\n\n# If file exists, echo it\necho_fexists() {\n  [ -f \"$1\" ] && echo \"$1\"\n}\n\nvolta_detect_profile() {\n  if [ -n \"${PROFILE}\" ] && [ -f \"${PROFILE}\" ]; then\n    echo \"${PROFILE}\"\n    return\n  fi\n\n  # try to detect the current shell\n  case \"$(basename \"/$SHELL\")\" in\n    bash)\n      # Shells on macOS default to opening with a login shell, while Linuxes\n      # default to a *non*-login shell, so if this is macOS we look for\n      # `.bash_profile` first; if it's Linux, we look for `.bashrc` first. The\n      # `*` fallthrough covers more than just Linux: it's everything that is not\n      # macOS (Darwin). It can be made narrower later if need be.\n      case $(uname) in\n        Darwin)\n          echo_fexists \"$HOME/.bash_profile\" || echo_fexists \"$HOME/.bashrc\"\n          ;;\n        *)\n          echo_fexists \"$HOME/.bashrc\" || echo_fexists \"$HOME/.bash_profile\"\n          ;;\n      esac\n      ;;\n    zsh)\n      echo_fexists \"$HOME/.zshenv\" || echo_fexists \"$HOME/.zshrc\"\n      ;;\n    fish)\n      echo \"$HOME/.config/fish/config.fish\"\n      ;;\n    *)\n      # Fall back to checking for profile file existence. Once again, the order\n      # differs between macOS and everything else.\n      local profiles\n      case $(uname) in\n        Darwin)\n          profiles=( .profile .bash_profile .bashrc .zshrc .config/fish/config.fish )\n          ;;\n        *)\n          profiles=( .profile .bashrc .bash_profile .zshrc .config/fish/config.fish )\n          ;;\n      esac\n\n      for profile in \"${profiles[@]}\"; do\n        echo_fexists \"$HOME/$profile\" && break\n      done\n      ;;\n  esac\n}\n\nvolta_build_path_str() {\n  local PROFILE\n  PROFILE=\"$1\"\n  local PROFILE_INSTALL_DIR\n  PROFILE_INSTALL_DIR=\"$2\"\n\n  local PATH_STR\n  if [[ $PROFILE =~ \\.fish$ ]]; then\n    PATH_STR=\"\\\\nset -gx VOLTA_HOME \\\"${PROFILE_INSTALL_DIR}\\\"\\\\ntest -s \\\"\\$VOLTA_HOME/load.fish\\\"; and source \\\"\\$VOLTA_HOME/load.fish\\\"\\\\n\\\\nstring match -r \\\".volta\\\" \\\"\\$PATH\\\" > /dev/null; or set -gx PATH \\\"\\$VOLTA_HOME/bin\\\" \\$PATH\"\n  else\n    PATH_STR=\"\\\\nexport VOLTA_HOME=\\\"${PROFILE_INSTALL_DIR}\\\"\\\\n[ -s \\\"\\$VOLTA_HOME/load.sh\\\" ] && \\\\. \\\"\\$VOLTA_HOME/load.sh\\\"\\\\n\\\\nexport PATH=\\\"\\${VOLTA_HOME}/bin:\\$PATH\\\"\"\n  fi\n\n  echo \"$PATH_STR\"\n}\n\nvolta_eprintf() {\n  command printf \"$1\\n\" 1>&2\n}\n\nvolta_info() {\n  local ACTION\n  local DETAILS\n  ACTION=\"$1\"\n  DETAILS=\"$2\"\n  command printf '\\033[1;32m%12s\\033[0m %s\\n' \"${ACTION}\" \"${DETAILS}\" 1>&2\n}\n\nvolta_error() {\n  command printf '\\033[1;31mError\\033[0m: ' 1>&2\n  volta_eprintf \"$1\"\n  volta_eprintf ''\n}\n\nvolta_warning() {\n  command printf '\\033[1;33mWarning\\033[0m: ' 1>&2\n  volta_eprintf \"$1\"\n}\n\nvolta_install() {\n  if [ -n \"${VOLTA_HOME-}\" ] && [ -e \"${VOLTA_HOME}\" ] && ! [ -d \"${VOLTA_HOME}\" ]; then\n    volta_error \"\\$VOLTA_HOME is set but is not a directory (${VOLTA_HOME}).\"\n    volta_eprintf \"Please check your profile scripts and environment.\"\n    exit 1\n  fi\n\n  volta_info 'Creating' \"Volta directory tree ($(volta_install_dir))\"\n  volta_create_tree\n\n  volta_info 'Unpacking' \"\\`volta\\` executable and shims\"\n  volta_create_binaries\n\n  local VOLTA_PROFILE\n  VOLTA_PROFILE=\"$(volta_detect_profile)\"\n  volta_info 'Editing' \"user profile ($VOLTA_PROFILE)\"\n\n  local PROFILE_INSTALL_DIR\n  PROFILE_INSTALL_DIR=$(volta_install_dir | sed \"s:^$HOME:\\$HOME:\")\n  local PATH_STR\n  PATH_STR=\"$(volta_build_path_str \"$VOLTA_PROFILE\" \"$PROFILE_INSTALL_DIR\")\"\n\n  if [ -z \"${VOLTA_PROFILE-}\" ] ; then\n    local TRIED_PROFILE\n    if [ -n \"${PROFILE}\" ]; then\n      TRIED_PROFILE=\"${VOLTA_PROFILE} (as defined in \\$PROFILE), \"\n    fi\n    volta_error \"No user profile found.\"\n    volta_eprintf \"Tried ${TRIED_PROFILE-}~/.bashrc, ~/.bash_profile, ~/.zshrc, ~/.profile, and ~.config/fish/config.fish.\"\n    volta_eprintf ''\n    volta_eprintf \"You can either create one of these and try again or add this to the appropriate file:\"\n    volta_eprintf \"${PATH_STR}\"\n    exit 1\n  else\n    if ! command grep -qc 'VOLTA_HOME' \"$VOLTA_PROFILE\"; then\n      command printf \"${PATH_STR}\" >> \"$VOLTA_PROFILE\"\n    else\n      volta_eprintf ''\n      volta_warning \"Your profile (${VOLTA_PROFILE}) already mentions\"\n      volta_eprintf \"         Volta and has not been changed.\"\n      volta_eprintf ''\n    fi\n  fi\n\n  if command grep -qc 'NOTION_HOME' \"$VOLTA_PROFILE\"; then\n      volta_eprintf ''\n      volta_warning \"Your profile (${VOLTA_PROFILE}) mentions Notion.\"\n      volta_eprintf \"         You probably want to remove that.\"\n      volta_eprintf ''\n  fi\n\n  volta_info \"Finished\" 'installation. Open a new terminal to start using Volta!'\n  exit 0\n}\n\nvolta_install\n\n}\n"
  },
  {
    "path": "dev/unix/release.sh",
    "content": "#!/usr/bin/env bash\n\n# Script to build the binaries and package them up for release.\n# This should be run from the top-level directory.\n\n# get the directory of this script\n# (from https://stackoverflow.com/a/246128)\nDIR=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" >/dev/null 2>&1 && pwd )\"\n\n# get shared functions from the volta-install.sh file\nsource \"$DIR/volta-install.sh\"\n\nusage() {\n  cat >&2 <<END_OF_USAGE\nrelease.sh\n\nCompile and package a release for Volta\n\nUSAGE:\n    ./dev/unix/release.sh [FLAGS] [OPTIONS]\n\nFLAGS:\n    -h, --help          Prints this help info\n\nOPTIONS:\n        --release       Build artifacts in release mode, with optimizations (default)\n        --dev           Build artifacts in dev mode, without optimizations\nEND_OF_USAGE\n}\n\n\n# default to compiling with '--release'\nbuild_with_release=\"true\"\n\n# parse input arguments\ncase \"$1\" in\n  -h|--help)\n    usage\n    exit 0\n    ;;\n  --dev)\n    build_with_release=\"false\"\n    ;;\n  ''|--release)\n    # not really necessary to set this again\n    build_with_release=\"true\"\n    ;;\n  *)\n    error \"Unknown argument '$1'\"\n    usage\n    exit1\n    ;;\nesac\n\n# read the current version from Cargo.toml\ncargo_toml_contents=\"$(<Cargo.toml)\"\nVOLTA_VERSION=\"$(parse_cargo_version \"$cargo_toml_contents\")\" || exit 1\n\n# figure out the OS details\nos=\"$(uname -s)\"\nopenssl_version=\"$(openssl version)\" || exit 1\nVOLTA_OS=\"$(parse_os_info \"$os\" \"$openssl_version\")\"\nif [ \"$?\" != 0 ]; then\n  error \"Releases for '$os' are not yet supported.\"\n  request \"To support '$os', add another case to parse_os_info() in volta-install.sh.\"\n  exit 1\nfi\n\nrelease_filename=\"volta-$VOLTA_VERSION-$VOLTA_OS\"\n\n# first make sure the release binaries have been built\ninfo 'Building' \"Volta for $(bold \"$release_filename\")\"\nif [ \"$build_with_release\" == \"true\" ]\nthen\n  target_dir=\"target/release\"\n  cargo build --release\nelse\n  target_dir=\"target/debug\"\n  cargo build\nfi || exit 1\n\n# then package the binaries and shell scripts together\ninfo 'Packaging' \"the compiled binaries\"\ncd \"$target_dir\"\n# using COPYFILE_DISABLE to avoid storing extended attribute files when run on OSX\n# (see https://superuser.com/q/61185)\nCOPYFILE_DISABLE=1 tar -czvf \"$release_filename.tar.gz\" volta volta-shim volta-migrate\n\ninfo 'Completed' \"release in file $target_dir/$release_filename.tar.gz\"\n"
  },
  {
    "path": "dev/unix/test-events",
    "content": "#!/usr/bin/env bash\n\n# long-running script to show events and test spawning processes\n\nmy_pid=\"$$\"\n\necho \"$my_pid called with $# args: $@\"\n\n# get JSON data from stdin\nread some_data\n\necho \"$my_pid got:\"\n# display nicely with jq if it is installed\ncommand -v jq >/dev/null 2>&1 && echo \"$some_data\" | jq '.' || echo \"$some_data\"\n\ni=0\nwhile [ $i -lt 3 ]\ndo\n  sleep 2s\n  echo \"$my_pid still running!\"\n  let i=i+1\ndone\n\necho \"$my_pid done!!\"\n\n"
  },
  {
    "path": "dev/unix/tests/install-script.bats",
    "content": "# test the volta-install.sh script\n\n# load the functions from the script\nsource dev/unix/volta-install.sh\n\n\n# happy path test to parse the version from Cargo.toml\n@test \"parse_cargo_version - normal Cargo.toml\" {\n  input=$(cat <<'END_CARGO_TOML'\n[package]\nname = \"volta\"\nversion = \"0.7.38\"\nauthors = [\"David Herman <david.herman@gmail.com>\"]\nlicense = \"BSD-2-Clause\"\nEND_CARGO_TOML\n)\n\n  expected_output=\"0.7.38\"\n\n  run parse_cargo_version \"$input\"\n  [ \"$status\" -eq 0 ]\n  diff <(echo \"$output\") <(echo \"$expected_output\")\n}\n\n# it doesn't parse the version from other dependencies\n@test \"parse_cargo_version - error\" {\n  input=$(cat <<'END_CARGO_TOML'\n[dependencies]\nvolta-core = { path = \"crates/volta-core\" }\nserde = { version = \"1.0\", features = [\"derive\"] }\nserde_json = \"1.0.37\"\nconsole = \"0.6.1\"\nEND_CARGO_TOML\n)\n\n  expected_output=$(echo -e \"\\033[1;31mError\\033[0m: Could not determine the current version from Cargo.toml\")\n\n  run parse_cargo_version \"$input\"\n  [ \"$status\" -eq 1 ]\n  diff <(echo \"$output\") <(echo \"$expected_output\")\n}\n\n# linux\n@test \"parse_os_info - linux\" {\n  expected_output=\"linux\"\n\n  run parse_os_info \"Linux\"\n  [ \"$status\" -eq 0 ]\n  diff <(echo \"$output\") <(echo \"$expected_output\")\n}\n\n# macos\n@test \"parse_os_info - macos\" {\n  expected_output=\"macos\"\n\n  run parse_os_info \"Darwin\"\n  [ \"$status\" -eq 0 ]\n  diff <(echo \"$output\") <(echo \"$expected_output\")\n}\n\n# unsupported OS\n@test \"parse_os_info - unsupported OS\" {\n  expected_output=\"\"\n\n  run parse_os_info \"DOS\"\n  [ \"$status\" -eq 1 ]\n  diff <(echo \"$output\") <(echo \"$expected_output\")\n}\n\n# test element_in helper function\n@test \"element_in works correctly\" {\n  run element_in \"foo\" \"foo\" \"bar\" \"baz\"\n  [ \"$status\" -eq 0 ]\n\n  array=( \"foo\" \"bar\" \"baz\" )\n  run element_in \"foo\" \"${array[@]}\"\n  [ \"$status\" -eq 0 ]\n  run element_in \"bar\" \"${array[@]}\"\n  [ \"$status\" -eq 0 ]\n  run element_in \"baz\" \"${array[@]}\"\n  [ \"$status\" -eq 0 ]\n\n  run element_in \"fob\" \"${array[@]}\"\n  [ \"$status\" -eq 1 ]\n}\n\n\n# test VOLTA_HOME settings\n\n@test \"volta_home_is_ok - true cases\" {\n  # unset is fine\n  unset VOLTA_HOME\n  run volta_home_is_ok\n  [ \"$status\" -eq 0 ]\n\n  # empty is fine\n  VOLTA_HOME=\"\"\n  run volta_home_is_ok\n  [ \"$status\" -eq 0 ]\n\n  # non-existing dir is fine\n  VOLTA_HOME=\"/some/dir/that/does/not/exist/anywhere\"\n  run volta_home_is_ok\n  [ \"$status\" -eq 0 ]\n\n  # existing dir is fine\n  VOLTA_HOME=\"$HOME\"\n  run volta_home_is_ok\n  [ \"$status\" -eq 0 ]\n}\n\n@test \"volta_home_is_ok - not ok\" {\n  # file is not ok\n  VOLTA_HOME=\"$(mktemp)\"\n  run volta_home_is_ok\n  [ \"$status\" -eq 1 ]\n}\n\n# TODO: test creating symlinks\n"
  },
  {
    "path": "dev/unix/volta-install-legacy.sh",
    "content": "#!/usr/bin/env bash\n\n# This is the bootstrap Unix installer served by `https://get.volta.sh`.\n# Its responsibility is to query the system to determine what OS (and in the\n# case of Linux, what OpenSSL version) the system has, fetch and install the\n# appropriate build of Volta, and modify the user's profile.\n\n# NOTE: to use an internal company repo, change how this determines the latest version\nget_latest_release() {\n  curl --silent \"https://volta.sh/latest-version\"\n}\n\nrelease_url() {\n  echo \"https://github.com/volta-cli/volta/releases\"\n}\n\ndownload_release_from_repo() {\n  local version=\"$1\"\n  local os_info=\"$2\"\n  local tmpdir=\"$3\"\n\n  local filename=\"volta-$version-$os_info.tar.gz\"\n  local download_file=\"$tmpdir/$filename\"\n  local archive_url=\"$(release_url)/download/v$version/$filename\"\n\n  curl --progress-bar --show-error --location --fail \"$archive_url\" --output \"$download_file\" && echo \"$download_file\"\n}\n\nusage() {\n    cat >&2 <<END_USAGE\nvolta-install: The installer for Volta\n\nUSAGE:\n    volta-install [FLAGS] [OPTIONS]\n\nFLAGS:\n    -h, --help                  Prints help information\n\nOPTIONS:\n        --dev                   Compile and install Volta locally, using the dev target\n        --release               Compile and install Volta locally, using the release target\n        --version <version>     Install a specific release version of Volta\nEND_USAGE\n}\n\ninfo() {\n  local action=\"$1\"\n  local details=\"$2\"\n  command printf '\\033[1;32m%12s\\033[0m %s\\n' \"$action\" \"$details\" 1>&2\n}\n\nerror() {\n  command printf '\\033[1;31mError\\033[0m: %s\\n\\n' \"$1\" 1>&2\n}\n\nwarning() {\n  command printf '\\033[1;33mWarning\\033[0m: %s\\n\\n' \"$1\" 1>&2\n}\n\nrequest() {\n  command printf '\\033[1m%s\\033[0m\\n' \"$1\" 1>&2\n}\n\neprintf() {\n  command printf '%s\\n' \"$1\" 1>&2\n}\n\nbold() {\n  command printf '\\033[1m%s\\033[0m' \"$1\"\n}\n\n# create symlinks for shims in the bin/ dir\ncreate_symlinks() {\n  local install_dir=\"$1\"\n\n  info 'Creating' \"symlinks and shims\"\n  local main_shims=( node npm npx yarn yarnpkg )\n  local shim_exec=\"$install_dir/shim\"\n  local main_exec=\"$install_dir/volta\"\n\n  # remove these symlinks or binaries if they exist, so that the symlinks can be created later\n  # (using -f so there is no error if the files don't exist)\n  for shim in \"${main_shims[@]}\"; do\n    rm -f \"$install_dir/bin/$shim\"\n  done\n\n  # update symlinks for any shims created by the user\n  for file in \"$install_dir\"/bin/*; do\n    if [ -e \"$file\" ] && ! [ -d \"$file\" ]; then\n      rm -f \"$file\"\n      ln -s \"$shim_exec\" \"$file\"\n      chmod 755 \"$file\"\n    fi\n  done\n\n  # re-link the non-user shims\n  for shim in \"${main_shims[@]}\"; do\n    ln -s \"$shim_exec\" \"$install_dir/bin/$shim\"\n    chmod 755 \"$install_dir/bin/$shim\"\n  done\n\n  # and make sure these are executable\n  chmod 755 \"$shim_exec\" \"$main_exec\"\n}\n\n# If file exists, echo it\necho_fexists() {\n  [ -f \"$1\" ] && echo \"$1\"\n}\n\ndetect_profile() {\n  local shellname=\"$1\"\n  local uname=\"$2\"\n\n  if [ -f \"$PROFILE\" ]; then\n    echo \"$PROFILE\"\n    return\n  fi\n\n  # try to detect the current shell\n  case \"$shellname\" in\n    bash)\n      # Shells on macOS default to opening with a login shell, while Linuxes\n      # default to a *non*-login shell, so if this is macOS we look for\n      # `.bash_profile` first; if it's Linux, we look for `.bashrc` first. The\n      # `*` fallthrough covers more than just Linux: it's everything that is not\n      # macOS (Darwin). It can be made narrower later if need be.\n      case $uname in\n        Darwin)\n          echo_fexists \"$HOME/.bash_profile\" || echo_fexists \"$HOME/.bashrc\"\n        ;;\n        *)\n          echo_fexists \"$HOME/.bashrc\" || echo_fexists \"$HOME/.bash_profile\"\n        ;;\n      esac\n      ;;\n    zsh)\n      echo_fexists \"$HOME/.zshenv\" || echo_fexists \"$HOME/.zshrc\"\n      ;;\n    fish)\n      echo \"$HOME/.config/fish/config.fish\"\n      ;;\n    *)\n      # Fall back to checking for profile file existence. Once again, the order\n      # differs between macOS and everything else.\n      local profiles\n      case $uname in\n        Darwin)\n          profiles=( .profile .bash_profile .bashrc .zshrc .config/fish/config.fish )\n          ;;\n        *)\n          profiles=( .profile .bashrc .bash_profile .zshrc .config/fish/config.fish )\n          ;;\n      esac\n\n      for profile in \"${profiles[@]}\"; do\n        echo_fexists \"$HOME/$profile\" && break\n      done\n      ;;\n  esac\n}\n\n# generate shell code to source the loading script and modify the path for the input profile\nbuild_path_str() {\n  local profile=\"$1\"\n  local profile_install_dir=\"$2\"\n\n  if [[ $profile =~ \\.fish$ ]]; then\n    # fish uses a little different syntax to load the shell integration script, and modify the PATH\n    cat <<END_FISH_SCRIPT\n\nset -gx VOLTA_HOME \"$profile_install_dir\"\ntest -s \"\\$VOLTA_HOME/load.fish\"; and source \"\\$VOLTA_HOME/load.fish\"\n\nstring match -r \".volta\" \"\\$PATH\" > /dev/null; or set -gx PATH \"\\$VOLTA_HOME/bin\" \\$PATH\nEND_FISH_SCRIPT\n  else\n    # bash and zsh\n    cat <<END_BASH_SCRIPT\n\nexport VOLTA_HOME=\"$profile_install_dir\"\n[ -s \"\\$VOLTA_HOME/load.sh\" ] && . \"\\$VOLTA_HOME/load.sh\"\n\nexport PATH=\"\\$VOLTA_HOME/bin:\\$PATH\"\nEND_BASH_SCRIPT\n  fi\n}\n\n# check for issue with VOLTA_HOME\n# if it is set, and exists, but is not a directory, the install will fail\nvolta_home_is_ok() {\n  if [ -n \"${VOLTA_HOME-}\" ] && [ -e \"$VOLTA_HOME\" ] && ! [ -d \"$VOLTA_HOME\" ]; then\n    error \"\\$VOLTA_HOME is set but is not a directory ($VOLTA_HOME).\"\n    eprintf \"Please check your profile scripts and environment.\"\n    return 1\n  fi\n  return 0\n}\n\nupdate_profile() {\n  local install_dir=\"$1\"\n\n  local profile_install_dir=$(echo \"$install_dir\" | sed \"s:^$HOME:\\$HOME:\")\n  local detected_profile=\"$(detect_profile $(basename \"/$SHELL\") $(uname -s) )\"\n  local path_str=\"$(build_path_str \"$detected_profile\" \"$profile_install_dir\")\"\n  info 'Editing' \"user profile ($detected_profile)\"\n\n  if [ -z \"${detected_profile-}\" ] ; then\n    error \"No user profile found.\"\n    eprintf \"Tried \\$PROFILE ($PROFILE), ~/.bashrc, ~/.bash_profile, ~/.zshrc, ~/.profile, and ~/.config/fish/config.fish.\"\n    eprintf ''\n    eprintf \"You can either create one of these and try again or add this to the appropriate file:\"\n    eprintf \"$path_str\"\n    return 1\n  else\n    if ! command grep -qc 'VOLTA_HOME' \"$detected_profile\"; then\n      command printf \"$path_str\" >> \"$detected_profile\"\n    else\n      warning \"Your profile ($detected_profile) already mentions Volta and has not been changed.\"\n    fi\n  fi\n\n  if command grep -qc 'NOTION_HOME' \"$detected_profile\"; then\n    eprintf ''\n    warning \"Your profile ($detected_profile) mentions Notion.\"\n    eprintf \"         You probably want to remove that.\"\n    eprintf ''\n  fi\n}\n\nlegacy_dir() {\n  echo \"${NOTION_HOME:-\"$HOME/.notion\"}\"\n}\n\n# Check for a legacy installation from when the tool was named Notion.\nno_legacy_install() {\n  if [ -d \"$(legacy_dir)\" ]; then\n    eprintf \"\"\n    error \"You have an existing Notion install, which can't be automatically upgraded to Volta.\"\n    request \"       Please delete $(legacy_dir) and try again.\"\n    eprintf \"\"\n    eprintf \"(We plan to implement automatic upgrades in the future. Thanks for bearing with us!)\"\n    eprintf \"\"\n    return 1\n  fi\n  return 0\n}\n\n# Check if it is OK to upgrade to the new version\nupgrade_is_ok() {\n  local will_install_version=\"$1\"\n  local install_dir=\"$2\"\n  local is_dev_install=\"$3\"\n\n  local volta_bin=\"$install_dir/volta\"\n\n  # this is not able to install Volta prior to 0.5.0 (when it was renamed)\n  if [[ \"$will_install_version\" =~ ^([0-9]+\\.[0-9]+) ]]; then\n    local major_minor=\"${BASH_REMATCH[1]}\"\n    case \"$major_minor\" in\n      0.1|0.2|0.3|0.4)\n        eprintf \"\"\n        error \"Cannot install Volta prior to version 0.5.0 (when it was named Notion)\"\n        request \"    To install Notion version $will_install_version, please check out the source and build manually.\"\n        eprintf \"\"\n        return 1\n        ;;\n    esac\n  fi\n\n  if [[ -n \"$install_dir\" && -x \"$volta_bin\" ]]; then\n    local prev_version=\"$( ($volta_bin --version 2>/dev/null || echo 0.1) | sed -E 's/^.*([0-9]+\\.[0-9]+\\.[0-9]+).*$/\\1/')\"\n    # if this is a local dev install, skip the equality check\n    # if installing the same version, this is a no-op\n    if [ \"$is_dev_install\" != \"true\" ] && [ \"$prev_version\" == \"$will_install_version\" ]; then\n      eprintf \"Version $will_install_version already installed\"\n      return 1\n    fi\n    # in the future, check $prev_version for incompatible upgrades\n  fi\n  return 0\n}\n\n# returns the os name to be used in the packaged release,\n# including the openssl info if necessary\nparse_os_info() {\n  local uname_str=\"$1\"\n  local openssl_version=\"$2\"\n\n  case \"$uname_str\" in\n    Linux)\n      parsed_version=\"$(parse_openssl_version \"$openssl_version\")\"\n      exit_code=\"$?\"\n      if [ \"$exit_code\" != 0 ]; then\n        return \"$exit_code\"\n      fi\n\n      echo \"linux-openssl-$parsed_version\"\n      ;;\n    Darwin)\n      echo \"macos\"\n      ;;\n    *)\n      return 1\n  esac\n  return 0\n}\n\nparse_os_pretty() {\n  local uname_str=\"$1\"\n\n  case \"$uname_str\" in\n    Linux)\n      echo \"Linux\"\n      ;;\n    Darwin)\n      echo \"macOS\"\n      ;;\n    *)\n      echo \"$uname_str\"\n  esac\n}\n\n# return true(0) if the element is contained in the input arguments\n# called like:\n#  if element_in \"foo\" \"${array[@]}\"; then ...\nelement_in() {\n  local match=\"$1\";\n  shift\n\n  local element;\n  # loop over the input arguments and return when a match is found\n  for element in \"$@\"; do\n    [ \"$element\" == \"$match\" ] && return 0\n  done\n  return 1\n}\n\n# parse the OpenSSL version from the input text\n# for most distros, we only care about MAJOR.MINOR, with the exception of RHEL/CENTOS,\nparse_openssl_version() {\n  local version_str=\"$1\"\n\n  # array containing the SSL libraries that are supported\n  # would be nice to use a bash 4.x associative array, but bash 3.x is the default on OSX\n  SUPPORTED_SSL_LIBS=( 'OpenSSL' )\n\n  # use regex to get the library name and version\n  # typical version string looks like 'OpenSSL 1.0.1e-fips 11 Feb 2013'\n  if [[ \"$version_str\" =~ ^([^\\ ]*)\\ ([0-9]+\\.[0-9]+) ]]\n  then\n    # check that the lib is supported\n    libname=\"${BASH_REMATCH[1]}\"\n    major_minor=\"${BASH_REMATCH[2]}\"\n    if ! element_in \"$libname\" \"${SUPPORTED_SSL_LIBS[@]}\"\n    then\n      error \"Releases for '$libname' not currently supported. Supported libraries are: ${SUPPORTED_SSL_LIBS[@]}.\"\n      return 1\n    fi\n\n    # for version 1.0.x, check for RHEL/CentOS style OpenSSL SONAME (.so.10)\n    if [ \"$major_minor\" == \"1.0\" ] && [ -f \"/usr/lib64/libcrypto.so.10\" ]; then\n      echo \"rhel\"\n    else\n      echo \"$major_minor\"\n    fi\n    return 0\n  else\n    error \"Could not determine OpenSSL version for '$version_str'.\"\n    return 1\n  fi\n}\n\ncreate_tree() {\n  local install_dir=\"$1\"\n\n  info 'Creating' \"directory layout\"\n\n  # .volta/\n  #     bin/\n  #     cache/\n  #         node/\n  #     log/\n  #     tmp/\n  #     tools/\n  #         image/\n  #             node/\n  #             packages/\n  #             yarn/\n  #         inventory/\n  #             node/\n  #             packages/\n  #             yarn/\n  #         user/\n\n  mkdir -p \"$install_dir\"\n  mkdir -p \"$install_dir\"/bin\n  mkdir -p \"$install_dir\"/cache/node\n  mkdir -p \"$install_dir\"/log\n  mkdir -p \"$install_dir\"/tmp\n  mkdir -p \"$install_dir\"/tools/image/{node,packages,yarn}\n  mkdir -p \"$install_dir\"/tools/inventory/{node,packages,yarn}\n  mkdir -p \"$install_dir\"/tools/user\n}\n\ninstall_version() {\n  local version_to_install=\"$1\"\n  local install_dir=\"$2\"\n\n  if ! volta_home_is_ok; then\n    exit 1\n  fi\n\n  case \"$version_to_install\" in\n    latest)\n      local latest_version=\"$(get_latest_release)\"\n      info 'Installing' \"latest version of Volta ($latest_version)\"\n      install_release \"$latest_version\" \"$install_dir\"\n      ;;\n    *)\n      # assume anything else is a specific version\n      info 'Installing' \"Volta version $version_to_install\"\n      install_release \"$version_to_install\" \"$install_dir\"\n      ;;\n  esac\n\n  if [ \"$?\" == 0 ]\n  then\n    create_symlinks \"$install_dir\" &&\n      update_profile \"$install_dir\" &&\n      info \"Finished\" 'installation. Open a new terminal to start using Volta!'\n  fi\n}\n\ninstall_release() {\n  local version=\"$1\"\n  local install_dir=\"$2\"\n  local is_dev_install=\"false\"\n\n  info 'Checking' \"for existing Volta installation\"\n  if no_legacy_install && upgrade_is_ok \"$version\" \"$install_dir\" \"$is_dev_install\"\n  then\n    download_archive=\"$(download_release \"$version\"; exit \"$?\")\"\n    exit_status=\"$?\"\n    if [ \"$exit_status\" != 0 ]\n    then\n      error \"Could not download Volta version '$version'. See $(release_url) for a list of available releases\"\n      return \"$exit_status\"\n    fi\n\n    install_from_file \"$download_archive\" \"$install_dir\"\n  else\n    # existing legacy install, or upgrade problem\n    return 1\n  fi\n}\n\ndownload_release() {\n  local version=\"$1\"\n\n  local uname_str=\"$(uname -s)\"\n  local openssl_version=\"$(openssl version)\"\n  local os_info\n  os_info=\"$(parse_os_info \"$uname_str\" \"$openssl_version\")\"\n  if [ \"$?\" != 0 ]; then\n    error \"The current operating system ($uname_str) does not appear to be supported by Volta.\"\n    return 1\n  fi\n  local pretty_os_name=\"$(parse_os_pretty \"$uname_str\")\"\n\n  info 'Fetching' \"archive for $pretty_os_name, version $version\"\n  # store the downloaded archive in a temporary directory\n  local download_dir=\"$(mktemp -d)\"\n  download_release_from_repo \"$version\" \"$os_info\" \"$download_dir\"\n}\n\ninstall_from_file() {\n  local archive=\"$1\"\n  local extract_to=\"$2\"\n\n  create_tree \"$extract_to\"\n\n  info 'Extracting' \"Volta binaries and launchers\"\n  # extract the files to the specified directory\n  tar -xzvf \"$archive\" -C \"$extract_to\"\n}\n\ncheck_architecture() {\n  local version=\"$1\"\n  local arch=\"$2\"\n\n  if [[ \"$version\" != \"local\"* ]]; then\n    if [ \"$arch\" != \"x86_64\" ]; then\n      error \"Sorry! Volta currently only provides pre-built binaries for x86_64 architectures.\"\n      return 1\n    fi\n  fi\n}\n\n\n# return if sourced (for testing the functions above)\nreturn 0 2>/dev/null\n\n# default to installing the latest available version\nversion_to_install=\"latest\"\n\n# install to VOLTA_HOME, defaulting to ~/.volta\ninstall_dir=\"${VOLTA_HOME:-\"$HOME/.volta\"}\"\n\n# parse command line options\nwhile [ $# -gt 0 ]\ndo\n  arg=\"$1\"\n\n  case \"$arg\" in\n    -h|--help)\n      usage\n      exit 0\n      ;;\n    --version)\n      shift # shift off the argument\n      version_to_install=\"$1\"\n      shift # shift off the value\n      ;;\n    *)\n      error \"unknown option: '$arg'\"\n      usage\n      exit 1\n      ;;\n  esac\ndone\n\ncheck_architecture \"$version_to_install\" \"$(uname -m)\" || exit 1\n\ninstall_version \"$version_to_install\" \"$install_dir\"\n"
  },
  {
    "path": "dev/unix/volta-install.sh",
    "content": "#!/usr/bin/env bash\n\n# This is the bootstrap Unix installer served by `https://get.volta.sh`.\n# Its responsibility is to query the system to determine what OS the system\n# has, fetch and install the appropriate build of Volta, and modify the user's\n# profile.\n\n# NOTE: to use an internal company repo, change how this determines the latest version\nget_latest_release() {\n  curl --silent \"https://volta.sh/latest-version\"\n}\n\nrelease_url() {\n  echo \"https://github.com/volta-cli/volta/releases\"\n}\n\ndownload_release_from_repo() {\n  local version=\"$1\"\n  local os_info=\"$2\"\n  local tmpdir=\"$3\"\n\n  local filename=\"volta-$version-$os_info.tar.gz\"\n  local download_file=\"$tmpdir/$filename\"\n  local archive_url=\"$(release_url)/download/v$version/$filename\"\n\n  curl --progress-bar --show-error --location --fail \"$archive_url\" --output \"$download_file\" --write-out \"$download_file\"\n}\n\nusage() {\n    cat >&2 <<END_USAGE\nvolta-install: The installer for Volta\n\nUSAGE:\n    volta-install [FLAGS] [OPTIONS]\n\nFLAGS:\n    -h, --help                  Prints help information\n\nOPTIONS:\n        --dev                   Compile and install Volta locally, using the dev target\n        --release               Compile and install Volta locally, using the release target\n        --skip-setup            Do not run 'volta setup' to modify startup scripts\n        --version <version>     Install a specific release version of Volta\nEND_USAGE\n}\n\ninfo() {\n  local action=\"$1\"\n  local details=\"$2\"\n  command printf '\\033[1;32m%12s\\033[0m %s\\n' \"$action\" \"$details\" 1>&2\n}\n\nerror() {\n  command printf '\\033[1;31mError\\033[0m: %s\\n\\n' \"$1\" 1>&2\n}\n\nwarning() {\n  command printf '\\033[1;33mWarning\\033[0m: %s\\n\\n' \"$1\" 1>&2\n}\n\nrequest() {\n  command printf '\\033[1m%s\\033[0m\\n' \"$1\" 1>&2\n}\n\neprintf() {\n  command printf '%s\\n' \"$1\" 1>&2\n}\n\nbold() {\n  command printf '\\033[1m%s\\033[0m' \"$1\"\n}\n\n# check for issue with VOLTA_HOME\n# if it is set, and exists, but is not a directory, the install will fail\nvolta_home_is_ok() {\n  if [ -n \"${VOLTA_HOME-}\" ] && [ -e \"$VOLTA_HOME\" ] && ! [ -d \"$VOLTA_HOME\" ]; then\n    error \"\\$VOLTA_HOME is set but is not a directory ($VOLTA_HOME).\"\n    eprintf \"Please check your profile scripts and environment.\"\n    return 1\n  fi\n  return 0\n}\n\n# Check if it is OK to upgrade to the new version\nupgrade_is_ok() {\n  local will_install_version=\"$1\"\n  local install_dir=\"$2\"\n  local is_dev_install=\"$3\"\n\n  # check for Volta in both the old location and the new 0.7.0 location\n  local volta_bin=\"$install_dir/volta\"\n  if [ ! -x \"$volta_bin\" ]; then\n    volta_bin=\"$install_dir/bin/volta\"\n  fi\n\n  # this is not able to install Volta prior to 0.5.0 (when it was renamed)\n  if [[ \"$will_install_version\" =~ ^([0-9]+\\.[0-9]+) ]]; then\n    local major_minor=\"${BASH_REMATCH[1]}\"\n    case \"$major_minor\" in\n      0.1|0.2|0.3|0.4|0.5)\n        eprintf \"\"\n        error \"Cannot install Volta prior to version 0.6.0\"\n        request \"    To install Volta version $will_install_version, please check out the source and build manually.\"\n        eprintf \"\"\n        return 1\n        ;;\n    esac\n  fi\n\n  if [[ -n \"$install_dir\" && -x \"$volta_bin\" ]]; then\n    local prev_version=\"$( ($volta_bin --version 2>/dev/null || echo 0.1) | sed -E 's/^.*([0-9]+\\.[0-9]+\\.[0-9]+).*$/\\1/')\"\n    # if this is a local dev install, skip the equality check\n    # if installing the same version, this is a no-op\n    if [ \"$is_dev_install\" != \"true\" ] && [ \"$prev_version\" == \"$will_install_version\" ]; then\n      eprintf \"Version $will_install_version already installed\"\n      return 1\n    fi\n    # in the future, check $prev_version for incompatible upgrades\n  fi\n  return 0\n}\n\n# returns the os name to be used in the packaged release\nparse_os_info() {\n  local uname_str=\"$1\"\n  local arch=\"$(uname -m)\"\n\n  case \"$uname_str\" in\n    Linux)\n      if [ \"$arch\" == \"x86_64\" ]; then\n        echo \"linux\"\n      elif [ \"$arch\" == \"aarch64\" ]; then\n        echo \"linux-arm\"\n      else\n        error \"Releases for architectures other than x64 and arm are not currently supported.\"\n        return 1\n      fi\n      ;;\n    Darwin)\n      echo \"macos\"\n      ;;\n    *)\n      return 1\n  esac\n  return 0\n}\n\nparse_os_pretty() {\n  local uname_str=\"$1\"\n\n  case \"$uname_str\" in\n    Linux)\n      echo \"Linux\"\n      ;;\n    Darwin)\n      echo \"macOS\"\n      ;;\n    *)\n      echo \"$uname_str\"\n  esac\n}\n\n# return true(0) if the element is contained in the input arguments\n# called like:\n#  if element_in \"foo\" \"${array[@]}\"; then ...\nelement_in() {\n  local match=\"$1\";\n  shift\n\n  local element;\n  # loop over the input arguments and return when a match is found\n  for element in \"$@\"; do\n    [ \"$element\" == \"$match\" ] && return 0\n  done\n  return 1\n}\n\ncreate_tree() {\n  local install_dir=\"$1\"\n\n  info 'Creating' \"directory layout\"\n\n  # .volta/\n  #     bin/\n\n  mkdir -p \"$install_dir\" && mkdir -p \"$install_dir\"/bin\n  if [ \"$?\" != 0 ]\n  then\n    error \"Could not create directory layout. Please make sure the target directory is writeable: $install_dir\"\n    exit 1\n  fi\n}\n\ninstall_version() {\n  local version_to_install=\"$1\"\n  local install_dir=\"$2\"\n  local should_run_setup=\"$3\"\n\n  if ! volta_home_is_ok; then\n    exit 1\n  fi\n\n  case \"$version_to_install\" in\n    latest)\n      local latest_version=\"$(get_latest_release)\"\n      info 'Installing' \"latest version of Volta ($latest_version)\"\n      install_release \"$latest_version\" \"$install_dir\"\n      ;;\n    local-dev)\n      info 'Installing' \"Volta locally after compiling\"\n      install_local \"dev\" \"$install_dir\"\n      ;;\n    local-release)\n      info 'Installing' \"Volta locally after compiling with '--release'\"\n      install_local \"release\" \"$install_dir\"\n      ;;\n    *)\n      # assume anything else is a specific version\n      info 'Installing' \"Volta version $version_to_install\"\n      install_release \"$version_to_install\" \"$install_dir\"\n      ;;\n  esac\n\n  if [ \"$?\" == 0 ]\n  then\n      if [ \"$should_run_setup\" == \"true\" ]; then\n        info 'Finished' \"installation. Updating user profile settings.\"\n        \"$install_dir\"/bin/volta setup\n      else\n        \"$install_dir\"/bin/volta --version &>/dev/null # creates the default shims\n        info 'Finished' \"installation. No changes were made to user profile settings.\"\n      fi\n  fi\n}\n\n# parse the 'version = \"X.Y.Z\"' line from the input Cargo.toml contents\n# and return the version string\nparse_cargo_version() {\n  local contents=\"$1\"\n\n  while read -r line\n  do\n    if [[ \"$line\" =~ ^version\\ =\\ \\\"(.*)\\\" ]]\n    then\n      echo \"${BASH_REMATCH[1]}\"\n      return 0\n    fi\n  done <<< \"$contents\"\n\n  error \"Could not determine the current version from Cargo.toml\"\n  return 1\n}\n\ninstall_release() {\n  local version=\"$1\"\n  local install_dir=\"$2\"\n  local is_dev_install=\"false\"\n\n  info 'Checking' \"for existing Volta installation\"\n  if upgrade_is_ok \"$version\" \"$install_dir\" \"$is_dev_install\"\n  then\n    download_archive=\"$(download_release \"$version\"; exit \"$?\")\"\n    exit_status=\"$?\"\n    if [ \"$exit_status\" != 0 ]\n    then\n      error \"Could not download Volta version '$version'. See $(release_url) for a list of available releases\"\n      return \"$exit_status\"\n    fi\n\n    install_from_file \"$download_archive\" \"$install_dir\"\n  else\n    # existing legacy install, or upgrade problem\n    return 1\n  fi\n}\n\ninstall_local() {\n  local dev_or_release=\"$1\"\n  local install_dir=\"$2\"\n  # this is a local install, so skip the version equality check\n  local is_dev_install=\"true\"\n\n  info 'Checking' \"for existing Volta installation\"\n  install_version=\"$(parse_cargo_version \"$(<Cargo.toml)\" )\" || return 1\n  if upgrade_is_ok \"$install_version\" \"$install_dir\" \"$is_dev_install\"\n  then\n    # compile and package the binaries, then install from that local archive\n    compiled_archive=\"$(compile_and_package \"$dev_or_release\")\" &&\n      install_from_file \"$compiled_archive\" \"$install_dir\"\n  else\n    # existing legacy install, or upgrade problem\n    return 1\n  fi\n}\n\ncompile_and_package() {\n  local dev_or_release=\"$1\"\n\n  local release_output\n\n  # get the directory of this script\n  # (from https://stackoverflow.com/a/246128)\n  DIR=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" >/dev/null 2>&1 && pwd )\"\n\n  # call the release script to create the packaged archive file\n  # '2> >(tee /dev/stderr)' copies stderr to stdout, to collect it and parse the filename\n  release_output=\"$( \"$DIR/release.sh\" \"--$dev_or_release\" 2> >(tee /dev/stderr) )\"\n  [ \"$?\" != 0 ] && return 1\n\n  # parse the release filename and return that\n  if [[ \"$release_output\" =~ release\\ in\\ file\\ (target[^\\ ]+) ]]; then\n    echo \"${BASH_REMATCH[1]}\"\n  else\n    error \"Could not determine output filename\"\n    return 1\n  fi\n}\n\ndownload_release() {\n  local version=\"$1\"\n\n  local uname_str=\"$(uname -s)\"\n  local os_info\n  os_info=\"$(parse_os_info \"$uname_str\")\"\n  if [ \"$?\" != 0 ]; then\n    error \"The current operating system ($uname_str) does not appear to be supported by Volta.\"\n    return 1\n  fi\n  local pretty_os_name=\"$(parse_os_pretty \"$uname_str\")\"\n\n  info 'Fetching' \"archive for $pretty_os_name, version $version\"\n  # store the downloaded archive in a temporary directory\n  local download_dir=\"$(mktemp -d)\"\n  download_release_from_repo \"$version\" \"$os_info\" \"$download_dir\"\n}\n\ninstall_from_file() {\n  local archive=\"$1\"\n  local install_dir=\"$2\"\n\n  create_tree \"$install_dir\"\n\n  info 'Extracting' \"Volta binaries and launchers\"\n  # extract the files to the specified directory\n  tar -xf \"$archive\" -C \"$install_dir\"/bin\n}\n\n# return if sourced (for testing the functions above)\nreturn 0 2>/dev/null\n\n# default to installing the latest available version\nversion_to_install=\"latest\"\n\n# default to running setup after installing\nshould_run_setup=\"true\"\n\n# install to VOLTA_HOME, defaulting to ~/.volta\ninstall_dir=\"${VOLTA_HOME:-\"$HOME/.volta\"}\"\n\n# parse command line options\nwhile [ $# -gt 0 ]\ndo\n  arg=\"$1\"\n\n  case \"$arg\" in\n    -h|--help)\n      usage\n      exit 0\n      ;;\n    --dev)\n      shift # shift off the argument\n      version_to_install=\"local-dev\"\n      ;;\n    --release)\n      shift # shift off the argument\n      version_to_install=\"local-release\"\n      ;;\n    --version)\n      shift # shift off the argument\n      version_to_install=\"$1\"\n      shift # shift off the value\n      ;;\n    --skip-setup)\n      shift # shift off the argument\n      should_run_setup=\"false\"\n      ;;\n    *)\n      error \"unknown option: '$arg'\"\n      usage\n      exit 1\n      ;;\n  esac\ndone\n\ninstall_version \"$version_to_install\" \"$install_dir\" \"$should_run_setup\"\n"
  },
  {
    "path": "rust-toolchain.toml",
    "content": "[toolchain]\nchannel = \"1.75\"\ncomponents = [\"clippy\", \"rustfmt\"]\nprofile = \"minimal\"\n"
  },
  {
    "path": "src/cli.rs",
    "content": "use clap::{builder::styling, ColorChoice, Parser};\n\nuse crate::command::{self, Command};\nuse volta_core::error::{ExitCode, Fallible};\nuse volta_core::session::Session;\nuse volta_core::style::{text_width, MAX_WIDTH};\n\n#[derive(Parser)]\n#[command(\n    about = \"The JavaScript Launcher ⚡\",\n    long_about = \"The JavaScript Launcher ⚡\n\n    To install a tool in your toolchain, use `volta install`.\n    To pin your project's runtime or package manager, use `volta pin`.\",\n    color = ColorChoice::Auto,\n    disable_version_flag = true,\n    styles = styles(),\n    term_width = text_width().unwrap_or(MAX_WIDTH),\n)]\npub(crate) struct Volta {\n    #[command(subcommand)]\n    pub(crate) command: Option<Subcommand>,\n\n    /// Enables verbose diagnostics\n    #[arg(long, global = true)]\n    pub(crate) verbose: bool,\n\n    /// Enables trace-level diagnostics.\n    #[arg(long, global = true, requires = \"verbose\")]\n    pub(crate) very_verbose: bool,\n\n    /// Prevents unnecessary output\n    #[arg(\n        long,\n        global = true,\n        conflicts_with = \"verbose\",\n        aliases = &[\"silent\"]\n    )]\n    pub(crate) quiet: bool,\n\n    /// Prints the current version of Volta\n    #[arg(short, long)]\n    pub(crate) version: bool,\n}\n\nimpl Volta {\n    pub(crate) fn run(self, session: &mut Session) -> Fallible<ExitCode> {\n        if self.version {\n            // suffix indicator for dev build\n            if cfg!(debug_assertions) {\n                println!(\"{}-dev\", env!(\"CARGO_PKG_VERSION\"));\n            } else {\n                println!(\"{}\", env!(\"CARGO_PKG_VERSION\"));\n            }\n            Ok(ExitCode::Success)\n        } else if let Some(command) = self.command {\n            command.run(session)\n        } else {\n            Volta::parse_from([\"volta\", \"help\"].iter()).run(session)\n        }\n    }\n}\n\n#[derive(clap::Subcommand)]\npub(crate) enum Subcommand {\n    /// Fetches a tool to the local machine\n    Fetch(command::Fetch),\n\n    /// Installs a tool in your toolchain\n    Install(command::Install),\n\n    /// Uninstalls a tool from your toolchain\n    Uninstall(command::Uninstall),\n\n    /// Pins your project's runtime or package manager\n    Pin(command::Pin),\n\n    /// Displays the current toolchain\n    #[command(alias = \"ls\")]\n    List(command::List),\n\n    /// Generates Volta completions\n    ///\n    /// By default, completions will be generated for the value of your current shell,\n    /// shell, i.e. the value of `SHELL`. If you set the `<shell>` option, completions\n    /// will be generated for that shell instead.\n    ///\n    /// If you specify a directory, the completions will be written to a file there;\n    /// otherwise, they will be written to `stdout`.\n    #[command(arg_required_else_help = true)]\n    Completions(command::Completions),\n\n    /// Locates the actual binary that will be called by Volta\n    Which(command::Which),\n\n    #[command(long_about = crate::command::r#use::USAGE, hide = true)]\n    Use(command::Use),\n\n    /// Enables Volta for the current user / shell\n    Setup(command::Setup),\n\n    /// Run a command with custom Node, npm, pnpm, and/or Yarn versions\n    Run(command::Run),\n}\n\nimpl Subcommand {\n    pub(crate) fn run(self, session: &mut Session) -> Fallible<ExitCode> {\n        match self {\n            Subcommand::Fetch(fetch) => fetch.run(session),\n            Subcommand::Install(install) => install.run(session),\n            Subcommand::Uninstall(uninstall) => uninstall.run(session),\n            Subcommand::Pin(pin) => pin.run(session),\n            Subcommand::List(list) => list.run(session),\n            Subcommand::Completions(completions) => completions.run(session),\n            Subcommand::Which(which) => which.run(session),\n            Subcommand::Use(r#use) => r#use.run(session),\n            Subcommand::Setup(setup) => setup.run(session),\n            Subcommand::Run(run) => run.run(session),\n        }\n    }\n}\n\nfn styles() -> styling::Styles {\n    styling::Styles::plain()\n        .header(\n            styling::AnsiColor::Yellow.on_default()\n                | styling::Effects::BOLD\n                | styling::Effects::ITALIC,\n        )\n        .usage(\n            styling::AnsiColor::Yellow.on_default()\n                | styling::Effects::BOLD\n                | styling::Effects::ITALIC,\n        )\n        .literal(styling::AnsiColor::Green.on_default() | styling::Effects::BOLD)\n        .placeholder(styling::AnsiColor::BrightBlue.on_default())\n}\n"
  },
  {
    "path": "src/command/completions.rs",
    "content": "use std::path::PathBuf;\n\nuse clap::CommandFactory;\nuse clap_complete::Shell;\nuse log::info;\n\nuse volta_core::{\n    error::{Context, ErrorKind, ExitCode, Fallible},\n    session::{ActivityKind, Session},\n    style::{note_prefix, success_prefix},\n};\n\nuse crate::command::Command;\n\n#[derive(Debug, clap::Args)]\npub(crate) struct Completions {\n    /// Shell to generate completions for\n    #[arg(index = 1, ignore_case = true, required = true)]\n    shell: Shell,\n\n    /// File to write generated completions to\n    #[arg(short, long = \"output\")]\n    out_file: Option<PathBuf>,\n\n    /// Write over an existing file, if any.\n    #[arg(short, long)]\n    force: bool,\n}\n\nimpl Command for Completions {\n    fn run(self, session: &mut Session) -> Fallible<ExitCode> {\n        session.add_event_start(ActivityKind::Completions);\n\n        let mut app = crate::cli::Volta::command();\n        let app_name = app.get_name().to_owned();\n        match self.out_file {\n            Some(path) => {\n                if path.is_file() && !self.force {\n                    return Err(ErrorKind::CompletionsOutFileError { path }.into());\n                }\n\n                // The user may have passed a path that does not yet exist. If\n                // so, we create it, informing the user we have done so.\n                if let Some(parent) = path.parent() {\n                    if !parent.is_dir() {\n                        info!(\n                            \"{} {} does not exist, creating it\",\n                            note_prefix(),\n                            parent.display()\n                        );\n                        std::fs::create_dir_all(parent).with_context(|| {\n                            ErrorKind::CreateDirError {\n                                dir: parent.to_path_buf(),\n                            }\n                        })?;\n                    }\n                }\n\n                let mut file = &std::fs::File::create(&path).with_context(|| {\n                    ErrorKind::CompletionsOutFileError {\n                        path: path.to_path_buf(),\n                    }\n                })?;\n\n                clap_complete::generate(self.shell, &mut app, app_name, &mut file);\n\n                info!(\n                    \"{} installed completions to {}\",\n                    success_prefix(),\n                    path.display()\n                );\n            }\n            None => clap_complete::generate(self.shell, &mut app, app_name, &mut std::io::stdout()),\n        };\n\n        session.add_event_end(ActivityKind::Completions, ExitCode::Success);\n        Ok(ExitCode::Success)\n    }\n}\n"
  },
  {
    "path": "src/command/fetch.rs",
    "content": "use volta_core::error::{ExitCode, Fallible};\nuse volta_core::session::{ActivityKind, Session};\nuse volta_core::tool;\n\nuse crate::command::Command;\n\n#[derive(clap::Args)]\npub(crate) struct Fetch {\n    /// Tools to fetch, like `node`, `yarn@latest` or `your-package@^14.4.3`.\n    #[arg(value_name = \"tool[@version]\", required = true)]\n    tools: Vec<String>,\n}\n\nimpl Command for Fetch {\n    fn run(self, session: &mut Session) -> Fallible<ExitCode> {\n        session.add_event_start(ActivityKind::Fetch);\n\n        for tool in tool::Spec::from_strings(&self.tools, \"fetch\")? {\n            tool.resolve(session)?.fetch(session)?;\n        }\n\n        session.add_event_end(ActivityKind::Fetch, ExitCode::Success);\n        Ok(ExitCode::Success)\n    }\n}\n"
  },
  {
    "path": "src/command/install.rs",
    "content": "use volta_core::error::{ExitCode, Fallible};\nuse volta_core::session::{ActivityKind, Session};\nuse volta_core::tool::Spec;\n\nuse crate::command::Command;\n\n#[derive(clap::Args)]\npub(crate) struct Install {\n    /// Tools to install, like `node`, `yarn@latest` or `your-package@^14.4.3`.\n    #[arg(value_name = \"tool[@version]\", required = true)]\n    tools: Vec<String>,\n}\n\nimpl Command for Install {\n    fn run(self, session: &mut Session) -> Fallible<ExitCode> {\n        session.add_event_start(ActivityKind::Install);\n\n        for tool in Spec::from_strings(&self.tools, \"install\")? {\n            tool.resolve(session)?.install(session)?;\n        }\n\n        session.add_event_end(ActivityKind::Install, ExitCode::Success);\n        Ok(ExitCode::Success)\n    }\n}\n"
  },
  {
    "path": "src/command/list/human.rs",
    "content": "//! Define the \"human\" format style for list commands.\n\nuse std::collections::BTreeMap;\n\nuse super::{Node, Package, PackageManager, PackageManagerKind, Toolchain};\nuse once_cell::sync::Lazy;\nuse textwrap::{fill, Options};\nuse volta_core::style::{text_width, tool_version, MAX_WIDTH};\n\nstatic INDENTATION: &str = \"    \";\nstatic NO_RUNTIME: &str = \"⚡️ No Node runtimes installed!\n\n    You can install a runtime by running `volta install node`. See `volta help install` for\n    details and more options.\";\n\nstatic TEXT_WIDTH: Lazy<usize> = Lazy::new(|| text_width().unwrap_or(MAX_WIDTH));\n\n#[allow(clippy::unnecessary_wraps)] // Needs to match the API of `plain::format`\npub(super) fn format(toolchain: &Toolchain) -> Option<String> {\n    // Formatting here depends on the toolchain: we do different degrees of\n    // indentation\n    Some(match toolchain {\n        Toolchain::Node(runtimes) => display_node(runtimes),\n        Toolchain::Active {\n            runtime,\n            package_managers,\n            packages,\n        } => display_active(runtime, package_managers, packages),\n        Toolchain::All {\n            runtimes,\n            package_managers,\n            packages,\n        } => display_all(runtimes, package_managers, packages),\n        Toolchain::PackageManagers { kind, managers } => display_package_managers(*kind, managers),\n        Toolchain::Packages(packages) => display_packages(packages),\n        Toolchain::Tool {\n            name,\n            host_packages,\n        } => display_tool(name, host_packages),\n    })\n}\n\n/// Format the output for `Toolchain::Active`.\n///\n/// Accepts the components *from* the toolchain rather than the item itself so\n/// that\nfn display_active(\n    runtime: &Option<Box<Node>>,\n    package_managers: &[PackageManager],\n    packages: &[Package],\n) -> String {\n    match runtime {\n        None => NO_RUNTIME.to_string(),\n        Some(node) => {\n            let runtime_version = wrap(format!(\"Node: {}\", format_runtime(node)));\n\n            let package_manager_versions = if package_managers.is_empty() {\n                String::new()\n            } else {\n                format!(\n                    \"\\n{}\",\n                    format_package_manager_list_condensed(package_managers)\n                )\n            };\n\n            let package_versions = if packages.is_empty() {\n                wrap(\"Tool binaries available: NONE\")\n            } else {\n                wrap(format!(\n                    \"Tool binaries available:\\n{}\",\n                    format_tool_list(packages)\n                ))\n            };\n\n            format!(\n                \"⚡️ Currently active tools:\\n\\n{}{}\\n{}\\n\\n{}\",\n                runtime_version,\n                package_manager_versions,\n                package_versions,\n                \"See options for more detailed reports by running `volta list --help`.\"\n            )\n        }\n    }\n}\n\n/// Format the output for `Toolchain::All`.\nfn display_all(\n    runtimes: &[Node],\n    package_managers: &[PackageManager],\n    packages: &[Package],\n) -> String {\n    if runtimes.is_empty() {\n        NO_RUNTIME.to_string()\n    } else {\n        let runtime_versions: String =\n            wrap(format!(\"Node runtimes:\\n{}\", format_runtime_list(runtimes)));\n        let package_manager_versions: String = wrap(format!(\n            \"Package managers:\\n{}\",\n            format_package_manager_list_verbose(package_managers)\n        ));\n        let package_versions = wrap(format!(\"Packages:\\n{}\", format_package_list(packages)));\n        format!(\n            \"⚡️ User toolchain:\\n\\n{}\\n\\n{}\\n\\n{}\",\n            runtime_versions, package_manager_versions, package_versions\n        )\n    }\n}\n\n/// Format a set of `Toolchain::Node`s.\nfn display_node(runtimes: &[Node]) -> String {\n    if runtimes.is_empty() {\n        NO_RUNTIME.to_string()\n    } else {\n        format!(\n            \"⚡️ Node runtimes in your toolchain:\\n\\n{}\",\n            format_runtime_list(runtimes)\n        )\n    }\n}\n\n/// Format a set of `Toolchain::PackageManager`s for `volta list npm`\nfn display_npms(managers: &[PackageManager]) -> String {\n    if managers.is_empty() {\n        \"⚡️ No custom npm versions installed (npm is still available bundled with Node).\n\nYou can install an npm version by running `volta install npm`.\nSee `volta help install` for details and more options.\"\n            .into()\n    } else {\n        let versions = wrap(\n            managers\n                .iter()\n                .map(format_package_manager)\n                .collect::<Vec<_>>()\n                .join(\"\\n\"),\n        );\n        format!(\"⚡️ Custom npm versions in your toolchain:\\n\\n{}\", versions)\n    }\n}\n\n/// Format a set of `Toolchain::PackageManager`s.\nfn display_package_managers(kind: PackageManagerKind, managers: &[PackageManager]) -> String {\n    match kind {\n        PackageManagerKind::Npm => display_npms(managers),\n        _ => {\n            if managers.is_empty() {\n                // Note: Using `format_package_manager_kind` to get the properly capitalized version of the tool\n                // Then using the `Display` impl on the kind to get the version to show in the command\n                format!(\n                    \"⚡️ No {} versions installed.\n\nYou can install a {0} version by running `volta install {}`.\nSee `volta help install` for details and more options.\",\n                    format_package_manager_kind(kind),\n                    kind\n                )\n            } else {\n                let versions = wrap(\n                    managers\n                        .iter()\n                        .map(format_package_manager)\n                        .collect::<Vec<String>>()\n                        .join(\"\\n\"),\n                );\n                format!(\n                    \"⚡️ {} versions in your toolchain:\\n\\n{}\",\n                    format_package_manager_kind(kind),\n                    versions\n                )\n            }\n        }\n    }\n}\n\n/// Format a set of `Toolchain::Package`s and their associated tools.\nfn display_packages(packages: &[Package]) -> String {\n    if packages.is_empty() {\n        String::from(\n            \"⚡️ No tools or packages installed.\n\nYou can safely install packages by running `volta install <package name>`.\nSee `volta help install` for details and more options.\",\n        )\n    } else {\n        format!(\n            \"⚡️ Package versions in your toolchain:\\n\\n{}\",\n            format_package_list(packages)\n        )\n    }\n}\n\n/// Format a single `Toolchain::Tool` with associated `Toolchain::Package`\n\nfn display_tool(tool: &str, host_packages: &[Package]) -> String {\n    if host_packages.is_empty() {\n        format!(\n            \"⚡️ No tools or packages named `{}` installed.\n\nYou can safely install packages by running `volta install <package name>`.\nSee `volta help install` for details and more options.\",\n            tool\n        )\n    } else {\n        let versions = wrap(\n            host_packages\n                .iter()\n                .map(format_package)\n                .collect::<Vec<String>>()\n                .join(\"\\n\"),\n        );\n        format!(\"⚡️ Tool `{}` available from:\\n\\n{}\", tool, versions)\n    }\n}\n\n/// Format a list of `Toolchain::Package`s without detail information\nfn format_tool_list(packages: &[Package]) -> String {\n    packages\n        .iter()\n        .map(format_tool)\n        .collect::<Vec<String>>()\n        .join(\"\\n\")\n}\n/// Format a single `Toolchain::Package` without detail information\nfn format_tool(package: &Package) -> String {\n    match package {\n        Package::Default { tools, .. } | Package::Project { tools, .. } => {\n            let tools = match tools.len() {\n                0 => String::from(\"\"),\n                _ => tools.join(\", \"),\n            };\n            wrap(format!(\"{}{}\", tools, list_package_source(package)))\n        }\n        Package::Fetched(..) => String::new(),\n    }\n}\n\n/// format a list of `Toolchain::Node`s.\nfn format_runtime_list(runtimes: &[Node]) -> String {\n    wrap(\n        runtimes\n            .iter()\n            .map(format_runtime)\n            .collect::<Vec<String>>()\n            .join(\"\\n\"),\n    )\n}\n\n/// format a single version of `Toolchain::Node`.\nfn format_runtime(runtime: &Node) -> String {\n    format!(\"v{}{}\", runtime.version, runtime.source)\n}\n\n/// format a list of `Toolchain::PackageManager`s in condensed form\nfn format_package_manager_list_condensed(package_managers: &[PackageManager]) -> String {\n    wrap(\n        package_managers\n            .iter()\n            .map(|manager| {\n                format!(\n                    \"{}: {}\",\n                    format_package_manager_kind(manager.kind),\n                    format_package_manager(manager)\n                )\n            })\n            .collect::<Vec<_>>()\n            .join(\"\\n\"),\n    )\n}\n\n/// format a list of `Toolchain::PackageManager`s in verbose form\nfn format_package_manager_list_verbose(package_managers: &[PackageManager]) -> String {\n    let mut manager_lists = BTreeMap::new();\n\n    for manager in package_managers {\n        manager_lists\n            .entry(manager.kind)\n            .or_insert_with(Vec::new)\n            .push(format_package_manager(manager));\n    }\n\n    wrap(\n        manager_lists\n            .iter()\n            .map(|(kind, list)| {\n                format!(\n                    \"{}:\\n{}\",\n                    format_package_manager_kind(*kind),\n                    wrap(list.join(\"\\n\"))\n                )\n            })\n            .collect::<Vec<_>>()\n            .join(\"\\n\"),\n    )\n}\n\n/// format a single `Toolchain::PackageManager`.\nfn format_package_manager(package_manager: &PackageManager) -> String {\n    format!(\"v{}{}\", package_manager.version, package_manager.source)\n}\n\n/// format the title for a kind of package manager\n///\n/// This is distinct from the `Display` impl, because we need 'Yarn' to be capitalized for human output\nfn format_package_manager_kind(kind: PackageManagerKind) -> String {\n    match kind {\n        PackageManagerKind::Npm => \"npm\".into(),\n        PackageManagerKind::Pnpm => \"pnpm\".into(),\n        PackageManagerKind::Yarn => \"Yarn\".into(),\n    }\n}\n\n/// format a list of `Toolchain::Package`s and their associated tools.\nfn format_package_list(packages: &[Package]) -> String {\n    wrap(\n        packages\n            .iter()\n            .map(format_package)\n            .collect::<Vec<String>>()\n            .join(\"\\n\"),\n    )\n}\n\n/// Format a single `Toolchain::Package` and its associated tools.\nfn format_package(package: &Package) -> String {\n    match package {\n        Package::Default {\n            details,\n            node,\n            tools,\n            ..\n        } => {\n            let tools = match tools.len() {\n                0 => String::from(\"\"),\n                _ => tools.join(\", \"),\n            };\n\n            let version = format!(\"{}{}\", details.version, list_package_source(package));\n            let binaries = wrap(format!(\"binary tools: {}\", tools));\n            let platform_detail = wrap(format!(\n                \"runtime: {}\\npackage manager: {}\",\n                tool_version(\"node\", node),\n                // TODO: Should be updated when we support installing with custom package_managers,\n                // whether Yarn or non-built-in versions of npm\n                \"npm@built-in\"\n            ));\n            let platform = wrap(format!(\"platform:\\n{}\", platform_detail));\n            format!(\"{}@{}\\n{}\\n{}\", details.name, version, binaries, platform)\n        }\n        Package::Project { name, tools, .. } => {\n            let tools = match tools.len() {\n                0 => String::from(\"\"),\n                _ => tools.join(\", \"),\n            };\n\n            let binaries = wrap(format!(\"binary tools: {}\", tools));\n            format!(\"{}{}\\n{}\", name, list_package_source(package), binaries)\n        }\n        Package::Fetched(details) => {\n            let package_info = format!(\"{}@{}\", details.name, details.version);\n            let footer_message = format!(\n                \"To make it available to execute, run `volta install {}`.\",\n                package_info\n            );\n            format!(\"{}\\n\\n{}\", package_info, footer_message)\n        }\n    }\n}\n\n/// List a the source from a `Toolchain::Package`.\nfn list_package_source(package: &Package) -> String {\n    match package {\n        Package::Default { .. } => String::from(\" (default)\"),\n        Package::Project { path, .. } => format!(\" (current @ {})\", path.display()),\n        Package::Fetched(..) => String::new(),\n    }\n}\n\n/// Wrap and indent the output\nfn wrap<S>(text: S) -> String\nwhere\n    S: AsRef<str>,\n{\n    let options = Options::new(*TEXT_WIDTH)\n        .initial_indent(INDENTATION)\n        .subsequent_indent(INDENTATION);\n\n    let mut wrapped = fill(text.as_ref(), options);\n    // The `fill` method in the latest `textwrap` version does not trim the indentation whitespace\n    // from the final line.\n    wrapped.truncate(wrapped.trim_end_matches(INDENTATION).len());\n    wrapped\n}\n\n// These tests are organized by way of the *commands* supplied to `list`, unlike\n// in the `plain` module, because the formatting varies by command here, as it\n// does not there.\n#[cfg(test)]\nmod tests {\n    use std::path::PathBuf;\n\n    use node_semver::Version;\n    use once_cell::sync::Lazy;\n\n    use super::*;\n\n    static NODE_12: Lazy<Version> = Lazy::new(|| Version::from((12, 2, 0)));\n    static NODE_11: Lazy<Version> = Lazy::new(|| Version::from((11, 9, 0)));\n    static NODE_10: Lazy<Version> = Lazy::new(|| Version::from((10, 15, 3)));\n    static YARN_VERSION: Lazy<Version> = Lazy::new(|| Version::from((1, 16, 0)));\n    static NPM_VERSION: Lazy<Version> = Lazy::new(|| Version::from((6, 13, 1)));\n    static PROJECT_PATH: Lazy<PathBuf> = Lazy::new(|| PathBuf::from(\"~/path/to/project.json\"));\n\n    mod active {\n        use super::*;\n        use crate::command::list::{\n            human::display_active, Node, PackageDetails, PackageManager, PackageManagerKind, Source,\n        };\n\n        #[test]\n        fn no_runtimes() {\n            let runtime = None;\n            let package_managers = vec![];\n            let packages = vec![];\n            assert_eq!(\n                display_active(&runtime, &package_managers, &packages),\n                NO_RUNTIME\n            );\n        }\n\n        #[test]\n        fn runtime_only_default() {\n            let expected = \"⚡️ Currently active tools:\n\n    Node: v12.2.0 (default)\n    Tool binaries available: NONE\n\nSee options for more detailed reports by running `volta list --help`.\";\n\n            let runtime = Some(Box::new(Node {\n                source: Source::Default,\n                version: NODE_12.clone(),\n            }));\n            let package_managers = vec![];\n            let packages = vec![];\n\n            assert_eq!(\n                display_active(&runtime, &package_managers, &packages),\n                expected\n            );\n        }\n\n        #[test]\n        fn runtime_only_project() {\n            let expected = \"⚡️ Currently active tools:\n\n    Node: v12.2.0 (current @ ~/path/to/project.json)\n    Tool binaries available: NONE\n\nSee options for more detailed reports by running `volta list --help`.\";\n\n            let runtime = Some(Box::new(Node {\n                source: Source::Project(PROJECT_PATH.clone()),\n                version: NODE_12.clone(),\n            }));\n            let package_managers = vec![];\n            let packages = vec![];\n\n            assert_eq!(\n                display_active(&runtime, &package_managers, &packages),\n                expected\n            );\n        }\n\n        #[test]\n        fn runtime_and_npm_default() {\n            let expected = \"⚡️ Currently active tools:\n\n    Node: v12.2.0 (default)\n    npm: v6.13.1 (default)\n    Tool binaries available: NONE\n\nSee options for more detailed reports by running `volta list --help`.\";\n\n            let runtime = Some(Box::new(Node {\n                source: Source::Default,\n                version: NODE_12.clone(),\n            }));\n            let package_managers = vec![PackageManager {\n                kind: PackageManagerKind::Npm,\n                source: Source::Default,\n                version: NPM_VERSION.clone(),\n            }];\n            let packages = vec![];\n\n            assert_eq!(\n                display_active(&runtime, &package_managers, &packages),\n                expected\n            );\n        }\n\n        #[test]\n        fn runtime_and_yarn_default() {\n            let expected = \"⚡️ Currently active tools:\n\n    Node: v12.2.0 (default)\n    Yarn: v1.16.0 (default)\n    Tool binaries available: NONE\n\nSee options for more detailed reports by running `volta list --help`.\";\n\n            let runtime = Some(Box::new(Node {\n                source: Source::Default,\n                version: NODE_12.clone(),\n            }));\n            let package_managers = vec![PackageManager {\n                kind: PackageManagerKind::Yarn,\n                source: Source::Default,\n                version: YARN_VERSION.clone(),\n            }];\n            let packages = vec![];\n\n            assert_eq!(\n                display_active(&runtime, &package_managers, &packages),\n                expected\n            );\n        }\n\n        #[test]\n        fn runtime_and_npm_mixed() {\n            let expected = \"⚡️ Currently active tools:\n\n    Node: v12.2.0 (default)\n    npm: v6.13.1 (current @ ~/path/to/project.json)\n    Tool binaries available: NONE\n\nSee options for more detailed reports by running `volta list --help`.\";\n\n            let runtime = Some(Box::new(Node {\n                source: Source::Default,\n                version: NODE_12.clone(),\n            }));\n            let package_managers = vec![PackageManager {\n                kind: PackageManagerKind::Npm,\n                source: Source::Project(PROJECT_PATH.clone()),\n                version: NPM_VERSION.clone(),\n            }];\n            let packages = vec![];\n\n            assert_eq!(\n                display_active(&runtime, &package_managers, &packages),\n                expected\n            );\n        }\n\n        #[test]\n        fn runtime_and_yarn_mixed() {\n            let expected = \"⚡️ Currently active tools:\n\n    Node: v12.2.0 (default)\n    Yarn: v1.16.0 (current @ ~/path/to/project.json)\n    Tool binaries available: NONE\n\nSee options for more detailed reports by running `volta list --help`.\";\n\n            let runtime = Some(Box::new(Node {\n                source: Source::Default,\n                version: NODE_12.clone(),\n            }));\n            let package_managers = vec![PackageManager {\n                kind: PackageManagerKind::Yarn,\n                source: Source::Project(PROJECT_PATH.clone()),\n                version: YARN_VERSION.clone(),\n            }];\n            let packages = vec![];\n\n            assert_eq!(\n                display_active(&runtime, &package_managers, &packages),\n                expected\n            );\n        }\n\n        #[test]\n        fn runtime_and_npm_project() {\n            let expected = \"⚡️ Currently active tools:\n\n    Node: v12.2.0 (current @ ~/path/to/project.json)\n    npm: v6.13.1 (current @ ~/path/to/project.json)\n    Tool binaries available: NONE\n\nSee options for more detailed reports by running `volta list --help`.\";\n\n            let runtime = Some(Box::new(Node {\n                source: Source::Project(PROJECT_PATH.clone()),\n                version: NODE_12.clone(),\n            }));\n            let package_managers = vec![PackageManager {\n                kind: PackageManagerKind::Npm,\n                source: Source::Project(PROJECT_PATH.clone()),\n                version: NPM_VERSION.clone(),\n            }];\n            let packages = vec![];\n\n            assert_eq!(\n                display_active(&runtime, &package_managers, &packages),\n                expected\n            );\n        }\n\n        #[test]\n        fn runtime_and_yarn_project() {\n            let expected = \"⚡️ Currently active tools:\n\n    Node: v12.2.0 (current @ ~/path/to/project.json)\n    Yarn: v1.16.0 (current @ ~/path/to/project.json)\n    Tool binaries available: NONE\n\nSee options for more detailed reports by running `volta list --help`.\";\n\n            let runtime = Some(Box::new(Node {\n                source: Source::Project(PROJECT_PATH.clone()),\n                version: NODE_12.clone(),\n            }));\n            let package_managers = vec![PackageManager {\n                kind: PackageManagerKind::Yarn,\n                source: Source::Project(PROJECT_PATH.clone()),\n                version: YARN_VERSION.clone(),\n            }];\n            let packages = vec![];\n\n            assert_eq!(\n                display_active(&runtime, &package_managers, &packages),\n                expected\n            );\n        }\n\n        #[test]\n        fn runtime_npm_and_yarn_default() {\n            let expected = \"⚡️ Currently active tools:\n\n    Node: v12.2.0 (default)\n    npm: v6.13.1 (default)\n    Yarn: v1.16.0 (default)\n    Tool binaries available: NONE\n\nSee options for more detailed reports by running `volta list --help`.\";\n\n            let runtime = Some(Box::new(Node {\n                source: Source::Default,\n                version: NODE_12.clone(),\n            }));\n            let package_managers = vec![\n                PackageManager {\n                    kind: PackageManagerKind::Npm,\n                    source: Source::Default,\n                    version: NPM_VERSION.clone(),\n                },\n                PackageManager {\n                    kind: PackageManagerKind::Yarn,\n                    source: Source::Default,\n                    version: YARN_VERSION.clone(),\n                },\n            ];\n            let packages = vec![];\n\n            assert_eq!(\n                display_active(&runtime, &package_managers, &packages),\n                expected\n            );\n        }\n\n        #[test]\n        fn runtime_npm_and_yarn_project() {\n            let expected = \"⚡️ Currently active tools:\n\n    Node: v12.2.0 (current @ ~/path/to/project.json)\n    npm: v6.13.1 (current @ ~/path/to/project.json)\n    Yarn: v1.16.0 (current @ ~/path/to/project.json)\n    Tool binaries available: NONE\n\nSee options for more detailed reports by running `volta list --help`.\";\n\n            let runtime = Some(Box::new(Node {\n                source: Source::Project(PROJECT_PATH.clone()),\n                version: NODE_12.clone(),\n            }));\n            let package_managers = vec![\n                PackageManager {\n                    kind: PackageManagerKind::Npm,\n                    source: Source::Project(PROJECT_PATH.clone()),\n                    version: NPM_VERSION.clone(),\n                },\n                PackageManager {\n                    kind: PackageManagerKind::Yarn,\n                    source: Source::Project(PROJECT_PATH.clone()),\n                    version: YARN_VERSION.clone(),\n                },\n            ];\n            let packages = vec![];\n\n            assert_eq!(\n                display_active(&runtime, &package_managers, &packages),\n                expected\n            );\n        }\n\n        #[test]\n        fn runtime_npm_and_yarn_mixed() {\n            let expected = \"⚡️ Currently active tools:\n\n    Node: v12.2.0 (default)\n    npm: v6.13.1 (current @ ~/path/to/project.json)\n    Yarn: v1.16.0 (default)\n    Tool binaries available: NONE\n\nSee options for more detailed reports by running `volta list --help`.\";\n\n            let runtime = Some(Box::new(Node {\n                source: Source::Default,\n                version: NODE_12.clone(),\n            }));\n            let package_managers = vec![\n                PackageManager {\n                    kind: PackageManagerKind::Npm,\n                    source: Source::Project(PROJECT_PATH.clone()),\n                    version: NPM_VERSION.clone(),\n                },\n                PackageManager {\n                    kind: PackageManagerKind::Yarn,\n                    source: Source::Default,\n                    version: YARN_VERSION.clone(),\n                },\n            ];\n            let packages = vec![];\n\n            assert_eq!(\n                display_active(&runtime, &package_managers, &packages),\n                expected\n            );\n        }\n\n        #[test]\n        fn with_default_tools() {\n            let expected = \"⚡️ Currently active tools:\n\n    Node: v12.2.0 (current @ ~/path/to/project.json)\n    npm: v6.13.1 (current @ ~/path/to/project.json)\n    Yarn: v1.16.0 (current @ ~/path/to/project.json)\n    Tool binaries available:\n        create-react-app (default)\n        tsc, tsserver (default)\n\nSee options for more detailed reports by running `volta list --help`.\";\n\n            let runtime = Some(Box::new(Node {\n                source: Source::Project(PROJECT_PATH.clone()),\n                version: NODE_12.clone(),\n            }));\n            let package_managers = vec![\n                PackageManager {\n                    kind: PackageManagerKind::Npm,\n                    source: Source::Project(PROJECT_PATH.clone()),\n                    version: NPM_VERSION.clone(),\n                },\n                PackageManager {\n                    kind: PackageManagerKind::Yarn,\n                    source: Source::Project(PROJECT_PATH.clone()),\n                    version: YARN_VERSION.clone(),\n                },\n            ];\n            let packages = vec![\n                Package::Default {\n                    details: PackageDetails {\n                        name: \"create-react-app\".to_string(),\n                        version: Version::from((3, 0, 1)),\n                    },\n                    node: NODE_12.clone(),\n                    tools: vec![\"create-react-app\".to_string()],\n                },\n                Package::Default {\n                    details: PackageDetails {\n                        name: \"typescript\".to_string(),\n                        version: Version::from((3, 4, 3)),\n                    },\n                    node: NODE_12.clone(),\n                    tools: vec![\"tsc\".to_string(), \"tsserver\".to_string()],\n                },\n            ];\n\n            assert_eq!(\n                display_active(&runtime, &package_managers, &packages),\n                expected\n            );\n        }\n\n        #[test]\n        fn with_project_tools() {\n            let expected = \"⚡️ Currently active tools:\n\n    Node: v12.2.0 (current @ ~/path/to/project.json)\n    npm: v6.13.1 (current @ ~/path/to/project.json)\n    Yarn: v1.16.0 (current @ ~/path/to/project.json)\n    Tool binaries available:\n        create-react-app (current @ ~/path/to/project.json)\n        tsc, tsserver (default)\n\nSee options for more detailed reports by running `volta list --help`.\";\n\n            let runtime = Some(Box::new(Node {\n                source: Source::Project(PROJECT_PATH.clone()),\n                version: NODE_12.clone(),\n            }));\n            let package_managers = vec![\n                PackageManager {\n                    kind: PackageManagerKind::Npm,\n                    source: Source::Project(PROJECT_PATH.clone()),\n                    version: NPM_VERSION.clone(),\n                },\n                PackageManager {\n                    kind: PackageManagerKind::Yarn,\n                    source: Source::Project(PROJECT_PATH.clone()),\n                    version: YARN_VERSION.clone(),\n                },\n            ];\n            let packages = vec![\n                Package::Project {\n                    name: \"create-react-app\".to_string(),\n                    path: PROJECT_PATH.clone(),\n                    tools: vec![\"create-react-app\".to_string()],\n                },\n                Package::Default {\n                    details: PackageDetails {\n                        name: \"typescript\".to_string(),\n                        version: Version::from((3, 4, 3)),\n                    },\n                    node: NODE_12.clone(),\n                    tools: vec![\"tsc\".to_string(), \"tsserver\".to_string()],\n                },\n            ];\n\n            assert_eq!(\n                display_active(&runtime, &package_managers, &packages),\n                expected\n            );\n        }\n    }\n\n    mod node {\n        use super::super::*;\n        use super::*;\n        use crate::command::list::Source;\n\n        #[test]\n        fn no_runtimes() {\n            let expected = NO_RUNTIME;\n\n            let runtimes = [];\n            assert_eq!(display_node(&runtimes).as_str(), expected);\n        }\n\n        #[test]\n        fn single_default() {\n            let expected = \"⚡️ Node runtimes in your toolchain:\n\n    v10.15.3 (default)\";\n            let runtimes = [Node {\n                source: Source::Default,\n                version: NODE_10.clone(),\n            }];\n\n            assert_eq!(display_node(&runtimes).as_str(), expected);\n        }\n\n        #[test]\n        fn single_project() {\n            let expected = \"⚡️ Node runtimes in your toolchain:\n\n    v12.2.0 (current @ ~/path/to/project.json)\";\n\n            let runtimes = [Node {\n                source: Source::Project(PROJECT_PATH.clone()),\n                version: NODE_12.clone(),\n            }];\n\n            assert_eq!(display_node(&runtimes).as_str(), expected);\n        }\n\n        #[test]\n        fn single_installed() {\n            let expected = \"⚡️ Node runtimes in your toolchain:\n\n    v11.9.0\";\n\n            let runtimes = [Node {\n                source: Source::None,\n                version: NODE_11.clone(),\n            }];\n\n            assert_eq!(display_node(&runtimes).as_str(), expected);\n        }\n\n        #[test]\n        fn multi() {\n            let expected = \"⚡️ Node runtimes in your toolchain:\n\n    v12.2.0 (current @ ~/path/to/project.json)\n    v11.9.0\n    v10.15.3 (default)\";\n\n            let runtimes = [\n                Node {\n                    source: Source::Project(PROJECT_PATH.clone()),\n                    version: NODE_12.clone(),\n                },\n                Node {\n                    source: Source::None,\n                    version: NODE_11.clone(),\n                },\n                Node {\n                    source: Source::Default,\n                    version: NODE_10.clone(),\n                },\n            ];\n\n            assert_eq!(display_node(&runtimes), expected);\n        }\n    }\n\n    mod package_managers {\n        use super::*;\n        use crate::command::list::{PackageManager, PackageManagerKind, Source};\n\n        #[test]\n        fn none_installed_npm() {\n            let expected =\n                \"⚡️ No custom npm versions installed (npm is still available bundled with Node).\n\nYou can install an npm version by running `volta install npm`.\nSee `volta help install` for details and more options.\";\n\n            assert_eq!(\n                display_package_managers(PackageManagerKind::Npm, &[]),\n                expected\n            );\n        }\n\n        #[test]\n        fn none_installed_yarn() {\n            let expected = \"⚡️ No Yarn versions installed.\n\nYou can install a Yarn version by running `volta install yarn`.\nSee `volta help install` for details and more options.\";\n\n            assert_eq!(\n                display_package_managers(PackageManagerKind::Yarn, &[]),\n                expected\n            );\n        }\n\n        #[test]\n        fn single_default_npm() {\n            let expected = \"⚡️ Custom npm versions in your toolchain:\n\n    v6.13.1 (default)\";\n\n            let package_managers = [PackageManager {\n                kind: PackageManagerKind::Npm,\n                source: Source::Default,\n                version: NPM_VERSION.clone(),\n            }];\n\n            assert_eq!(\n                display_package_managers(PackageManagerKind::Npm, &package_managers),\n                expected\n            );\n        }\n\n        #[test]\n        fn single_default_yarn() {\n            let expected = \"⚡️ Yarn versions in your toolchain:\n\n    v1.16.0 (default)\";\n\n            let package_managers = [PackageManager {\n                kind: PackageManagerKind::Yarn,\n                source: Source::Default,\n                version: YARN_VERSION.clone(),\n            }];\n\n            assert_eq!(\n                display_package_managers(PackageManagerKind::Yarn, &package_managers),\n                expected\n            );\n        }\n\n        #[test]\n        fn single_project_npm() {\n            let expected = \"⚡️ Custom npm versions in your toolchain:\n\n    v6.13.1 (current @ ~/path/to/project.json)\";\n\n            let package_managers = [PackageManager {\n                kind: PackageManagerKind::Npm,\n                source: Source::Project(PROJECT_PATH.clone()),\n                version: NPM_VERSION.clone(),\n            }];\n\n            assert_eq!(\n                display_package_managers(PackageManagerKind::Npm, &package_managers),\n                expected\n            );\n        }\n\n        #[test]\n        fn single_project_yarn() {\n            let expected = \"⚡️ Yarn versions in your toolchain:\n\n    v1.16.0 (current @ ~/path/to/project.json)\";\n\n            let package_managers = [PackageManager {\n                kind: PackageManagerKind::Yarn,\n                source: Source::Project(PROJECT_PATH.clone()),\n                version: YARN_VERSION.clone(),\n            }];\n\n            assert_eq!(\n                display_package_managers(PackageManagerKind::Yarn, &package_managers),\n                expected\n            );\n        }\n\n        #[test]\n        fn single_installed_npm() {\n            let expected = \"⚡️ Custom npm versions in your toolchain:\n\n    v6.13.1\";\n\n            let package_managers = [PackageManager {\n                kind: PackageManagerKind::Npm,\n                source: Source::None,\n                version: NPM_VERSION.clone(),\n            }];\n\n            assert_eq!(\n                display_package_managers(PackageManagerKind::Npm, &package_managers),\n                expected\n            );\n        }\n\n        #[test]\n        fn single_installed_yarn() {\n            let expected = \"⚡️ Yarn versions in your toolchain:\n\n    v1.16.0\";\n\n            let package_managers = [PackageManager {\n                kind: PackageManagerKind::Yarn,\n                source: Source::None,\n                version: YARN_VERSION.clone(),\n            }];\n\n            assert_eq!(\n                display_package_managers(PackageManagerKind::Yarn, &package_managers),\n                expected\n            );\n        }\n\n        #[test]\n        fn multi_npm() {\n            let expected = \"⚡️ Custom npm versions in your toolchain:\n\n    v5.6.0\n    v6.13.1 (default)\n    v6.14.2 (current @ ~/path/to/project.json)\";\n\n            let package_managers = [\n                PackageManager {\n                    kind: PackageManagerKind::Npm,\n                    source: Source::None,\n                    version: Version::from((5, 6, 0)),\n                },\n                PackageManager {\n                    kind: PackageManagerKind::Npm,\n                    source: Source::Default,\n                    version: NPM_VERSION.clone(),\n                },\n                PackageManager {\n                    kind: PackageManagerKind::Npm,\n                    source: Source::Project(PROJECT_PATH.clone()),\n                    version: Version::from((6, 14, 2)),\n                },\n            ];\n\n            assert_eq!(\n                display_package_managers(PackageManagerKind::Npm, &package_managers),\n                expected\n            );\n        }\n\n        #[test]\n        fn multi_yarn() {\n            let expected = \"⚡️ Yarn versions in your toolchain:\n\n    v1.3.0\n    v1.16.0 (default)\n    v1.17.0 (current @ ~/path/to/project.json)\";\n\n            let package_managers = [\n                PackageManager {\n                    kind: PackageManagerKind::Yarn,\n                    source: Source::None,\n                    version: Version::from((1, 3, 0)),\n                },\n                PackageManager {\n                    kind: PackageManagerKind::Yarn,\n                    source: Source::Default,\n                    version: YARN_VERSION.clone(),\n                },\n                PackageManager {\n                    kind: PackageManagerKind::Yarn,\n                    source: Source::Project(PROJECT_PATH.clone()),\n                    version: Version::from((1, 17, 0)),\n                },\n            ];\n\n            assert_eq!(\n                display_package_managers(PackageManagerKind::Yarn, &package_managers),\n                expected\n            );\n        }\n    }\n\n    mod packages {\n        use super::*;\n        use crate::command::list::{Package, PackageDetails};\n        use node_semver::Version;\n\n        #[test]\n        fn none() {\n            let expected = \"⚡️ No tools or packages installed.\n\nYou can safely install packages by running `volta install <package name>`.\nSee `volta help install` for details and more options.\";\n\n            assert_eq!(display_packages(&[]), expected);\n        }\n\n        #[test]\n        fn single_default() {\n            let expected = \"⚡️ Package versions in your toolchain:\n\n    ember-cli@3.10.1 (default)\n        binary tools: ember\n        platform:\n            runtime: node@12.2.0\n            package manager: npm@built-in\";\n\n            let packages = [Package::Default {\n                details: PackageDetails {\n                    name: \"ember-cli\".to_string(),\n                    version: Version::from((3, 10, 1)),\n                },\n                node: NODE_12.clone(),\n                tools: vec![\"ember\".to_string()],\n            }];\n\n            assert_eq!(display_packages(&packages), expected);\n        }\n\n        #[test]\n        fn single_project() {\n            let expected = \"⚡️ Package versions in your toolchain:\n\n    ember-cli (current @ ~/path/to/project.json)\n        binary tools: ember\";\n\n            let packages = [Package::Project {\n                name: \"ember-cli\".to_string(),\n                path: PROJECT_PATH.clone(),\n                tools: vec![\"ember\".to_string()],\n            }];\n\n            assert_eq!(display_packages(&packages), expected);\n        }\n\n        #[test]\n        fn single_fetched() {\n            let expected = \"⚡️ Package versions in your toolchain:\n\n    ember-cli@3.10.1\n    \n    To make it available to execute, run `volta install ember-cli@3.10.1`.\";\n\n            let packages = [Package::Fetched(PackageDetails {\n                name: \"ember-cli\".to_string(),\n                version: Version::from((3, 10, 1)),\n            })];\n\n            assert_eq!(display_packages(&packages), expected);\n        }\n\n        #[test]\n        fn multi_fetched() {\n            let expected = \"⚡️ Package versions in your toolchain:\n\n    ember-cli@3.10.1\n    \n    To make it available to execute, run `volta install ember-cli@3.10.1`.\n    ember-cli@3.8.2\n    \n    To make it available to execute, run `volta install ember-cli@3.8.2`.\";\n\n            let packages = [\n                Package::Fetched(PackageDetails {\n                    name: \"ember-cli\".to_string(),\n                    version: Version::from((3, 10, 1)),\n                }),\n                Package::Fetched(PackageDetails {\n                    name: \"ember-cli\".to_string(),\n                    version: Version::from((3, 8, 2)),\n                }),\n            ];\n\n            assert_eq!(display_packages(&packages), expected);\n        }\n\n        #[test]\n        fn multi() {\n            let expected = \"⚡️ Package versions in your toolchain:\n\n    ember-cli@3.10.1 (default)\n        binary tools: ember\n        platform:\n            runtime: node@12.2.0\n            package manager: npm@built-in\n    ember-cli (current @ ~/path/to/project.json)\n        binary tools: ember\";\n\n            let packages = [\n                Package::Default {\n                    details: PackageDetails {\n                        name: \"ember-cli\".to_string(),\n                        version: Version::from((3, 10, 1)),\n                    },\n                    node: NODE_12.clone(),\n                    tools: vec![\"ember\".to_string()],\n                },\n                Package::Project {\n                    name: \"ember-cli\".to_string(),\n                    path: PROJECT_PATH.clone(),\n                    tools: vec![\"ember\".to_string()],\n                },\n            ];\n\n            assert_eq!(display_packages(&packages), expected);\n        }\n    }\n\n    mod tools {\n        use super::*;\n        use crate::command::list::{Package, PackageDetails};\n        use node_semver::Version;\n\n        #[test]\n        fn none() {\n            let expected = \"⚡️ No tools or packages named `ember` installed.\n\nYou can safely install packages by running `volta install <package name>`.\nSee `volta help install` for details and more options.\";\n\n            assert_eq!(display_tool(\"ember\", &[]), expected);\n        }\n\n        #[test]\n        fn single_default() {\n            let expected = \"⚡️ Tool `ember` available from:\n\n    ember-cli@3.10.1 (default)\n        binary tools: ember\n        platform:\n            runtime: node@12.2.0\n            package manager: npm@built-in\";\n\n            let packages = [Package::Default {\n                details: PackageDetails {\n                    name: \"ember-cli\".to_string(),\n                    version: Version::from((3, 10, 1)),\n                },\n                node: NODE_12.clone(),\n                tools: vec![\"ember\".to_string()],\n            }];\n\n            assert_eq!(display_tool(\"ember\", &packages), expected);\n        }\n\n        #[test]\n        fn single_project() {\n            let expected = \"⚡️ Tool `ember` available from:\n\n    ember-cli (current @ ~/path/to/project.json)\n        binary tools: ember\";\n\n            let packages = [Package::Project {\n                name: \"ember-cli\".to_string(),\n                path: PROJECT_PATH.clone(),\n                tools: vec![\"ember\".to_string()],\n            }];\n\n            assert_eq!(display_tool(\"ember\", &packages), expected);\n        }\n\n        #[test]\n        fn single_fetched() {\n            let expected = \"⚡️ Tool `ember` available from:\n\n    ember-cli@3.10.1\n    \n    To make it available to execute, run `volta install ember-cli@3.10.1`.\";\n\n            let packages = [Package::Fetched(PackageDetails {\n                name: \"ember-cli\".to_string(),\n                version: Version::from((3, 10, 1)),\n            })];\n\n            assert_eq!(display_tool(\"ember\", &packages), expected);\n        }\n\n        #[test]\n        fn multi_fetched() {\n            let expected = \"⚡️ Tool `ember` available from:\n\n    ember-cli@3.10.1\n    \n    To make it available to execute, run `volta install ember-cli@3.10.1`.\n    ember-cli@3.8.2\n    \n    To make it available to execute, run `volta install ember-cli@3.8.2`.\";\n\n            let packages = [\n                Package::Fetched(PackageDetails {\n                    name: \"ember-cli\".to_string(),\n                    version: Version::from((3, 10, 1)),\n                }),\n                Package::Fetched(PackageDetails {\n                    name: \"ember-cli\".to_string(),\n                    version: Version::from((3, 8, 2)),\n                }),\n            ];\n\n            assert_eq!(display_tool(\"ember\", &packages), expected);\n        }\n\n        #[test]\n        fn multi() {\n            let expected = \"⚡️ Tool `ember` available from:\n\n    ember-cli@3.10.1 (default)\n        binary tools: ember\n        platform:\n            runtime: node@12.2.0\n            package manager: npm@built-in\n    ember-cli (current @ ~/path/to/project.json)\n        binary tools: ember\";\n\n            let packages = [\n                Package::Default {\n                    details: PackageDetails {\n                        name: \"ember-cli\".to_string(),\n                        version: Version::from((3, 10, 1)),\n                    },\n                    node: NODE_12.clone(),\n                    tools: vec![\"ember\".to_string()],\n                },\n                Package::Project {\n                    name: \"ember-cli\".to_string(),\n                    path: PROJECT_PATH.clone(),\n                    tools: vec![\"ember\".to_string()],\n                },\n            ];\n\n            assert_eq!(display_tool(\"ember\", &packages), expected);\n        }\n    }\n\n    mod all {\n        use super::*;\n        use crate::command::list::{PackageDetails, PackageManagerKind, Source};\n\n        #[test]\n        fn empty() {\n            let runtimes = [];\n            let package_managers = [];\n            let packages = [];\n\n            assert_eq!(\n                display_all(&runtimes, &package_managers, &packages),\n                NO_RUNTIME\n            );\n        }\n\n        #[test]\n        fn runtime_and_npm() {\n            let expected = \"⚡️ User toolchain:\n\n    Node runtimes:\n        v12.2.0 (current @ ~/path/to/project.json)\n        v11.9.0\n        v10.15.3 (default)\n\n    Package managers:\n        npm:\n            v6.13.1 (default)\n            v6.12.0 (current @ ~/path/to/project.json)\n            v5.6.0\n\n    Packages:\n\";\n\n            let runtimes = [\n                Node {\n                    source: Source::Project(PROJECT_PATH.clone()),\n                    version: NODE_12.clone(),\n                },\n                Node {\n                    source: Source::None,\n                    version: NODE_11.clone(),\n                },\n                Node {\n                    source: Source::Default,\n                    version: NODE_10.clone(),\n                },\n            ];\n\n            let package_managers = [\n                PackageManager {\n                    kind: PackageManagerKind::Npm,\n                    source: Source::Default,\n                    version: NPM_VERSION.clone(),\n                },\n                PackageManager {\n                    kind: PackageManagerKind::Npm,\n                    source: Source::Project(PROJECT_PATH.clone()),\n                    version: Version::from((6, 12, 0)),\n                },\n                PackageManager {\n                    kind: PackageManagerKind::Npm,\n                    source: Source::None,\n                    version: Version::from((5, 6, 0)),\n                },\n            ];\n\n            let packages = vec![];\n            assert_eq!(\n                display_all(&runtimes, &package_managers, &packages),\n                expected\n            );\n        }\n\n        #[test]\n        fn runtime_and_yarn() {\n            let expected = \"⚡️ User toolchain:\n\n    Node runtimes:\n        v12.2.0 (current @ ~/path/to/project.json)\n        v11.9.0\n        v10.15.3 (default)\n\n    Package managers:\n        Yarn:\n            v1.16.0 (default)\n            v1.17.0 (current @ ~/path/to/project.json)\n            v1.4.0\n\n    Packages:\n\";\n\n            let runtimes = [\n                Node {\n                    source: Source::Project(PROJECT_PATH.clone()),\n                    version: NODE_12.clone(),\n                },\n                Node {\n                    source: Source::None,\n                    version: NODE_11.clone(),\n                },\n                Node {\n                    source: Source::Default,\n                    version: NODE_10.clone(),\n                },\n            ];\n\n            let package_managers = [\n                PackageManager {\n                    kind: PackageManagerKind::Yarn,\n                    source: Source::Default,\n                    version: YARN_VERSION.clone(),\n                },\n                PackageManager {\n                    kind: PackageManagerKind::Yarn,\n                    source: Source::Project(PROJECT_PATH.clone()),\n                    version: Version::from((1, 17, 0)),\n                },\n                PackageManager {\n                    kind: PackageManagerKind::Yarn,\n                    source: Source::None,\n                    version: Version::from((1, 4, 0)),\n                },\n            ];\n\n            let packages = vec![];\n            assert_eq!(\n                display_all(&runtimes, &package_managers, &packages),\n                expected\n            );\n        }\n\n        #[test]\n        fn full() {\n            let expected = \"⚡️ User toolchain:\n\n    Node runtimes:\n        v12.2.0 (current @ ~/path/to/project.json)\n        v11.9.0\n        v10.15.3 (default)\n\n    Package managers:\n        npm:\n            v6.13.1 (default)\n            v6.12.0 (current @ ~/path/to/project.json)\n            v5.6.0\n        Yarn:\n            v1.16.0 (default)\n            v1.17.0 (current @ ~/path/to/project.json)\n            v1.4.0\n\n    Packages:\n        typescript@3.4.3 (default)\n            binary tools: tsc, tsserver\n            platform:\n                runtime: node@12.2.0\n                package manager: npm@built-in\n        typescript (current @ ~/path/to/project.json)\n            binary tools: tsc, tsserver\n        ember-cli (current @ ~/path/to/project.json)\n            binary tools: ember\n        ember-cli@3.8.2 (default)\n            binary tools: ember\n            platform:\n                runtime: node@12.2.0\n                package manager: npm@built-in\";\n\n            let runtimes = [\n                Node {\n                    source: Source::Project(PROJECT_PATH.clone()),\n                    version: NODE_12.clone(),\n                },\n                Node {\n                    source: Source::None,\n                    version: NODE_11.clone(),\n                },\n                Node {\n                    source: Source::Default,\n                    version: NODE_10.clone(),\n                },\n            ];\n\n            let package_managers = [\n                PackageManager {\n                    kind: PackageManagerKind::Npm,\n                    source: Source::Default,\n                    version: NPM_VERSION.clone(),\n                },\n                PackageManager {\n                    kind: PackageManagerKind::Npm,\n                    source: Source::Project(PROJECT_PATH.clone()),\n                    version: Version::from((6, 12, 0)),\n                },\n                PackageManager {\n                    kind: PackageManagerKind::Npm,\n                    source: Source::None,\n                    version: Version::from((5, 6, 0)),\n                },\n                PackageManager {\n                    kind: PackageManagerKind::Yarn,\n                    source: Source::Default,\n                    version: YARN_VERSION.clone(),\n                },\n                PackageManager {\n                    kind: PackageManagerKind::Yarn,\n                    source: Source::Project(PROJECT_PATH.clone()),\n                    version: Version::from((1, 17, 0)),\n                },\n                PackageManager {\n                    kind: PackageManagerKind::Yarn,\n                    source: Source::None,\n                    version: Version::from((1, 4, 0)),\n                },\n            ];\n\n            let packages = [\n                Package::Default {\n                    details: PackageDetails {\n                        name: \"typescript\".to_string(),\n                        version: Version::from((3, 4, 3)),\n                    },\n                    node: NODE_12.clone(),\n                    tools: vec![\"tsc\".to_string(), \"tsserver\".to_string()],\n                },\n                Package::Project {\n                    name: \"typescript\".to_string(),\n                    path: PROJECT_PATH.clone(),\n                    tools: vec![\"tsc\".to_string(), \"tsserver\".to_string()],\n                },\n                Package::Project {\n                    name: \"ember-cli\".to_string(),\n                    path: PROJECT_PATH.clone(),\n                    tools: vec![\"ember\".to_string()],\n                },\n                Package::Default {\n                    details: PackageDetails {\n                        name: \"ember-cli\".to_string(),\n                        version: Version::from((3, 8, 2)),\n                    },\n                    node: NODE_12.clone(),\n                    tools: vec![\"ember\".to_string()],\n                },\n            ];\n            assert_eq!(\n                display_all(&runtimes, &package_managers, &packages),\n                expected\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "src/command/list/mod.rs",
    "content": "mod human;\nmod plain;\nmod toolchain;\n\nuse std::io::IsTerminal as _;\nuse std::{fmt, path::PathBuf, str::FromStr};\n\nuse node_semver::Version;\n\nuse crate::command::Command;\nuse toolchain::Toolchain;\nuse volta_core::error::{ExitCode, Fallible};\nuse volta_core::inventory::package_configs;\nuse volta_core::project::Project;\nuse volta_core::session::{ActivityKind, Session};\nuse volta_core::tool::PackageConfig;\n\n#[derive(clap::ValueEnum, Copy, Clone)]\nenum Format {\n    Human,\n    Plain,\n}\n\n/// The source of a given item, from the perspective of a user.\n///\n/// Note: this is distinct from `volta_core::platform::sourced::Source`, which\n/// represents the source only of a `Platform`, which is a composite structure.\n/// By contrast, this `Source` is concerned *only* with a single item.\n#[derive(Clone, PartialEq, Debug)]\nenum Source {\n    /// The item is from a project. The wrapped `PathBuf` is the path to the\n    /// project's `package.json`.\n    Project(PathBuf),\n\n    /// The item is the user's default.\n    Default,\n\n    /// The item is one that has been *fetched* but is not *installed* anywhere.\n    None,\n}\n\nimpl Source {\n    fn allowed_with(&self, filter: &Filter) -> bool {\n        match filter {\n            Filter::Default => self == &Source::Default,\n            Filter::Current => matches!(self, Source::Default | Source::Project(_)),\n            Filter::None => true,\n        }\n    }\n}\n\nimpl fmt::Display for Source {\n    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {\n        write!(\n            f,\n            \"{}\",\n            match self {\n                Source::Project(path) => format!(\" (current @ {})\", path.display()),\n                Source::Default => String::from(\" (default)\"),\n                Source::None => String::from(\"\"),\n            }\n        )\n    }\n}\n\n/// A package and its associated tools, for displaying to the user as part of\n/// their toolchain.\nstruct PackageDetails {\n    /// The name of the package.\n    pub name: String,\n    /// The package's own version.\n    pub version: Version,\n}\n\nenum Package {\n    Default {\n        details: PackageDetails,\n        /// The version of Node the package is installed against.\n        node: Version,\n        /// The names of the tools associated with the package.\n        tools: Vec<String>,\n    },\n    Project {\n        name: String,\n        /// The names of the tools associated with the package.\n        tools: Vec<String>,\n        path: PathBuf,\n    },\n    Fetched(PackageDetails),\n}\n\nimpl Package {\n    fn new(config: &PackageConfig, source: &Source) -> Package {\n        let details = PackageDetails {\n            name: config.name.clone(),\n            version: config.version.clone(),\n        };\n\n        match source {\n            Source::Default => Package::Default {\n                details,\n                node: config.platform.node.clone(),\n                tools: config.bins.clone(),\n            },\n            Source::Project(path) => Package::Project {\n                name: details.name,\n                tools: config.bins.clone(),\n                path: path.clone(),\n            },\n            Source::None => Package::Fetched(details),\n        }\n    }\n\n    fn from_inventory_and_project(project: Option<&Project>) -> Fallible<Vec<Package>> {\n        package_configs().map(|configs| {\n            configs\n                .iter()\n                .map(|config| {\n                    let source = Self::source(&config.name, project);\n                    Package::new(config, &source)\n                })\n                .collect()\n        })\n    }\n\n    fn source(name: &str, project: Option<&Project>) -> Source {\n        match project {\n            Some(project) if project.has_direct_dependency(name) => {\n                Source::Project(project.manifest_file().to_owned())\n            }\n            _ => Source::Default,\n        }\n    }\n}\n\n#[derive(Clone)]\nstruct Node {\n    pub source: Source,\n    pub version: Version,\n}\n\n#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]\nenum PackageManagerKind {\n    Npm,\n    Pnpm,\n    Yarn,\n}\n\nimpl fmt::Display for PackageManagerKind {\n    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {\n        write!(\n            f,\n            \"{}\",\n            match self {\n                PackageManagerKind::Npm => \"npm\",\n                PackageManagerKind::Pnpm => \"pnpm\",\n                PackageManagerKind::Yarn => \"yarn\",\n            }\n        )\n    }\n}\n\n#[derive(Clone)]\nstruct PackageManager {\n    kind: PackageManagerKind,\n    source: Source,\n    version: Version,\n}\n\n/// How (if at all) should the list query be narrowed?\nenum Filter {\n    /// Display only the currently active tool(s).\n    ///\n    /// For example, if the user queries `volta list --current yarn`, show only\n    /// the version of Yarn currently in use: project, default, or none.\n    Current,\n\n    /// Show only the user's default tool(s).\n    ///\n    /// For example, if the user queries `volta list --default node`, show only\n    /// the user's default Node version.\n    Default,\n\n    /// Do not filter at all. Show all tool(s) matching the query.\n    None,\n}\n\n#[derive(clap::Args)]\npub(crate) struct List {\n    /// The tool to lookup - `all`, `node`, `npm`, `yarn`, `pnpm`, or the name\n    /// of a package or binary.\n    #[arg(value_name = \"tool\")]\n    subcommand: Option<Subcommand>,\n\n    /// Specify the output format.\n    ///\n    /// Defaults to `human` for TTYs, `plain` otherwise.\n    #[arg(long)]\n    format: Option<Format>,\n\n    /// Show the currently-active tool(s).\n    ///\n    /// Equivalent to `volta list` when not specifying a specific tool.\n    #[arg(short, long, conflicts_with = \"default\")]\n    current: bool,\n\n    /// Show your default tool(s).\n    #[arg(short, long, conflicts_with = \"current\")]\n    default: bool,\n}\n\n/// Which tool should we look up?\n#[derive(Clone)]\nenum Subcommand {\n    /// Show every item in the toolchain.\n    All,\n\n    /// Show locally cached Node versions.\n    Node,\n\n    /// Show locally cached npm versions.\n    Npm,\n\n    /// Show locally cached pnpm versions.\n    Pnpm,\n\n    /// Show locally cached Yarn versions.\n    Yarn,\n\n    /// Show locally cached versions of a package or a package binary.\n    PackageOrTool { name: String },\n}\n\nimpl FromStr for Subcommand {\n    type Err = std::convert::Infallible;\n\n    fn from_str(s: &str) -> Result<Self, Self::Err> {\n        Ok(match s {\n            \"all\" => Subcommand::All,\n            \"node\" => Subcommand::Node,\n            \"npm\" => Subcommand::Npm,\n            \"pnpm\" => Subcommand::Pnpm,\n            \"yarn\" => Subcommand::Yarn,\n            s => Subcommand::PackageOrTool { name: s.into() },\n        })\n    }\n}\n\nimpl List {\n    fn output_format(&self) -> Format {\n        // We start by checking if the user has explicitly set a value: if they\n        // have, that trumps our TTY-checking. Then, if the user has *not*\n        // specified an option, we use `Human` mode for TTYs and `Plain` for\n        // non-TTY contexts.\n        self.format.unwrap_or(if std::io::stdout().is_terminal() {\n            Format::Human\n        } else {\n            Format::Plain\n        })\n    }\n}\n\nimpl Command for List {\n    fn run(self, session: &mut Session) -> Fallible<ExitCode> {\n        session.add_event_start(ActivityKind::List);\n\n        let project = session.project()?;\n        let default_platform = session.default_platform()?;\n        let format = match self.output_format() {\n            Format::Human => human::format,\n            Format::Plain => plain::format,\n        };\n\n        let filter = match (self.current, self.default) {\n            (true, false) => Filter::Current,\n            (false, true) => Filter::Default,\n            (true, true) => unreachable!(\"simultaneous `current` and `default` forbidden by clap\"),\n            _ => Filter::None,\n        };\n\n        let toolchain = match self.subcommand {\n            // For no subcommand, show the user's current toolchain\n            None => Toolchain::active(project, default_platform)?,\n            Some(Subcommand::All) => Toolchain::all(project, default_platform)?,\n            Some(Subcommand::Node) => Toolchain::node(project, default_platform, &filter)?,\n            Some(Subcommand::Npm) => Toolchain::npm(project, default_platform, &filter)?,\n            Some(Subcommand::Pnpm) => Toolchain::pnpm(project, default_platform, &filter)?,\n            Some(Subcommand::Yarn) => Toolchain::yarn(project, default_platform, &filter)?,\n            Some(Subcommand::PackageOrTool { name }) => {\n                Toolchain::package_or_tool(&name, project, &filter)?\n            }\n        };\n\n        if let Some(string) = format(&toolchain) {\n            println!(\"{}\", string)\n        };\n\n        session.add_event_end(ActivityKind::List, ExitCode::Success);\n        Ok(ExitCode::Success)\n    }\n}\n"
  },
  {
    "path": "src/command/list/plain.rs",
    "content": "//! Define the \"plain\" format style for list commands.\n\nuse node_semver::Version;\n\nuse volta_core::style::tool_version;\n\nuse super::{Node, Package, PackageManager, Source, Toolchain};\n\npub(super) fn format(toolchain: &Toolchain) -> Option<String> {\n    let (runtimes, package_managers, packages) = match toolchain {\n        Toolchain::Node(runtimes) => (describe_runtimes(runtimes), None, None),\n        Toolchain::PackageManagers { managers, .. } => {\n            (None, describe_package_managers(managers), None)\n        }\n        Toolchain::Packages(packages) => (None, None, describe_packages(packages)),\n        Toolchain::Tool {\n            name,\n            host_packages,\n        } => (None, None, Some(describe_tool_set(name, host_packages))),\n        Toolchain::Active {\n            runtime,\n            package_managers,\n            packages,\n        } => (\n            runtime\n                .as_ref()\n                .and_then(|r| describe_runtimes(&[(**r).clone()])),\n            describe_package_managers(package_managers),\n            describe_packages(packages),\n        ),\n        Toolchain::All {\n            runtimes,\n            package_managers,\n            packages,\n        } => (\n            describe_runtimes(runtimes),\n            describe_package_managers(package_managers),\n            describe_packages(packages),\n        ),\n    };\n\n    match (runtimes, package_managers, packages) {\n        (Some(runtimes), Some(package_managers), Some(packages)) => {\n            Some(format!(\"{}\\n{}\\n{}\", runtimes, package_managers, packages))\n        }\n        (Some(runtimes), Some(package_managers), None) => {\n            Some(format!(\"{}\\n{}\", runtimes, package_managers))\n        }\n        (Some(runtimes), None, Some(packages)) => Some(format!(\"{}\\n{}\", runtimes, packages)),\n        (Some(runtimes), None, None) => Some(runtimes),\n        (None, Some(package_managers), Some(packages)) => {\n            Some(format!(\"{}\\n{}\", package_managers, packages))\n        }\n        (None, Some(package_managers), None) => Some(package_managers),\n        (None, None, Some(packages)) => Some(packages),\n        (None, None, None) => None,\n    }\n}\n\nfn describe_runtimes(runtimes: &[Node]) -> Option<String> {\n    if runtimes.is_empty() {\n        None\n    } else {\n        Some(\n            runtimes\n                .iter()\n                .map(|runtime| display_node(&runtime.source, &runtime.version))\n                .collect::<Vec<String>>()\n                .join(\"\\n\"),\n        )\n    }\n}\n\nfn describe_package_managers(package_managers: &[PackageManager]) -> Option<String> {\n    if package_managers.is_empty() {\n        None\n    } else {\n        Some(\n            package_managers\n                .iter()\n                .map(display_package_manager)\n                .collect::<Vec<String>>()\n                .join(\"\\n\"),\n        )\n    }\n}\n\nfn describe_packages(packages: &[Package]) -> Option<String> {\n    if packages.is_empty() {\n        None\n    } else {\n        Some(\n            packages\n                .iter()\n                .map(display_package)\n                .collect::<Vec<String>>()\n                .join(\"\\n\"),\n        )\n    }\n}\n\nfn describe_tool_set(name: &str, hosts: &[Package]) -> String {\n    hosts\n        .iter()\n        .filter_map(|package| display_tool(name, package))\n        .collect::<Vec<String>>()\n        .join(\"\\n\")\n}\n\nfn display_node(source: &Source, version: &Version) -> String {\n    format!(\"runtime {}{}\", tool_version(\"node\", version), source)\n}\n\nfn display_package_manager(package_manager: &PackageManager) -> String {\n    format!(\n        \"package-manager {}{}\",\n        tool_version(package_manager.kind, &package_manager.version),\n        package_manager.source\n    )\n}\n\nfn package_source(package: &Package) -> String {\n    match package {\n        Package::Default { .. } => String::from(\" (default)\"),\n        Package::Project { path, .. } => format!(\" (current @ {})\", path.display()),\n        Package::Fetched(..) => String::new(),\n    }\n}\n\nfn display_package(package: &Package) -> String {\n    match package {\n        Package::Default {\n            details,\n            node,\n            tools,\n            ..\n        } => {\n            let tools = match tools.len() {\n                0 => String::from(\" \"),\n                _ => format!(\" {} \", tools.join(\", \")),\n            };\n\n            format!(\n                \"package {} /{}/ {} {}{}\",\n                tool_version(&details.name, &details.version),\n                tools,\n                tool_version(\"node\", node),\n                // Should be updated when we support installing with custom package_managers,\n                // whether Yarn or non-built-in versions of npm\n                \"npm@built-in\",\n                package_source(package)\n            )\n        }\n        Package::Project { name, tools, .. } => {\n            let tools = match tools.len() {\n                0 => String::from(\" \"),\n                _ => format!(\" {} \", tools.join(\", \")),\n            };\n\n            format!(\n                \"package {} /{}/ {} {}{}\",\n                tool_version(name, \"project\"),\n                tools,\n                \"node@project\",\n                \"npm@project\",\n                package_source(package)\n            )\n        }\n        Package::Fetched(details) => format!(\n            \"package {} (fetched)\",\n            tool_version(&details.name, &details.version)\n        ),\n    }\n}\n\nfn display_tool(name: &str, host: &Package) -> Option<String> {\n    match host {\n        Package::Default { details, node, .. } => Some(format!(\n            \"tool {} / {} / {} {}{}\",\n            name,\n            tool_version(&details.name, &details.version),\n            tool_version(\"node\", node),\n            \"npm@built-in\",\n            package_source(host)\n        )),\n        Package::Project {\n            name: host_name, ..\n        } => Some(format!(\n            \"tool {} / {} / {} {}{}\",\n            name,\n            tool_version(host_name, \"project\"),\n            \"node@project\",\n            \"npm@project\",\n            package_source(host)\n        )),\n        Package::Fetched(..) => None,\n    }\n}\n\n// These tests are organized by way of the *item* being printed, unlike in the\n// `human` module, because the formatting is consistent across command formats.\n#[cfg(test)]\nmod tests {\n    use std::path::PathBuf;\n\n    use node_semver::Version;\n    use once_cell::sync::Lazy;\n\n    use crate::command::list::PackageDetails;\n\n    static NODE_VERSION: Lazy<Version> = Lazy::new(|| Version::from((12, 4, 0)));\n    static TYPESCRIPT_VERSION: Lazy<Version> = Lazy::new(|| Version::from((3, 4, 1)));\n    static NPM_VERSION: Lazy<Version> = Lazy::new(|| Version::from((6, 13, 4)));\n    static YARN_VERSION: Lazy<Version> = Lazy::new(|| Version::from((1, 16, 0)));\n    static PROJECT_PATH: Lazy<PathBuf> = Lazy::new(|| PathBuf::from(\"/a/b/c\"));\n\n    mod node {\n        use super::super::*;\n        use super::*;\n\n        #[test]\n        fn default() {\n            let source = Source::Default;\n            assert_eq!(\n                display_node(&source, &NODE_VERSION).as_str(),\n                \"runtime node@12.4.0 (default)\"\n            );\n        }\n\n        #[test]\n        fn project() {\n            let source = Source::Project(PROJECT_PATH.clone());\n            assert_eq!(\n                display_node(&source, &NODE_VERSION).as_str(),\n                \"runtime node@12.4.0 (current @ /a/b/c)\"\n            );\n        }\n\n        #[test]\n        fn installed_not_set() {\n            let source = Source::None;\n            assert_eq!(\n                display_node(&source, &NODE_VERSION).as_str(),\n                \"runtime node@12.4.0\"\n            );\n        }\n    }\n\n    mod npm {\n        use super::super::*;\n        use super::*;\n        use crate::command::list::*;\n\n        #[test]\n        fn default() {\n            assert_eq!(\n                display_package_manager(&PackageManager {\n                    kind: PackageManagerKind::Npm,\n                    source: Source::Default,\n                    version: NPM_VERSION.clone(),\n                })\n                .as_str(),\n                \"package-manager npm@6.13.4 (default)\"\n            );\n        }\n\n        #[test]\n        fn project() {\n            assert_eq!(\n                display_package_manager(&PackageManager {\n                    kind: PackageManagerKind::Npm,\n                    source: Source::Project(PROJECT_PATH.clone()),\n                    version: NPM_VERSION.clone(),\n                })\n                .as_str(),\n                \"package-manager npm@6.13.4 (current @ /a/b/c)\"\n            );\n        }\n\n        #[test]\n        fn installed_not_set() {\n            assert_eq!(\n                display_package_manager(&PackageManager {\n                    kind: PackageManagerKind::Npm,\n                    source: Source::None,\n                    version: NPM_VERSION.clone(),\n                })\n                .as_str(),\n                \"package-manager npm@6.13.4\"\n            );\n        }\n    }\n\n    mod yarn {\n        use super::super::*;\n        use super::*;\n        use crate::command::list::*;\n\n        #[test]\n        fn default() {\n            assert_eq!(\n                display_package_manager(&PackageManager {\n                    kind: PackageManagerKind::Yarn,\n                    source: Source::Default,\n                    version: YARN_VERSION.clone(),\n                })\n                .as_str(),\n                \"package-manager yarn@1.16.0 (default)\"\n            );\n        }\n\n        #[test]\n        fn project() {\n            assert_eq!(\n                display_package_manager(&PackageManager {\n                    kind: PackageManagerKind::Yarn,\n                    source: Source::Project(PROJECT_PATH.clone()),\n                    version: YARN_VERSION.clone()\n                })\n                .as_str(),\n                \"package-manager yarn@1.16.0 (current @ /a/b/c)\"\n            );\n        }\n\n        #[test]\n        fn installed_not_set() {\n            assert_eq!(\n                display_package_manager(&PackageManager {\n                    kind: PackageManagerKind::Yarn,\n                    source: Source::None,\n                    version: YARN_VERSION.clone()\n                })\n                .as_str(),\n                \"package-manager yarn@1.16.0\"\n            );\n        }\n    }\n\n    mod package {\n        use super::super::*;\n        use super::*;\n\n        #[test]\n        fn single_default() {\n            assert_eq!(\n                describe_packages(&[Package::Default {\n                    details: PackageDetails {\n                        name: \"typescript\".into(),\n                        version: TYPESCRIPT_VERSION.clone(),\n                    },\n                    node: NODE_VERSION.clone(),\n                    tools: vec![\"tsc\".into(), \"tsserver\".into()]\n                }])\n                .expect(\"Should always return a `String` if given a non-empty set\")\n                .as_str(),\n                \"package typescript@3.4.1 / tsc, tsserver / node@12.4.0 npm@built-in (default)\"\n            );\n        }\n\n        #[test]\n        fn single_project() {\n            assert_eq!(\n                describe_packages(&[Package::Project {\n                    name: \"typescript\".into(),\n                    path: PROJECT_PATH.clone(),\n                    tools: vec![\"tsc\".into(), \"tsserver\".into()]\n                }])\n                .expect(\"Should always return a `String` if given a non-empty set\")\n                .as_str(),\n                \"package typescript@project / tsc, tsserver / node@project npm@project (current @ /a/b/c)\"\n            );\n        }\n\n        #[test]\n        fn mixed() {\n            assert_eq!(\n                describe_packages(&[\n                    Package::Project {\n                        name: \"typescript\".into(),\n                        path: PROJECT_PATH.clone(),\n                        tools: vec![\"tsc\".into(), \"tsserver\".into()]\n                    },\n                    Package::Default {\n                        details: PackageDetails {\n                            name: \"ember-cli\".into(),\n                            version: Version::from((3, 10, 0)),\n                        },\n                        node: NODE_VERSION.clone(),\n                        tools: vec![\"ember\".into()],\n                    },\n                    Package::Fetched(PackageDetails {\n                        name: \"create-react-app\".into(),\n                        version: Version::from((1, 0, 0)),\n                    })\n                ])\n                .expect(\"Should always return a `String` if given a non-empty set\")\n                .as_str(),\n                \"package typescript@project / tsc, tsserver / node@project npm@project (current @ /a/b/c)\\n\\\n                 package ember-cli@3.10.0 / ember / node@12.4.0 npm@built-in (default)\\n\\\n                 package create-react-app@1.0.0 (fetched)\"\n            );\n        }\n\n        #[test]\n        fn installed_not_set() {\n            assert_eq!(\n                describe_packages(&[Package::Fetched(PackageDetails {\n                    name: \"typescript\".into(),\n                    version: TYPESCRIPT_VERSION.clone(),\n                })])\n                .expect(\"Should always return a `String` if given a non-empty set\")\n                .as_str(),\n                \"package typescript@3.4.1 (fetched)\"\n            );\n        }\n    }\n\n    mod tool {\n        use super::super::*;\n        use super::*;\n\n        #[test]\n        fn default() {\n            assert_eq!(\n                display_tool(\n                    \"tsc\",\n                    &Package::Default {\n                        details: PackageDetails {\n                            name: \"typescript\".into(),\n                            version: TYPESCRIPT_VERSION.clone(),\n                        },\n                        node: NODE_VERSION.clone(),\n                        tools: vec![\"tsc\".into(), \"tsserver\".into()],\n                    }\n                )\n                .expect(\"should always return `Some` for `Default`\")\n                .as_str(),\n                \"tool tsc / typescript@3.4.1 / node@12.4.0 npm@built-in (default)\"\n            );\n        }\n\n        #[test]\n        fn project() {\n            assert_eq!(\n                display_tool(\n                    \"tsc\",\n                    &Package::Project {\n                        name: \"typescript\".into(),\n                        path: PROJECT_PATH.clone(),\n                        tools: vec![\"tsc\".into(), \"tsserver\".into()],\n                    }\n                )\n                .expect(\"should always return `Some` for `Project`\")\n                .as_str(),\n                \"tool tsc / typescript@project / node@project npm@project (current @ /a/b/c)\"\n            );\n        }\n\n        #[test]\n        fn fetched() {\n            assert_eq!(\n                display_tool(\n                    \"tsc\",\n                    &Package::Fetched(PackageDetails {\n                        name: \"typescript\".into(),\n                        version: TYPESCRIPT_VERSION.clone()\n                    })\n                ),\n                None\n            );\n        }\n    }\n\n    mod toolchain {\n        use super::super::*;\n        use super::*;\n        use crate::command::list::{Node, PackageManager, PackageManagerKind, Toolchain};\n\n        #[test]\n        fn full() {\n            assert_eq!(\n                format(&Toolchain::All {\n                    runtimes: vec![\n                        Node {\n                            source: Source::Default,\n                            version: NODE_VERSION.clone()\n                        },\n                        Node {\n                            source: Source::None,\n                            version: Version::from((8, 2, 4))\n                        }\n                    ],\n                    package_managers: vec![\n                        PackageManager {\n                            kind: PackageManagerKind::Npm,\n                            source: Source::Project(PROJECT_PATH.clone()),\n                            version: NPM_VERSION.clone(),\n                        },\n                        PackageManager {\n                            kind: PackageManagerKind::Npm,\n                            source: Source::Default,\n                            version: Version::from((5, 10, 0))\n                        },\n                        PackageManager {\n                            kind: PackageManagerKind::Yarn,\n                            source: Source::Project(PROJECT_PATH.clone()),\n                            version: YARN_VERSION.clone()\n                        },\n                        PackageManager {\n                            kind: PackageManagerKind::Yarn,\n                            source: Source::Default,\n                            version: Version::from((1, 17, 0))\n                        }\n                    ],\n                    packages: vec![\n                        Package::Default {\n                            details: PackageDetails {\n                                name: \"ember-cli\".into(),\n                                version: Version::from((3, 10, 2)),\n                            },\n                            node: NODE_VERSION.clone(),\n                            tools: vec![\"ember\".into()]\n                        },\n                        Package::Project {\n                            name: \"ember-cli\".into(),\n                            path: PROJECT_PATH.clone(),\n                            tools: vec![\"ember\".into()]\n                        },\n                        Package::Default {\n                            details: PackageDetails {\n                                name: \"typescript\".into(),\n                                version: TYPESCRIPT_VERSION.clone(),\n                            },\n                            node: NODE_VERSION.clone(),\n                            tools: vec![\"tsc\".into(), \"tsserver\".into()]\n                        }\n                    ]\n                })\n                .expect(\"`format` with a non-empty toolchain returns `Some`\")\n                .as_str(),\n                \"runtime node@12.4.0 (default)\\n\\\n                 runtime node@8.2.4\\n\\\n                 package-manager npm@6.13.4 (current @ /a/b/c)\\n\\\n                 package-manager npm@5.10.0 (default)\\n\\\n                 package-manager yarn@1.16.0 (current @ /a/b/c)\\n\\\n                 package-manager yarn@1.17.0 (default)\\n\\\n                 package ember-cli@3.10.2 / ember / node@12.4.0 npm@built-in (default)\\n\\\n                 package ember-cli@project / ember / node@project npm@project (current @ /a/b/c)\\n\\\n                 package typescript@3.4.1 / tsc, tsserver / node@12.4.0 npm@built-in (default)\"\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "src/command/list/toolchain.rs",
    "content": "use super::{Filter, Node, Package, PackageManager, Source};\nuse crate::command::list::PackageManagerKind;\nuse node_semver::Version;\nuse volta_core::error::Fallible;\nuse volta_core::inventory::{\n    node_versions, npm_versions, package_configs, pnpm_versions, yarn_versions,\n};\nuse volta_core::platform::PlatformSpec;\nuse volta_core::project::Project;\nuse volta_core::tool::PackageConfig;\n\npub(super) enum Toolchain {\n    Node(Vec<Node>),\n    PackageManagers {\n        kind: PackageManagerKind,\n        managers: Vec<PackageManager>,\n    },\n    Packages(Vec<Package>),\n    Tool {\n        name: String,\n        host_packages: Vec<Package>,\n    },\n    Active {\n        runtime: Option<Box<Node>>,\n        package_managers: Vec<PackageManager>,\n        packages: Vec<Package>,\n    },\n    All {\n        runtimes: Vec<Node>,\n        package_managers: Vec<PackageManager>,\n        packages: Vec<Package>,\n    },\n}\n\n/// Lightweight rule for which item to get the `Source` for.\nenum Lookup {\n    /// Look up the Node runtime\n    Runtime,\n    /// Look up the npm package manager\n    Npm,\n    /// Look up the pnpm package manager\n    Pnpm,\n    /// Look up the Yarn package manager\n    Yarn,\n}\n\nimpl Lookup {\n    fn version_from_spec(&self) -> impl Fn(&PlatformSpec) -> Option<Version> + '_ {\n        move |spec| match self {\n            Lookup::Runtime => Some(spec.node.clone()),\n            Lookup::Npm => spec.npm.clone(),\n            Lookup::Pnpm => spec.pnpm.clone(),\n            Lookup::Yarn => spec.yarn.clone(),\n        }\n    }\n\n    fn version_source(\n        self,\n        project: Option<&Project>,\n        default_platform: Option<&PlatformSpec>,\n        version: &Version,\n    ) -> Source {\n        project\n            .and_then(|proj| {\n                proj.platform()\n                    .and_then(self.version_from_spec())\n                    .and_then(|project_version| {\n                        if &project_version == version {\n                            Some(Source::Project(proj.manifest_file().to_owned()))\n                        } else {\n                            None\n                        }\n                    })\n            })\n            .or_else(|| {\n                default_platform\n                    .and_then(self.version_from_spec())\n                    .and_then(|default_version| {\n                        if &default_version == version {\n                            Some(Source::Default)\n                        } else {\n                            None\n                        }\n                    })\n            })\n            .unwrap_or(Source::None)\n    }\n\n    /// Determine the `Source` for a given kind of tool (`Lookup`).\n    fn active_tool(\n        self,\n        project: Option<&Project>,\n        default: Option<&PlatformSpec>,\n    ) -> Option<(Source, Version)> {\n        project\n            .and_then(|proj| {\n                proj.platform()\n                    .and_then(self.version_from_spec())\n                    .map(|version| (Source::Project(proj.manifest_file().to_owned()), version))\n            })\n            .or_else(|| {\n                default\n                    .and_then(self.version_from_spec())\n                    .map(|version| (Source::Default, version))\n            })\n    }\n}\n\n/// Look up the `Source` for a tool with a given name.\nfn tool_source(name: &str, project: Option<&Project>) -> Fallible<Source> {\n    match project {\n        Some(project) => {\n            if project.has_direct_bin(name.as_ref())? {\n                Ok(Source::Project(project.manifest_file().to_owned()))\n            } else {\n                Ok(Source::Default)\n            }\n        }\n        _ => Ok(Source::Default),\n    }\n}\n\nimpl Toolchain {\n    pub(super) fn active(\n        project: Option<&Project>,\n        default_platform: Option<&PlatformSpec>,\n    ) -> Fallible<Toolchain> {\n        let runtime = Lookup::Runtime\n            .active_tool(project, default_platform)\n            .map(|(source, version)| Box::new(Node { source, version }));\n\n        let package_managers =\n            Lookup::Npm\n                .active_tool(project, default_platform)\n                .map(|(source, version)| PackageManager {\n                    kind: PackageManagerKind::Npm,\n                    source,\n                    version,\n                })\n                .into_iter()\n                .chain(Lookup::Pnpm.active_tool(project, default_platform).map(\n                    |(source, version)| PackageManager {\n                        kind: PackageManagerKind::Pnpm,\n                        source,\n                        version,\n                    },\n                ))\n                .chain(Lookup::Yarn.active_tool(project, default_platform).map(\n                    |(source, version)| PackageManager {\n                        kind: PackageManagerKind::Yarn,\n                        source,\n                        version,\n                    },\n                ))\n                .collect();\n\n        let packages = Package::from_inventory_and_project(project)?;\n\n        Ok(Toolchain::Active {\n            runtime,\n            package_managers,\n            packages,\n        })\n    }\n\n    pub(super) fn all(\n        project: Option<&Project>,\n        default_platform: Option<&PlatformSpec>,\n    ) -> Fallible<Toolchain> {\n        let runtimes = node_versions()?\n            .iter()\n            .map(|version| Node {\n                source: Lookup::Runtime.version_source(project, default_platform, version),\n                version: version.clone(),\n            })\n            .collect();\n\n        let package_managers = npm_versions()?\n            .iter()\n            .map(|version| PackageManager {\n                kind: PackageManagerKind::Npm,\n                source: Lookup::Npm.version_source(project, default_platform, version),\n                version: version.clone(),\n            })\n            .chain(pnpm_versions()?.iter().map(|version| PackageManager {\n                kind: PackageManagerKind::Pnpm,\n                source: Lookup::Pnpm.version_source(project, default_platform, version),\n                version: version.clone(),\n            }))\n            .chain(yarn_versions()?.iter().map(|version| PackageManager {\n                kind: PackageManagerKind::Yarn,\n                source: Lookup::Yarn.version_source(project, default_platform, version),\n                version: version.clone(),\n            }))\n            .collect();\n\n        let packages = Package::from_inventory_and_project(project)?;\n\n        Ok(Toolchain::All {\n            runtimes,\n            package_managers,\n            packages,\n        })\n    }\n\n    pub(super) fn node(\n        project: Option<&Project>,\n        default_platform: Option<&PlatformSpec>,\n        filter: &Filter,\n    ) -> Fallible<Toolchain> {\n        let runtimes = node_versions()?\n            .iter()\n            .filter_map(|version| {\n                let source = Lookup::Runtime.version_source(project, default_platform, version);\n                if source.allowed_with(filter) {\n                    let version = version.clone();\n                    Some(Node { source, version })\n                } else {\n                    None\n                }\n            })\n            .collect();\n\n        Ok(Toolchain::Node(runtimes))\n    }\n\n    pub(super) fn npm(\n        project: Option<&Project>,\n        default_platform: Option<&PlatformSpec>,\n        filter: &Filter,\n    ) -> Fallible<Toolchain> {\n        let managers = npm_versions()?\n            .iter()\n            .filter_map(|version| {\n                let source = Lookup::Npm.version_source(project, default_platform, version);\n                if source.allowed_with(filter) {\n                    Some(PackageManager {\n                        kind: PackageManagerKind::Npm,\n                        source,\n                        version: version.clone(),\n                    })\n                } else {\n                    None\n                }\n            })\n            .collect();\n\n        Ok(Toolchain::PackageManagers {\n            kind: PackageManagerKind::Npm,\n            managers,\n        })\n    }\n\n    pub(super) fn pnpm(\n        project: Option<&Project>,\n        default_platform: Option<&PlatformSpec>,\n        filter: &Filter,\n    ) -> Fallible<Toolchain> {\n        let managers = pnpm_versions()?\n            .iter()\n            .filter_map(|version| {\n                let source = Lookup::Pnpm.version_source(project, default_platform, version);\n                if source.allowed_with(filter) {\n                    Some(PackageManager {\n                        kind: PackageManagerKind::Pnpm,\n                        source,\n                        version: version.clone(),\n                    })\n                } else {\n                    None\n                }\n            })\n            .collect();\n\n        Ok(Toolchain::PackageManagers {\n            kind: PackageManagerKind::Pnpm,\n            managers,\n        })\n    }\n\n    pub(super) fn yarn(\n        project: Option<&Project>,\n        default_platform: Option<&PlatformSpec>,\n        filter: &Filter,\n    ) -> Fallible<Toolchain> {\n        let managers = yarn_versions()?\n            .iter()\n            .filter_map(|version| {\n                let source = Lookup::Yarn.version_source(project, default_platform, version);\n                if source.allowed_with(filter) {\n                    Some(PackageManager {\n                        kind: PackageManagerKind::Yarn,\n                        source,\n                        version: version.clone(),\n                    })\n                } else {\n                    None\n                }\n            })\n            .collect();\n\n        Ok(Toolchain::PackageManagers {\n            kind: PackageManagerKind::Yarn,\n            managers,\n        })\n    }\n\n    pub(super) fn package_or_tool(\n        name: &str,\n        project: Option<&Project>,\n        filter: &Filter,\n    ) -> Fallible<Toolchain> {\n        /// An internal-only helper for tracking whether we found a given item\n        /// from the `PackageCollection` as a *package* or as a *tool*.\n        #[derive(PartialEq, Debug)]\n        enum Kind {\n            Package,\n            Tool,\n        }\n\n        /// A convenient name for this tuple, since we have to name it in a few\n        /// spots below.\n        type Triple<'p> = (Kind, &'p PackageConfig, Source);\n\n        let configs = package_configs()?;\n        let packages_and_tools = configs\n            .iter()\n            .filter_map(|config| {\n                // Start with the package itself, since tools often match\n                // the package name and we prioritize packages.\n                if config.name == name {\n                    let source = Package::source(name, project);\n                    if source.allowed_with(filter) {\n                        Some(Ok((Kind::Package, config, source)))\n                    } else {\n                        None\n                    }\n\n                // Then check if the passed name matches an installed package's\n                // binaries. If it does, we have a tool.\n                } else if config.bins.iter().any(|bin| bin.as_str() == name) {\n                    tool_source(name, project)\n                        .map(|source| {\n                            if source.allowed_with(filter) {\n                                Some((Kind::Tool, config, source))\n                            } else {\n                                None\n                            }\n                        })\n                        .transpose()\n\n                // Otherwise, we don't have any match all.\n                } else {\n                    None\n                }\n            })\n            // Then eagerly collect the first error (if there are any) and\n            // return it; otherwise we have a totally valid collection.\n            .collect::<Fallible<Vec<Triple>>>()?;\n\n        let (has_packages, has_tools) =\n            packages_and_tools\n                .iter()\n                .fold((false, false), |(packages, tools), (kind, ..)| {\n                    (\n                        packages || kind == &Kind::Package,\n                        tools || kind == &Kind::Tool,\n                    )\n                });\n\n        let toolchain = match (has_packages, has_tools) {\n            // If there are neither packages nor tools, treat it as `Packages`,\n            // but don't re-process the data just to construct an empty `Vec`!\n            (false, false) => Toolchain::Packages(vec![]),\n            // If there are any packages, we resolve this *as* `Packages`, even\n            // if there are also matching tools, since we give priority to\n            // listing packages between packages and tools.\n            (true, _) => {\n                let packages = packages_and_tools\n                    .into_iter()\n                    .filter_map(|(kind, config, source)| match kind {\n                        Kind::Package => Some(Package::new(config, &source)),\n                        Kind::Tool => None,\n                    })\n                    .collect();\n\n                Toolchain::Packages(packages)\n            }\n            // If there are no packages matching, but we do have tools matching,\n            // we return `Tool`.\n            (false, true) => {\n                let host_packages = packages_and_tools\n                    .into_iter()\n                    .filter_map(|(kind, config, source)| match kind {\n                        Kind::Tool => Some(Package::new(config, &source)),\n                        Kind::Package => None, // should be none of these!\n                    })\n                    .collect();\n\n                Toolchain::Tool {\n                    name: name.into(),\n                    host_packages,\n                }\n            }\n        };\n\n        Ok(toolchain)\n    }\n}\n"
  },
  {
    "path": "src/command/mod.rs",
    "content": "pub(crate) mod completions;\npub(crate) mod fetch;\npub(crate) mod install;\npub(crate) mod list;\npub(crate) mod pin;\npub(crate) mod run;\npub(crate) mod setup;\npub(crate) mod uninstall;\npub(crate) mod r#use;\npub(crate) mod which;\n\npub(crate) use self::which::Which;\npub(crate) use completions::Completions;\npub(crate) use fetch::Fetch;\npub(crate) use install::Install;\npub(crate) use list::List;\npub(crate) use pin::Pin;\npub(crate) use r#use::Use;\npub(crate) use run::Run;\npub(crate) use setup::Setup;\npub(crate) use uninstall::Uninstall;\n\nuse volta_core::error::{ExitCode, Fallible};\nuse volta_core::session::Session;\n\n/// A Volta command.\npub(crate) trait Command: Sized {\n    /// Executes the command. Returns `Ok(true)` if the process should return 0,\n    /// `Ok(false)` if the process should return 1, and `Err(e)` if the process\n    /// should return `e.exit_code()`.\n    fn run(self, session: &mut Session) -> Fallible<ExitCode>;\n}\n"
  },
  {
    "path": "src/command/pin.rs",
    "content": "use volta_core::error::{ExitCode, Fallible};\nuse volta_core::session::{ActivityKind, Session};\nuse volta_core::tool::Spec;\n\nuse crate::command::Command;\n\n#[derive(clap::Args)]\npub(crate) struct Pin {\n    /// Tools to pin, like `node@lts` or `yarn@^1.14`.\n    #[arg(value_name = \"tool[@version]\", required = true)]\n    tools: Vec<String>,\n}\n\nimpl Command for Pin {\n    fn run(self, session: &mut Session) -> Fallible<ExitCode> {\n        session.add_event_start(ActivityKind::Pin);\n\n        for tool in Spec::from_strings(&self.tools, \"pin\")? {\n            tool.resolve(session)?.pin(session)?;\n        }\n\n        session.add_event_end(ActivityKind::Pin, ExitCode::Success);\n        Ok(ExitCode::Success)\n    }\n}\n"
  },
  {
    "path": "src/command/run.rs",
    "content": "use std::collections::HashMap;\nuse std::ffi::OsString;\n\nuse crate::command::Command;\nuse crate::common::{Error, IntoResult};\nuse log::warn;\nuse volta_core::error::{report_error, ExitCode, Fallible};\nuse volta_core::platform::{CliPlatform, InheritOption};\nuse volta_core::run::execute_tool;\nuse volta_core::session::{ActivityKind, Session};\nuse volta_core::tool::{node, npm, pnpm, yarn};\n\n#[derive(Debug, clap::Args)]\npub(crate) struct Run {\n    /// Set the custom Node version\n    #[arg(long, value_name = \"version\")]\n    node: Option<String>,\n\n    /// Set the custom npm version\n    #[arg(long, value_name = \"version\", conflicts_with = \"bundled_npm\")]\n    npm: Option<String>,\n\n    /// Forces npm to be the version bundled with Node\n    #[arg(long, conflicts_with = \"npm\")]\n    bundled_npm: bool,\n\n    /// Set the custon pnpm version\n    #[arg(long, value_name = \"version\", conflicts_with = \"no_pnpm\")]\n    pnpm: Option<String>,\n\n    /// Disables pnpm\n    #[arg(long, conflicts_with = \"pnpm\")]\n    no_pnpm: bool,\n\n    /// Set the custom Yarn version\n    #[arg(long, value_name = \"version\", conflicts_with = \"no_yarn\")]\n    yarn: Option<String>,\n\n    /// Disables Yarn\n    #[arg(long, conflicts_with = \"yarn\")]\n    no_yarn: bool,\n\n    /// Set an environment variable (can be used multiple times)\n    #[arg(long = \"env\", value_name = \"NAME=value\", num_args = 1)]\n    envs: Vec<String>,\n\n    /// The command to run, along with any arguments\n    #[arg(\n        allow_hyphen_values = true,\n        trailing_var_arg = true,\n        value_name = \"COMMAND\",\n        required = true\n    )]\n    command_and_args: Vec<OsString>,\n}\n\nimpl Command for Run {\n    fn run(self, session: &mut Session) -> Fallible<ExitCode> {\n        session.add_event_start(ActivityKind::Run);\n\n        let envs = self.parse_envs();\n        let platform = self.parse_platform(session)?;\n\n        // Safety: At least one value is required for `command_and_args`, so there must be at\n        // least one value in the list. If no value is provided, Clap will show a \"required\n        // argument missing\" message and this function won't be called.\n        let command = &self.command_and_args[0];\n        let args = &self.command_and_args[1..];\n\n        match execute_tool(command, args, &envs, platform, session).into_result() {\n            Ok(()) => {\n                session.add_event_end(ActivityKind::Run, ExitCode::Success);\n                Ok(ExitCode::Success)\n            }\n            Err(Error::Tool(code)) => {\n                session.add_event_tool_end(ActivityKind::Run, code);\n                Ok(ExitCode::ExecutionFailure)\n            }\n            Err(Error::Volta(err)) => {\n                report_error(env!(\"CARGO_PKG_VERSION\"), &err);\n                session.add_event_error(ActivityKind::Run, &err);\n                session.add_event_end(ActivityKind::Run, err.exit_code());\n                Ok(err.exit_code())\n            }\n        }\n    }\n}\n\nimpl Run {\n    /// Builds a CliPlatform from the provided cli options\n    ///\n    /// Will resolve a semver / tag version if necessary\n    fn parse_platform(&self, session: &mut Session) -> Fallible<CliPlatform> {\n        let node = self\n            .node\n            .as_ref()\n            .map(|version| node::resolve(version.parse()?, session))\n            .transpose()?;\n\n        let npm = match (self.bundled_npm, &self.npm) {\n            (true, _) => InheritOption::None,\n            (false, None) => InheritOption::Inherit,\n            (false, Some(version)) => match npm::resolve(version.parse()?, session)? {\n                None => InheritOption::Inherit,\n                Some(npm) => InheritOption::Some(npm),\n            },\n        };\n\n        let pnpm = match (self.no_pnpm, &self.pnpm) {\n            (true, _) => InheritOption::None,\n            (false, None) => InheritOption::Inherit,\n            (false, Some(version)) => {\n                InheritOption::Some(pnpm::resolve(version.parse()?, session)?)\n            }\n        };\n\n        let yarn = match (self.no_yarn, &self.yarn) {\n            (true, _) => InheritOption::None,\n            (false, None) => InheritOption::Inherit,\n            (false, Some(version)) => {\n                InheritOption::Some(yarn::resolve(version.parse()?, session)?)\n            }\n        };\n\n        Ok(CliPlatform {\n            node,\n            npm,\n            pnpm,\n            yarn,\n        })\n    }\n\n    /// Convert the environment variable settings passed to the command line into a map\n    ///\n    /// We ignore any setting that doesn't have a value associated with it\n    /// We also ignore the PATH environment variable as that is set when running a command\n    fn parse_envs(&self) -> HashMap<&str, &str> {\n        self.envs.iter().filter_map(|entry| {\n            let mut key_value = entry.splitn(2, '=');\n\n            match (key_value.next(), key_value.next()) {\n                (None, _) => None,\n                (Some(_), None) => None,\n                (Some(key), _) if key.eq_ignore_ascii_case(\"PATH\") => {\n                    warn!(\"Ignoring {} environment variable as it will be overwritten when executing the command\", key);\n                    None\n                }\n                (Some(key), Some(value)) => Some((key, value)),\n            }\n        }).collect()\n    }\n}\n"
  },
  {
    "path": "src/command/setup.rs",
    "content": "use log::info;\nuse volta_core::error::{ExitCode, Fallible};\nuse volta_core::layout::volta_home;\nuse volta_core::session::{ActivityKind, Session};\nuse volta_core::shim::regenerate_shims_for_dir;\nuse volta_core::style::success_prefix;\n\nuse crate::command::Command;\n\n#[derive(clap::Args)]\npub(crate) struct Setup {}\n\nimpl Command for Setup {\n    fn run(self, session: &mut Session) -> Fallible<ExitCode> {\n        session.add_event_start(ActivityKind::Setup);\n\n        os::setup_environment()?;\n        regenerate_shims_for_dir(volta_home()?.shim_dir())?;\n\n        info!(\n            \"{} Setup complete. Open a new terminal to start using Volta!\",\n            success_prefix()\n        );\n\n        session.add_event_end(ActivityKind::Setup, ExitCode::Success);\n        Ok(ExitCode::Success)\n    }\n}\n\n#[cfg(unix)]\nmod os {\n    use std::env;\n    use std::fs::File;\n    use std::io::{self, BufRead, BufReader, Write};\n    use std::path::{Path, PathBuf};\n\n    use log::{debug, warn};\n    use volta_core::error::{ErrorKind, Fallible};\n    use volta_core::layout::volta_home;\n\n    pub fn setup_environment() -> Fallible<()> {\n        let home = volta_home()?;\n        let formatted_home = format_home(home.root());\n\n        // Don't update the user's shell config files if VOLTA_HOME and PATH already contain what we need.\n        let home_in_path = match env::var_os(\"PATH\") {\n            Some(paths) => env::split_paths(&paths).find(|p| p == home.shim_dir()),\n            None => None,\n        };\n\n        if env::var_os(\"VOLTA_HOME\").is_some() && home_in_path.is_some() {\n            debug!(\n                \"Skipping dot-file modification as VOLTA_HOME is set, and included in the PATH.\"\n            );\n            return Ok(());\n        }\n\n        debug!(\"Searching for profiles to update\");\n        let profiles = determine_profiles()?;\n\n        let found_profile = profiles.into_iter().fold(false, |prev, profile| {\n            let contents = read_profile_without_volta(&profile).unwrap_or_default();\n\n            let write_profile = match profile.extension() {\n                Some(ext) if ext == \"fish\" => write_profile_fish,\n                _ => write_profile_sh,\n            };\n\n            match write_profile(&profile, contents, &formatted_home) {\n                Ok(()) => true,\n                Err(err) => {\n                    warn!(\n                        \"Found profile script, but could not modify it: {}\",\n                        profile.display()\n                    );\n                    debug!(\"Profile modification error: {}\", err);\n                    prev\n                }\n            }\n        });\n\n        if found_profile {\n            Ok(())\n        } else {\n            Err(ErrorKind::NoShellProfile {\n                env_profile: String::new(),\n                bin_dir: home.shim_dir().to_owned(),\n            }\n            .into())\n        }\n    }\n\n    /// Returns a list of profile files to modify / create.\n    ///\n    /// Any file in the list should be created if it doesn't already exist\n    fn determine_profiles() -> Fallible<Vec<PathBuf>> {\n        let home_dir = dirs::home_dir().ok_or(ErrorKind::NoHomeEnvironmentVar)?;\n        let shell = env::var(\"SHELL\").unwrap_or_else(|_| String::new());\n        // Always include `~/.profile`\n        let mut profiles = vec![home_dir.join(\".profile\")];\n\n        // PROFILE environment variable, if set\n        if let Ok(profile_env) = env::var(\"PROFILE\") {\n            if !profile_env.is_empty() {\n                profiles.push(profile_env.into());\n            }\n        }\n\n        add_zsh_profile(&home_dir, &shell, &mut profiles);\n        add_bash_profiles(&home_dir, &shell, &mut profiles);\n        add_fish_profile(&home_dir, &shell, &mut profiles);\n\n        Ok(profiles)\n    }\n\n    /// Add zsh profile script, if necessary\n    fn add_zsh_profile(home_dir: &Path, shell: &str, profiles: &mut Vec<PathBuf>) {\n        let zdotdir_env = env::var(\"ZDOTDIR\").unwrap_or_else(|_| String::new());\n        let zdotdir = if zdotdir_env.is_empty() {\n            home_dir\n        } else {\n            Path::new(&zdotdir_env)\n        };\n\n        let zshenv = zdotdir.join(\".zshenv\");\n\n        let zshrc = zdotdir.join(\".zshrc\");\n\n        if shell.contains(\"zsh\") || zshenv.exists() {\n            profiles.push(zshenv);\n        } else if zshrc.exists() {\n            profiles.push(zshrc);\n        }\n    }\n\n    /// Add bash profile scripts, if necessary\n    ///\n    /// Note: We only add the bash scripts if they already exist, as creating new files can impact\n    /// the processing of existing files in bash (e.g. preventing ~/.profile from being loaded)\n    fn add_bash_profiles(home_dir: &Path, shell: &str, profiles: &mut Vec<PathBuf>) {\n        let mut bash_added = false;\n\n        let bashrc = home_dir.join(\".bashrc\");\n        if bashrc.exists() {\n            bash_added = true;\n            profiles.push(bashrc);\n        }\n\n        let bash_profile = home_dir.join(\".bash_profile\");\n        if bash_profile.exists() {\n            bash_added = true;\n            profiles.push(bash_profile);\n        }\n\n        if shell.contains(\"bash\") && !bash_added {\n            let suggested_bash_profile = if cfg!(target_os = \"macos\") {\n                \"~/.bash_profile\"\n            } else {\n                \"~/.bashrc\"\n            };\n\n            warn!(\n                \"We detected that you are using bash, however we couldn't find any bash profile scripts.\nIf you run into problems running Volta, create {} and run `volta setup` again.\",\n                suggested_bash_profile\n            );\n        }\n    }\n\n    /// Add fish profile scripts, if necessary\n    fn add_fish_profile(home_dir: &Path, shell: &str, profiles: &mut Vec<PathBuf>) {\n        let fish_config = home_dir.join(\".config/fish/config.fish\");\n\n        if shell.contains(\"fish\") || fish_config.exists() {\n            profiles.push(fish_config);\n        }\n    }\n\n    fn read_profile_without_volta(path: &Path) -> Option<String> {\n        let file = File::open(path).ok()?;\n        let reader = BufReader::new(file);\n\n        reader\n            .lines()\n            .filter(|line_result| match line_result {\n                Ok(line) if !line.contains(\"VOLTA\") => true,\n                Ok(_) => false,\n                Err(_) => true,\n            })\n            .collect::<io::Result<Vec<String>>>()\n            .map(|lines| lines.join(\"\\n\"))\n            .ok()\n    }\n\n    fn format_home(volta_home: &Path) -> String {\n        if let Some(home_dir) = env::var_os(\"HOME\") {\n            if let Ok(suffix) = volta_home.strip_prefix(home_dir) {\n                // If the HOME environment variable is set _and_ the proposed VOLTA_HOME starts\n                // with that value, use $HOME when writing the profile scripts\n                return format!(\"$HOME/{}\", suffix.display());\n            }\n        }\n\n        volta_home.display().to_string()\n    }\n\n    fn write_profile_sh(path: &Path, contents: String, volta_home: &str) -> io::Result<()> {\n        let mut file = File::create(path)?;\n        write!(\n            file,\n            \"{}\\nexport VOLTA_HOME=\\\"{}\\\"\\nexport PATH=\\\"$VOLTA_HOME/bin:$PATH\\\"\\n\",\n            contents, volta_home,\n        )\n    }\n\n    fn write_profile_fish(path: &Path, contents: String, volta_home: &str) -> io::Result<()> {\n        let mut file = File::create(path)?;\n        write!(\n            file,\n            \"{}\\nset -gx VOLTA_HOME \\\"{}\\\"\\nset -gx PATH \\\"$VOLTA_HOME/bin\\\" $PATH\\n\",\n            contents, volta_home,\n        )\n    }\n}\n\n#[cfg(windows)]\nmod os {\n    use std::process::Command;\n\n    use log::debug;\n    use volta_core::error::{Context, ErrorKind, Fallible};\n    use volta_core::layout::volta_home;\n    use winreg::enums::HKEY_CURRENT_USER;\n    use winreg::RegKey;\n\n    pub fn setup_environment() -> Fallible<()> {\n        let shim_dir = volta_home()?.shim_dir().to_string_lossy().to_string();\n        let hkcu = RegKey::predef(HKEY_CURRENT_USER);\n        let env = hkcu\n            .open_subkey(\"Environment\")\n            .with_context(|| ErrorKind::ReadUserPathError)?;\n        let path: String = env\n            .get_value(\"Path\")\n            .with_context(|| ErrorKind::ReadUserPathError)?;\n\n        if !path.contains(&shim_dir) {\n            // Use `setx` command to edit the user Path environment variable\n            let mut command = Command::new(\"setx\");\n            command.arg(\"Path\");\n            command.arg(format!(\"{};{}\", shim_dir, path));\n\n            debug!(\"Modifying User Path with command: {:?}\", command);\n            let output = command\n                .output()\n                .with_context(|| ErrorKind::WriteUserPathError)?;\n\n            if !output.status.success() {\n                debug!(\"[setx stderr]\\n{}\", String::from_utf8_lossy(&output.stderr));\n                debug!(\"[setx stdout]\\n{}\", String::from_utf8_lossy(&output.stdout));\n                return Err(ErrorKind::WriteUserPathError.into());\n            }\n        }\n\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "src/command/uninstall.rs",
    "content": "use volta_core::error::{ErrorKind, ExitCode, Fallible};\nuse volta_core::session::{ActivityKind, Session};\nuse volta_core::tool;\nuse volta_core::version::VersionSpec;\n\nuse crate::command::Command;\n\n#[derive(clap::Args)]\npub(crate) struct Uninstall {\n    /// The tool to uninstall, like `ember-cli-update`, `typescript`, or <package>\n    tool: String,\n}\n\nimpl Command for Uninstall {\n    fn run(self, session: &mut Session) -> Fallible<ExitCode> {\n        session.add_event_start(ActivityKind::Uninstall);\n\n        let tool = tool::Spec::try_from_str(&self.tool)?;\n\n        // For packages, specifically report that we do not support uninstalling\n        // specific versions. For runtimes and package managers, we currently\n        // *intentionally* let this fall through to inform the user that we do\n        // not support uninstalling those *at all*.\n        if let tool::Spec::Package(_name, version) = &tool {\n            let VersionSpec::None = version else {\n                return Err(ErrorKind::Unimplemented {\n                    feature: \"uninstalling specific versions of tools\".into(),\n                }\n                .into());\n            };\n        }\n\n        tool.uninstall()?;\n\n        session.add_event_end(ActivityKind::Uninstall, ExitCode::Success);\n        Ok(ExitCode::Success)\n    }\n}\n"
  },
  {
    "path": "src/command/use.rs",
    "content": "use crate::command::Command;\nuse volta_core::error::{ErrorKind, ExitCode, Fallible};\nuse volta_core::session::{ActivityKind, Session};\n\n// NOTE: These use the same text as the `long_about` in crate::cli.\n//       It's hard to abstract since it's in an attribute string.\n\npub(crate) const USAGE: &str = \"The subcommand `use` is deprecated.\n\n    To install a tool in your toolchain, use `volta install`.\n    To pin your project's runtime or package manager, use `volta pin`.\n\";\n\nconst ADVICE: &str = \"\n    To install a tool in your toolchain, use `volta install`.\n    To pin your project's runtime or package manager, use `volta pin`.\n\";\n\n#[derive(clap::Args)]\npub(crate) struct Use {\n    #[allow(dead_code)]\n    anything: Vec<String>, // Prevent Clap argument errors when invoking e.g. `volta use node`\n}\n\nimpl Command for Use {\n    fn run(self, session: &mut Session) -> Fallible<ExitCode> {\n        session.add_event_start(ActivityKind::Help);\n        let result = Err(ErrorKind::DeprecatedCommandError {\n            command: \"use\".to_string(),\n            advice: ADVICE.to_string(),\n        }\n        .into());\n        session.add_event_end(ActivityKind::Help, ExitCode::InvalidArguments);\n        result\n    }\n}\n"
  },
  {
    "path": "src/command/which.rs",
    "content": "use std::env;\nuse std::ffi::OsString;\n\nuse which::which_in;\n\nuse volta_core::error::{Context, ErrorKind, ExitCode, Fallible};\nuse volta_core::platform::{Platform, System};\nuse volta_core::run::binary::DefaultBinary;\nuse volta_core::session::{ActivityKind, Session};\n\nuse crate::command::Command;\n\n#[derive(clap::Args)]\npub(crate) struct Which {\n    /// The binary to find, e.g. `node` or `npm`\n    binary: OsString,\n}\n\nimpl Command for Which {\n    // 1. Start by checking if the user has a tool installed in the project or\n    //    as a user default. If so, we're done.\n    // 2. Otherwise, use the platform image and/or the system environment to\n    //    determine a lookup path to run `which` in.\n    fn run(self, session: &mut Session) -> Fallible<ExitCode> {\n        session.add_event_start(ActivityKind::Which);\n\n        let default_tool = DefaultBinary::from_name(&self.binary, session)?;\n        let project_bin_path = session\n            .project()?\n            .and_then(|project| project.find_bin(&self.binary));\n\n        let tool_path = match (default_tool, project_bin_path) {\n            (Some(_), Some(bin_path)) => Some(bin_path),\n            (Some(tool), _) => Some(tool.bin_path),\n            _ => None,\n        };\n\n        if let Some(path) = tool_path {\n            println!(\"{}\", path.to_string_lossy());\n\n            let exit_code = ExitCode::Success;\n            session.add_event_end(ActivityKind::Which, exit_code);\n            return Ok(exit_code);\n        }\n\n        // Treat any error with obtaining the current platform image as if the image doesn't exist\n        // However, errors in obtaining the current working directory or the System path should\n        // still be treated as errors.\n        let path = match Platform::current(session)\n            .unwrap_or(None)\n            .and_then(|platform| platform.checkout(session).ok())\n            .and_then(|image| image.path().ok())\n        {\n            Some(path) => path,\n            None => System::path()?,\n        };\n\n        let cwd = env::current_dir().with_context(|| ErrorKind::CurrentDirError)?;\n        let exit_code = match which_in(&self.binary, Some(path), cwd) {\n            Ok(result) => {\n                println!(\"{}\", result.to_string_lossy());\n                ExitCode::Success\n            }\n            Err(_) => {\n                // `which_in` Will return an Err if it can't find the binary in the path\n                // In that case, we don't want to print anything out, but we want to return\n                // Exit Code 1 (ExitCode::UnknownError)\n                ExitCode::UnknownError\n            }\n        };\n\n        session.add_event_end(ActivityKind::Which, exit_code);\n        Ok(exit_code)\n    }\n}\n"
  },
  {
    "path": "src/common.rs",
    "content": "use std::process::{Command, ExitStatus};\n\nuse volta_core::error::{Context, ErrorKind, VoltaError};\nuse volta_core::layout::{volta_home, volta_install};\n\npub enum Error {\n    Volta(VoltaError),\n    Tool(i32),\n}\n\npub fn ensure_layout() -> Result<(), Error> {\n    let home = volta_home().map_err(Error::Volta)?;\n\n    if !home.layout_file().exists() {\n        let install = volta_install().map_err(Error::Volta)?;\n        Command::new(install.migrate_executable())\n            .env(\"VOLTA_LOGLEVEL\", format!(\"{}\", log::max_level()))\n            .status()\n            .with_context(|| ErrorKind::CouldNotStartMigration)\n            .into_result()?;\n    }\n\n    Ok(())\n}\n\npub trait IntoResult<T> {\n    fn into_result(self) -> Result<T, Error>;\n}\n\nimpl IntoResult<()> for Result<ExitStatus, VoltaError> {\n    fn into_result(self) -> Result<(), Error> {\n        match self {\n            Ok(status) => {\n                if status.success() {\n                    Ok(())\n                } else {\n                    let code = status.code().unwrap_or(1);\n                    Err(Error::Tool(code))\n                }\n            }\n            Err(err) => Err(Error::Volta(err)),\n        }\n    }\n}\n"
  },
  {
    "path": "src/main.rs",
    "content": "#[macro_use]\nmod command;\nmod cli;\n\nuse clap::Parser;\n\nuse volta_core::error::report_error;\nuse volta_core::log::{LogContext, LogVerbosity, Logger};\nuse volta_core::session::{ActivityKind, Session};\n\nmod common;\nuse common::{ensure_layout, Error};\n\n/// The entry point for the `volta` CLI.\npub fn main() {\n    let volta = cli::Volta::parse();\n    let verbosity = match (&volta.verbose, &volta.quiet) {\n        (false, false) => LogVerbosity::Default,\n        (true, false) => {\n            if volta.very_verbose {\n                LogVerbosity::VeryVerbose\n            } else {\n                LogVerbosity::Verbose\n            }\n        }\n        (false, true) => LogVerbosity::Quiet,\n        (true, true) => {\n            unreachable!(\"Clap should prevent the user from providing both --verbose and --quiet\")\n        }\n    };\n    Logger::init(LogContext::Volta, verbosity).expect(\"Only a single logger should be initialized\");\n    log::trace!(\"log level: {verbosity:?}\");\n\n    let mut session = Session::init();\n    session.add_event_start(ActivityKind::Volta);\n\n    let result = ensure_layout().and_then(|()| volta.run(&mut session).map_err(Error::Volta));\n    match result {\n        Ok(exit_code) => {\n            session.add_event_end(ActivityKind::Volta, exit_code);\n            session.exit(exit_code);\n        }\n        Err(Error::Tool(code)) => {\n            session.add_event_tool_end(ActivityKind::Volta, code);\n            session.exit_tool(code);\n        }\n        Err(Error::Volta(err)) => {\n            report_error(env!(\"CARGO_PKG_VERSION\"), &err);\n            session.add_event_error(ActivityKind::Volta, &err);\n            let code = err.exit_code();\n            session.add_event_end(ActivityKind::Volta, code);\n            session.exit(code);\n        }\n    }\n}\n"
  },
  {
    "path": "src/volta-migrate.rs",
    "content": "use volta_core::error::{report_error, ExitCode};\nuse volta_core::layout::volta_home;\nuse volta_core::log::{LogContext, LogVerbosity, Logger};\nuse volta_migrate::run_migration;\n\npub fn main() {\n    Logger::init(LogContext::Migration, LogVerbosity::Default)\n        .expect(\"Only a single Logger should be initialized\");\n\n    // In order to migrate the existing Volta directory while avoiding unconditional changes to the user's system,\n    // the Homebrew formula runs volta-migrate with `--no-create` flag in the post-install phase.\n    let no_create = matches!(std::env::args_os().nth(1), Some(flag) if flag == \"--no-create\");\n    if no_create && volta_home().map_or(true, |home| !home.root().exists()) {\n        ExitCode::Success.exit();\n    }\n\n    let exit_code = match run_migration() {\n        Ok(()) => ExitCode::Success,\n        Err(err) => {\n            report_error(env!(\"CARGO_PKG_VERSION\"), &err);\n            err.exit_code()\n        }\n    };\n\n    exit_code.exit();\n}\n"
  },
  {
    "path": "src/volta-shim.rs",
    "content": "mod common;\n\nuse common::{ensure_layout, Error, IntoResult};\nuse volta_core::error::{report_error, ExitCode};\nuse volta_core::log::{LogContext, LogVerbosity, Logger};\nuse volta_core::run::execute_shim;\nuse volta_core::session::{ActivityKind, Session};\nuse volta_core::signal::setup_signal_handler;\n\npub fn main() {\n    Logger::init(LogContext::Shim, LogVerbosity::Default)\n        .expect(\"Only a single Logger should be initialized\");\n    setup_signal_handler();\n\n    let mut session = Session::init();\n    session.add_event_start(ActivityKind::Tool);\n\n    let result = ensure_layout().and_then(|()| execute_shim(&mut session).into_result());\n    match result {\n        Ok(()) => {\n            session.add_event_end(ActivityKind::Tool, ExitCode::Success);\n            session.exit(ExitCode::Success);\n        }\n        Err(Error::Tool(code)) => {\n            session.add_event_tool_end(ActivityKind::Tool, code);\n            session.exit_tool(code);\n        }\n        Err(Error::Volta(err)) => {\n            report_error(env!(\"CARGO_PKG_VERSION\"), &err);\n            session.add_event_error(ActivityKind::Tool, &err);\n            session.add_event_end(ActivityKind::Tool, err.exit_code());\n            session.exit(ExitCode::ExecutionFailure);\n        }\n    }\n}\n"
  },
  {
    "path": "tests/acceptance/corrupted_download.rs",
    "content": "use crate::support::sandbox::{sandbox, DistroMetadata, NodeFixture, PnpmFixture, Yarn1Fixture};\nuse hamcrest2::assert_that;\nuse hamcrest2::prelude::*;\nuse node_semver::Version;\nuse test_support::matchers::execs;\n\nuse volta_core::error::ExitCode;\n\nconst NODE_VERSION_INFO: &str = r#\"[\n{\"version\":\"v10.99.1040\",\"npm\":\"6.2.26\",\"lts\": \"Dubnium\",\"files\":[\"linux-x64\",\"osx-x64-tar\",\"win-x64-zip\",\"win-x86-zip\", \"linux-arm64\"]},\n{\"version\":\"v0.0.1\",\"npm\":\"0.0.2\",\"lts\": \"Sure\",\"files\":[\"linux-x64\",\"osx-x64-tar\",\"win-x64-zip\",\"win-x86-zip\", \"linux-arm64\"]}\n]\n\"#;\n\nconst NODE_VERSION_FIXTURES: [DistroMetadata; 2] = [\n    DistroMetadata {\n        version: \"0.0.1\",\n        compressed_size: 10,\n        uncompressed_size: Some(0x0028_0000),\n    },\n    DistroMetadata {\n        version: \"10.99.1040\",\n        compressed_size: 273,\n        uncompressed_size: Some(0x0028_0000),\n    },\n];\n\nconst PNPM_VERSION_INFO: &str = r#\"\n{\n    \"name\":\"pnpm\",\n    \"dist-tags\": { \"latest\":\"7.7.1\" },\n    \"versions\": {\n        \"0.0.1\": { \"version\":\"0.0.1\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }},\n        \"7.7.1\": { \"version\":\"7.7.1\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }}\n    }\n}\n\"#;\n\nconst PNPM_VERSION_FIXTURES: [DistroMetadata; 2] = [\n    DistroMetadata {\n        version: \"0.0.1\",\n        compressed_size: 10,\n        uncompressed_size: Some(0x0028_0000),\n    },\n    DistroMetadata {\n        version: \"7.7.1\",\n        compressed_size: 518,\n        uncompressed_size: Some(0x0028_0000),\n    },\n];\n\nconst YARN_1_VERSION_INFO: &str = r#\"{\n    \"name\":\"yarn\",\n    \"dist-tags\": { \"latest\": \"1.2.42\" },\n    \"versions\": {\n        \"0.0.1\": { \"version\":\"0.0.1\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }},\n        \"1.2.42\": { \"version\":\"1.2.42\", \"dist\": { \"shasum:\"\", \"tarball\":\"\" }}\n    }\n}\"#;\n\nconst YARN_1_VERSION_FIXTURES: [DistroMetadata; 2] = [\n    DistroMetadata {\n        version: \"0.0.1\",\n        compressed_size: 10,\n        uncompressed_size: Some(0x0028_0000),\n    },\n    DistroMetadata {\n        version: \"1.2.42\",\n        compressed_size: 174,\n        uncompressed_size: Some(0x0028_0000),\n    },\n];\n\n#[test]\nfn install_corrupted_node_leaves_inventory_unchanged() {\n    let s = sandbox()\n        .node_available_versions(NODE_VERSION_INFO)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .build();\n\n    assert_that!(\n        s.volta(\"install node@0.0.1\"),\n        execs().with_status(ExitCode::UnknownError as i32)\n    );\n\n    assert!(!s.node_inventory_archive_exists(&Version::parse(\"0.0.1\").unwrap()));\n}\n\n#[test]\nfn install_valid_node_saves_to_inventory() {\n    let s = sandbox()\n        .node_available_versions(NODE_VERSION_INFO)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .build();\n\n    assert_that!(\n        s.volta(\"install node@10.99.1040\"),\n        execs().with_status(ExitCode::Success as i32)\n    );\n\n    assert!(s.node_inventory_archive_exists(&Version::parse(\"10.99.1040\").unwrap()));\n}\n\n#[test]\nfn install_corrupted_pnpm_leaves_inventory_unchanged() {\n    let s = sandbox()\n        .node_available_versions(NODE_VERSION_INFO)\n        .pnpm_available_versions(PNPM_VERSION_INFO)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .distro_mocks::<PnpmFixture>(&PNPM_VERSION_FIXTURES)\n        .env(\"VOLTA_FEATURE_PNPM\", \"1\")\n        .build();\n\n    assert_that!(\n        s.volta(\"install pnpm@0.0.1\"),\n        execs().with_status(ExitCode::UnknownError as i32)\n    );\n\n    assert!(!s.pnpm_inventory_archive_exists(\"0.0.1\"));\n}\n\n#[test]\nfn install_valid_pnpm_saves_to_inventory() {\n    let s = sandbox()\n        .platform(r#\"{ \"node\": { \"runtime\": \"1.2.3\", \"npm\": null }, \"yarn\": null }\"#)\n        .node_available_versions(NODE_VERSION_INFO)\n        .pnpm_available_versions(PNPM_VERSION_INFO)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .distro_mocks::<PnpmFixture>(&PNPM_VERSION_FIXTURES)\n        .env(\"VOLTA_FEATURE_PNPM\", \"1\")\n        .build();\n\n    assert_that!(\n        s.volta(\"install pnpm@7.7.1\"),\n        execs().with_status(ExitCode::Success as i32)\n    );\n\n    assert!(s.pnpm_inventory_archive_exists(\"7.7.1\"));\n}\n\n#[test]\nfn install_corrupted_yarn_leaves_inventory_unchanged() {\n    let s = sandbox()\n        .node_available_versions(NODE_VERSION_INFO)\n        .yarn_1_available_versions(YARN_1_VERSION_INFO)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .distro_mocks::<Yarn1Fixture>(&YARN_1_VERSION_FIXTURES)\n        .build();\n\n    assert_that!(\n        s.volta(\"install yarn@0.0.1\"),\n        execs().with_status(ExitCode::UnknownError as i32)\n    );\n\n    assert!(!s.yarn_inventory_archive_exists(\"0.0.1\"));\n}\n\n#[test]\nfn install_valid_yarn_saves_to_inventory() {\n    let s = sandbox()\n        .platform(r#\"{ \"node\": { \"runtime\": \"1.2.3\", \"npm\": null }, \"yarn\": null }\"#)\n        .node_available_versions(NODE_VERSION_INFO)\n        .yarn_1_available_versions(YARN_1_VERSION_INFO)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .distro_mocks::<Yarn1Fixture>(&YARN_1_VERSION_FIXTURES)\n        .build();\n\n    assert_that!(\n        s.volta(\"install yarn@1.2.42\"),\n        execs().with_status(ExitCode::Success as i32)\n    );\n\n    assert!(s.yarn_inventory_archive_exists(\"1.2.42\"));\n}\n"
  },
  {
    "path": "tests/acceptance/direct_install.rs",
    "content": "use crate::support::sandbox::{sandbox, DistroMetadata, NodeFixture, NpmFixture, Yarn1Fixture};\nuse hamcrest2::assert_that;\nuse hamcrest2::prelude::*;\nuse test_support::matchers::execs;\n\nuse volta_core::error::ExitCode;\n\nfn platform_with_node(node: &str) -> String {\n    format!(\n        r#\"{{\n  \"node\": {{\n    \"runtime\": \"{}\",\n    \"npm\": null\n  }},\n  \"yarn\": null\n}}\"#,\n        node\n    )\n}\n\nfn platform_with_node_yarn(node: &str, yarn: &str) -> String {\n    format!(\n        r#\"{{\n  \"node\": {{\n    \"runtime\": \"{}\",\n    \"npm\": null\n  }},\n  \"yarn\": \"{}\"\n}}\"#,\n        node, yarn\n    )\n}\n\nconst NODE_VERSION_INFO: &str = r#\"[\n{\"version\":\"v10.99.1040\",\"npm\":\"6.2.26\",\"lts\": \"Dubnium\",\"files\":[\"linux-x64\",\"osx-x64-tar\",\"win-x64-zip\",\"win-x86-zip\", \"linux-arm64\"]},\n{\"version\":\"v9.27.6\",\"npm\":\"5.6.17\",\"lts\": false,\"files\":[\"linux-x64\",\"osx-x64-tar\",\"win-x64-zip\",\"win-x86-zip\", \"linux-arm64\"]},\n{\"version\":\"v8.9.10\",\"npm\":\"5.6.7\",\"lts\": false,\"files\":[\"linux-x64\",\"osx-x64-tar\",\"win-x64-zip\",\"win-x86-zip\", \"linux-arm64\"]},\n{\"version\":\"v6.19.62\",\"npm\":\"3.10.1066\",\"lts\": false,\"files\":[\"linux-x64\",\"osx-x64-tar\",\"win-x64-zip\",\"win-x86-zip\", \"linux-arm64\"]}\n]\n\"#;\n\ncfg_if::cfg_if! {\n    if #[cfg(target_os = \"macos\")] {\n        const NODE_VERSION_FIXTURES: [DistroMetadata; 4] = [\n            DistroMetadata {\n                version: \"10.99.1040\",\n                compressed_size: 273,\n                uncompressed_size: Some(0x0028_0000),\n            },\n            DistroMetadata {\n                version: \"9.27.6\",\n                compressed_size: 272,\n                uncompressed_size: Some(0x0028_0000),\n            },\n            DistroMetadata {\n                version: \"8.9.10\",\n                compressed_size: 272,\n                uncompressed_size: Some(0x0028_0000),\n            },\n            DistroMetadata {\n                version: \"6.19.62\",\n                compressed_size: 273,\n                uncompressed_size: Some(0x0028_0000),\n            },\n        ];\n    } else if #[cfg(target_os = \"linux\")] {\n        const NODE_VERSION_FIXTURES: [DistroMetadata; 4] = [\n            DistroMetadata {\n                version: \"10.99.1040\",\n                compressed_size: 273,\n                uncompressed_size: Some(0x0028_0000),\n            },\n            DistroMetadata {\n                version: \"9.27.6\",\n                compressed_size: 272,\n                uncompressed_size: Some(0x0028_0000),\n            },\n            DistroMetadata {\n                version: \"8.9.10\",\n                compressed_size: 270,\n                uncompressed_size: Some(0x0028_0000),\n            },\n            DistroMetadata {\n                version: \"6.19.62\",\n                compressed_size: 273,\n                uncompressed_size: Some(0x0028_0000),\n            },\n        ];\n    } else if #[cfg(target_os = \"windows\")] {\n        const NODE_VERSION_FIXTURES: [DistroMetadata; 4] = [\n            DistroMetadata {\n                version: \"10.99.1040\",\n                compressed_size: 1096,\n                uncompressed_size: None,\n            },\n            DistroMetadata {\n                version: \"9.27.6\",\n                compressed_size: 1068,\n                uncompressed_size: None,\n            },\n            DistroMetadata {\n                version: \"8.9.10\",\n                compressed_size: 1055,\n                uncompressed_size: None,\n            },\n            DistroMetadata {\n                version: \"6.19.62\",\n                compressed_size: 1056,\n                uncompressed_size: None,\n            },\n        ];\n    } else {\n        compile_error!(\"Unsupported target_os for tests (expected 'macos', 'linux', or 'windows').\");\n    }\n}\n\nconst YARN_1_VERSION_INFO: &str = r#\"[\n{\"tag_name\":\"v1.2.42\",\"assets\":[{\"name\":\"yarn-v1.2.42.tar.gz\"}]},\n{\"tag_name\":\"v1.3.1\",\"assets\":[{\"name\":\"yarn-v1.3.1.msi\"}]},\n{\"tag_name\":\"v1.4.159\",\"assets\":[{\"name\":\"yarn-v1.4.159.tar.gz\"}]},\n{\"tag_name\":\"v1.7.71\",\"assets\":[{\"name\":\"yarn-v1.7.71.tar.gz\"}]},\n{\"tag_name\":\"v1.12.99\",\"assets\":[{\"name\":\"yarn-v1.12.99.tar.gz\"}]}\n]\"#;\n\nconst YARN_1_VERSION_FIXTURES: [DistroMetadata; 4] = [\n    DistroMetadata {\n        version: \"1.12.99\",\n        compressed_size: 178,\n        uncompressed_size: Some(0x0028_0000),\n    },\n    DistroMetadata {\n        version: \"1.7.71\",\n        compressed_size: 176,\n        uncompressed_size: Some(0x0028_0000),\n    },\n    DistroMetadata {\n        version: \"1.4.159\",\n        compressed_size: 177,\n        uncompressed_size: Some(0x0028_0000),\n    },\n    DistroMetadata {\n        version: \"1.2.42\",\n        compressed_size: 174,\n        uncompressed_size: Some(0x0028_0000),\n    },\n];\n\nconst NPM_VERSION_INFO: &str = r#\"\n{\n    \"name\":\"npm\",\n    \"dist-tags\": { \"latest\":\"8.1.5\" },\n    \"versions\": {\n        \"1.2.3\": { \"version\":\"1.2.3\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }},\n        \"4.5.6\": { \"version\":\"4.5.6\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }},\n        \"8.1.5\": { \"version\":\"8.1.5\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }}\n    }\n}\n\"#;\n\nconst NPM_VERSION_FIXTURES: [DistroMetadata; 3] = [\n    DistroMetadata {\n        version: \"1.2.3\",\n        compressed_size: 239,\n        uncompressed_size: Some(0x0028_0000),\n    },\n    DistroMetadata {\n        version: \"4.5.6\",\n        compressed_size: 239,\n        uncompressed_size: Some(0x0028_0000),\n    },\n    DistroMetadata {\n        version: \"8.1.5\",\n        compressed_size: 239,\n        uncompressed_size: Some(0x0028_0000),\n    },\n];\n\n#[test]\nfn npm_global_install_node_intercepts() {\n    let s = sandbox()\n        .platform(&platform_with_node(\"6.19.62\"))\n        .node_available_versions(NODE_VERSION_INFO)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .env(\"VOLTA_LOGLEVEL\", \"info\")\n        .build();\n\n    assert_that!(\n        s.npm(\"i -g node@10.99.1040\"),\n        execs()\n            .with_status(ExitCode::Success as i32)\n            .with_stdout_contains(\"[..]using Volta to install Node\")\n            .with_stdout_contains(\"[..]installed and set node@10.99.1040[..]\")\n    );\n}\n\n#[test]\nfn yarn_global_add_node_intercepts() {\n    let s = sandbox()\n        .platform(&platform_with_node(\"6.19.62\"))\n        .node_available_versions(NODE_VERSION_INFO)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .env(\"VOLTA_LOGLEVEL\", \"info\")\n        .build();\n\n    assert_that!(\n        s.yarn(\"global add node@9.27.6\"),\n        execs()\n            .with_status(ExitCode::Success as i32)\n            .with_stdout_contains(\"[..]using Volta to install Node\")\n            .with_stdout_contains(\"[..]installed and set node@9.27.6[..]\")\n    );\n}\n\n#[test]\nfn npm_global_install_npm_intercepts() {\n    let s = sandbox()\n        .platform(&platform_with_node(\"10.99.1040\"))\n        .node_available_versions(NODE_VERSION_INFO)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .npm_available_versions(NPM_VERSION_INFO)\n        .distro_mocks::<NpmFixture>(&NPM_VERSION_FIXTURES)\n        .env(\"VOLTA_LOGLEVEL\", \"info\")\n        .build();\n\n    assert_that!(\n        s.npm(\"i -g npm@8.1.5\"),\n        execs()\n            .with_status(ExitCode::Success as i32)\n            .with_stdout_contains(\"[..]using Volta to install npm\")\n            .with_stdout_contains(\"[..]installed and set npm@8.1.5 as default\")\n    );\n}\n\n#[test]\nfn yarn_global_add_npm_intercepts() {\n    let s = sandbox()\n        .platform(&platform_with_node(\"10.99.1040\"))\n        .node_available_versions(NODE_VERSION_INFO)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .npm_available_versions(NPM_VERSION_INFO)\n        .distro_mocks::<NpmFixture>(&NPM_VERSION_FIXTURES)\n        .env(\"VOLTA_LOGLEVEL\", \"info\")\n        .build();\n\n    assert_that!(\n        s.yarn(\"global add npm@4.5.6\"),\n        execs()\n            .with_status(ExitCode::Success as i32)\n            .with_stdout_contains(\"[..]using Volta to install npm\")\n            .with_stdout_contains(\"[..]installed and set npm@4.5.6 as default\")\n    );\n}\n\n#[test]\nfn npm_global_install_yarn_intercepts() {\n    let s = sandbox()\n        .platform(&platform_with_node(\"10.99.1040\"))\n        .node_available_versions(NODE_VERSION_INFO)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .yarn_1_available_versions(YARN_1_VERSION_INFO)\n        .distro_mocks::<Yarn1Fixture>(&YARN_1_VERSION_FIXTURES)\n        .env(\"VOLTA_LOGLEVEL\", \"info\")\n        .build();\n\n    assert_that!(\n        s.npm(\"i -g yarn@1.12.99\"),\n        execs()\n            .with_status(ExitCode::Success as i32)\n            .with_stdout_contains(\"[..]using Volta to install Yarn\")\n            .with_stdout_contains(\"[..]installed and set yarn@1.12.99 as default\")\n    );\n}\n\n#[test]\nfn yarn_global_add_yarn_intercepts() {\n    let s = sandbox()\n        .platform(&platform_with_node(\"10.99.1040\"))\n        .node_available_versions(NODE_VERSION_INFO)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .yarn_1_available_versions(YARN_1_VERSION_INFO)\n        .distro_mocks::<Yarn1Fixture>(&YARN_1_VERSION_FIXTURES)\n        .env(\"VOLTA_LOGLEVEL\", \"info\")\n        .build();\n\n    assert_that!(\n        s.yarn(\"global add yarn@1.7.71\"),\n        execs()\n            .with_status(ExitCode::Success as i32)\n            .with_stdout_contains(\"[..]using Volta to install Yarn\")\n            .with_stdout_contains(\"[..]installed and set yarn@1.7.71 as default\")\n    );\n}\n\n#[test]\nfn npm_global_install_supports_multiples() {\n    let s = sandbox()\n        .platform(&platform_with_node(\"10.99.1040\"))\n        .node_available_versions(NODE_VERSION_INFO)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .npm_available_versions(NPM_VERSION_INFO)\n        .distro_mocks::<NpmFixture>(&NPM_VERSION_FIXTURES)\n        .yarn_1_available_versions(YARN_1_VERSION_INFO)\n        .distro_mocks::<Yarn1Fixture>(&YARN_1_VERSION_FIXTURES)\n        .env(\"VOLTA_LOGLEVEL\", \"info\")\n        .build();\n\n    assert_that!(\n        s.npm(\"i -g npm@8.1.5 yarn@1.12.99\"),\n        execs()\n            .with_status(ExitCode::Success as i32)\n            .with_stdout_contains(\"[..]Volta is processing each package separately\")\n            .with_stdout_contains(\"[..]using Volta to install npm\")\n            .with_stdout_contains(\"[..]installed and set npm@8.1.5 as default\")\n            .with_stdout_contains(\"[..]using Volta to install Yarn\")\n            .with_stdout_contains(\"[..]installed and set yarn@1.12.99 as default\")\n    );\n}\n\n#[test]\nfn npm_global_install_without_packages_is_treated_as_not_global() {\n    let s = sandbox()\n        .platform(&platform_with_node(\"10.99.1040\"))\n        .node_available_versions(NODE_VERSION_INFO)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .env(\"VOLTA_LOGLEVEL\", \"info\")\n        .build();\n\n    assert_that!(\n        s.npm(\"i --global\"),\n        execs()\n            .with_status(ExitCode::Success as i32)\n            .with_stdout_does_not_contain(\"[..]Volta is processing each package separately\")\n    );\n}\n\n#[test]\nfn yarn_global_add_supports_multiples() {\n    let s = sandbox()\n        .platform(&platform_with_node(\"10.99.1040\"))\n        .node_available_versions(NODE_VERSION_INFO)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .npm_available_versions(NPM_VERSION_INFO)\n        .distro_mocks::<NpmFixture>(&NPM_VERSION_FIXTURES)\n        .yarn_1_available_versions(YARN_1_VERSION_INFO)\n        .distro_mocks::<Yarn1Fixture>(&YARN_1_VERSION_FIXTURES)\n        .env(\"VOLTA_LOGLEVEL\", \"info\")\n        .build();\n\n    assert_that!(\n        s.yarn(\"global add npm@8.1.5 yarn@1.12.99\"),\n        execs()\n            .with_status(ExitCode::Success as i32)\n            .with_stdout_contains(\"[..]Volta is processing each package separately\")\n            .with_stdout_contains(\"[..]using Volta to install npm\")\n            .with_stdout_contains(\"[..]installed and set npm@8.1.5 as default\")\n            .with_stdout_contains(\"[..]using Volta to install Yarn\")\n            .with_stdout_contains(\"[..]installed and set yarn@1.12.99 as default\")\n    );\n}\n\n#[test]\nfn yarn_global_add_without_packages_is_treated_as_not_global() {\n    let s = sandbox()\n        .platform(&platform_with_node_yarn(\"10.99.1040\", \"1.2.42\"))\n        .node_available_versions(NODE_VERSION_INFO)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .yarn_1_available_versions(YARN_1_VERSION_INFO)\n        .distro_mocks::<Yarn1Fixture>(&YARN_1_VERSION_FIXTURES)\n        .env(\"VOLTA_LOGLEVEL\", \"info\")\n        .build();\n\n    assert_that!(\n        s.yarn(\"global add\"),\n        execs()\n            .with_status(ExitCode::Success as i32)\n            .with_stdout_does_not_contain(\"[..]Volta is processing each package separately\")\n    );\n}\n\n#[test]\nfn npm_global_with_override_does_not_intercept() {\n    let s = sandbox()\n        .platform(&platform_with_node(\"10.99.1040\"))\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .env(\"VOLTA_LOGLEVEL\", \"info\")\n        .env(\"VOLTA_UNSAFE_GLOBAL\", \"1\")\n        .build();\n\n    assert_that!(\n        s.npm(\"install --global npm@8\"),\n        execs()\n            .with_status(ExitCode::Success as i32)\n            .with_stdout_does_not_contain(\"[..]using Volta to install npm\")\n    );\n}\n\n#[test]\nfn yarn_global_with_override_does_not_intercept() {\n    let s = sandbox()\n        .platform(&platform_with_node_yarn(\"10.99.1040\", \"1.12.99\"))\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .distro_mocks::<Yarn1Fixture>(&YARN_1_VERSION_FIXTURES)\n        .env(\"VOLTA_LOGLEVEL\", \"info\")\n        .env(\"VOLTA_UNSAFE_GLOBAL\", \"1\")\n        .build();\n\n    assert_that!(\n        s.yarn(\"global add npm@8\"),\n        execs()\n            .with_status(ExitCode::Success as i32)\n            .with_stdout_does_not_contain(\"[..]using Volta to install npm\")\n    );\n}\n"
  },
  {
    "path": "tests/acceptance/direct_uninstall.rs",
    "content": "//! Tests for `npm uninstall`, `npm uninstall --global`, `yarn remove`, and\n//! `yarn global remove`, which we support as alternatives to `volta uninstall`\n//! and which should use its logic.\n\nuse crate::support::sandbox::{sandbox, DistroMetadata, NodeFixture, Sandbox, Yarn1Fixture};\nuse hamcrest2::assert_that;\nuse hamcrest2::prelude::*;\nuse test_support::matchers::execs;\n\nfn platform_with_node(node: &str) -> String {\n    format!(\n        r#\"{{\n\"node\": {{\n  \"runtime\": \"{}\",\n  \"npm\": null\n}},\n\"yarn\": null\n}}\"#,\n        node\n    )\n}\n\nfn platform_with_node_yarn(node: &str, yarn: &str) -> String {\n    format!(\n        r#\"{{\n\"node\": {{\n  \"runtime\": \"{}\",\n  \"npm\": null\n}},\n\"yarn\": \"{}\"\n}}\"#,\n        node, yarn\n    )\n}\n\nconst PKG_CONFIG_COWSAY: &str = r#\"{\n    \"name\": \"cowsay\",\n    \"version\": \"1.4.0\",\n    \"platform\": {\n      \"node\": \"11.10.1\",\n      \"npm\": \"6.7.0\",\n      \"yarn\": null\n    },\n    \"bins\": [\n      \"cowsay\",\n      \"cowthink\"\n    ],\n    \"manager\": \"Npm\"\n  }\"#;\n\nconst PKG_CONFIG_TYPESCRIPT: &str = r#\"{\n  \"name\": \"typescript\",\n  \"version\": \"1.4.0\",\n  \"platform\": {\n    \"node\": \"11.10.1\",\n    \"npm\": \"6.7.0\",\n    \"yarn\": null\n  },\n  \"bins\": [\n    \"tsc\",\n    \"tsserver\"\n  ],\n  \"manager\": \"Npm\"\n}\"#;\n\nfn bin_config(name: &str, pkg: &str) -> String {\n    format!(\n        r#\"{{\n  \"name\": \"{}\",\n  \"package\": \"{}\",\n  \"version\": \"1.4.0\",\n  \"platform\": {{\n    \"node\": \"11.10.1\",\n    \"npm\": \"6.7.0\",\n    \"yarn\": null\n  }},\n  \"manager\": \"Npm\"\n}}\"#,\n        name, pkg\n    )\n}\n\ncfg_if::cfg_if! {\n    if #[cfg(target_os = \"macos\")] {\n        const NODE_VERSION_FIXTURES: [DistroMetadata; 1] = [\n            DistroMetadata {\n                version: \"10.99.1040\",\n                compressed_size: 273,\n                uncompressed_size: Some(0x0028_0000),\n            },\n        ];\n    } else if #[cfg(target_os = \"linux\")] {\n        const NODE_VERSION_FIXTURES: [DistroMetadata; 1] = [\n            DistroMetadata {\n                version: \"10.99.1040\",\n                compressed_size: 273,\n                uncompressed_size: Some(0x0028_0000),\n            },\n        ];\n    } else if #[cfg(target_os = \"windows\")] {\n        const NODE_VERSION_FIXTURES: [DistroMetadata; 1] = [\n            DistroMetadata {\n                version: \"10.99.1040\",\n                compressed_size: 1096,\n                uncompressed_size: None,\n            },\n        ];\n    } else {\n        compile_error!(\"Unsupported target_os for tests (expected 'macos', 'linux', or 'windows').\");\n    }\n}\n\nconst YARN_1_VERSION_FIXTURES: [DistroMetadata; 1] = [DistroMetadata {\n    version: \"1.2.42\",\n    compressed_size: 174,\n    uncompressed_size: Some(0x0028_0000),\n}];\n\n#[test]\nfn npm_uninstall_uses_volta_logic() {\n    let s = sandbox()\n        .platform(&platform_with_node(\"10.99.1040\"))\n        .package_config(\"cowsay\", PKG_CONFIG_COWSAY)\n        .binary_config(\"cowsay\", &bin_config(\"cowsay\", \"cowsay\"))\n        .binary_config(\"cowthink\", &bin_config(\"cowthink\", \"cowsay\"))\n        .shim(\"cowsay\")\n        .shim(\"cowthink\")\n        .package_image(\"cowsay\", \"1.4.0\", None)\n        .env(\"VOLTA_LOGLEVEL\", \"info\")\n        .build();\n\n    assert_that!(\n        s.npm(\"uninstall --global cowsay\"),\n        execs()\n            .with_status(0)\n            .with_stdout_contains(\"[..]using Volta to uninstall cowsay\")\n            .with_stdout_contains(\"Removed executable 'cowsay' installed by 'cowsay'\")\n            .with_stdout_contains(\"Removed executable 'cowthink' installed by 'cowsay'\")\n            .with_stdout_contains(\"[..]package 'cowsay' uninstalled\")\n    );\n\n    // check that everything is deleted\n    assert!(!Sandbox::package_config_exists(\"cowsay\"));\n    assert!(!Sandbox::bin_config_exists(\"cowsay\"));\n    assert!(!Sandbox::bin_config_exists(\"cowthink\"));\n    assert!(!Sandbox::shim_exists(\"cowsay\"));\n    assert!(!Sandbox::shim_exists(\"cowthink\"));\n    assert!(!Sandbox::package_image_exists(\"cowsay\"));\n}\n\n#[test]\nfn npm_uninstall_supports_multiples() {\n    let s = sandbox()\n        .platform(&platform_with_node(\"10.99.1040\"))\n        .package_config(\"cowsay\", PKG_CONFIG_COWSAY)\n        .binary_config(\"cowsay\", &bin_config(\"cowsay\", \"cowsay\"))\n        .binary_config(\"cowthink\", &bin_config(\"cowthink\", \"cowsay\"))\n        .shim(\"cowsay\")\n        .shim(\"cowthink\")\n        .package_image(\"cowsay\", \"1.4.0\", None)\n        .package_config(\"typescript\", PKG_CONFIG_TYPESCRIPT)\n        .binary_config(\"tsc\", &bin_config(\"tsc\", \"typescript\"))\n        .binary_config(\"tsserver\", &bin_config(\"tsserver\", \"typescript\"))\n        .shim(\"tsc\")\n        .shim(\"tsserver\")\n        .package_image(\"typescript\", \"1.4.0\", None)\n        .env(\"VOLTA_LOGLEVEL\", \"info\")\n        .build();\n\n    assert_that!(\n        s.npm(\"uninstall --global cowsay typescript\"),\n        execs()\n            .with_status(0)\n            .with_stdout_contains(\"[..]using Volta to uninstall cowsay\")\n            .with_stdout_contains(\"Removed executable 'cowsay' installed by 'cowsay'\")\n            .with_stdout_contains(\"Removed executable 'cowthink' installed by 'cowsay'\")\n            .with_stdout_contains(\"[..]package 'cowsay' uninstalled\")\n            .with_stdout_contains(\"[..]using Volta to uninstall typescript\")\n            .with_stdout_contains(\"Removed executable 'tsc' installed by 'typescript'\")\n            .with_stdout_contains(\"Removed executable 'tsserver' installed by 'typescript'\")\n            .with_stdout_contains(\"[..]package 'typescript' uninstalled\")\n    );\n\n    // check that everything is deleted\n    assert!(!Sandbox::package_config_exists(\"cowsay\"));\n    assert!(!Sandbox::bin_config_exists(\"cowsay\"));\n    assert!(!Sandbox::bin_config_exists(\"cowthink\"));\n    assert!(!Sandbox::shim_exists(\"cowsay\"));\n    assert!(!Sandbox::shim_exists(\"cowthink\"));\n    assert!(!Sandbox::package_image_exists(\"cowsay\"));\n\n    assert!(!Sandbox::package_config_exists(\"typescript\"));\n    assert!(!Sandbox::bin_config_exists(\"tsc\"));\n    assert!(!Sandbox::bin_config_exists(\"tsserver\"));\n    assert!(!Sandbox::shim_exists(\"tsc\"));\n    assert!(!Sandbox::shim_exists(\"tsserver\"));\n    assert!(!Sandbox::package_image_exists(\"typescript\"));\n}\n\n#[test]\nfn npm_uninstall_without_packages_skips_volta_logic() {\n    let s = sandbox()\n        .platform(&platform_with_node(\"10.99.1040\"))\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .env(\"VOLTA_LOGLEVEL\", \"info\")\n        .build();\n\n    assert_that!(\n        s.npm(\"uninstall -g\"),\n        execs()\n            .with_status(0)\n            .with_stdout_does_not_contain(\"[..]Volta is processing each package separately\")\n    );\n}\n\n#[test]\nfn yarn_remove_uses_volta_logic() {\n    let s = sandbox()\n        .platform(&platform_with_node(\"10.99.1040\"))\n        .package_config(\"cowsay\", PKG_CONFIG_COWSAY)\n        .binary_config(\"cowsay\", &bin_config(\"cowsay\", \"cowsay\"))\n        .binary_config(\"cowthink\", &bin_config(\"cowthink\", \"cowsay\"))\n        .shim(\"cowsay\")\n        .shim(\"cowthink\")\n        .package_image(\"cowsay\", \"1.4.0\", None)\n        .env(\"VOLTA_LOGLEVEL\", \"info\")\n        .build();\n\n    assert_that!(\n        s.yarn(\"global remove cowsay\"),\n        execs()\n            .with_status(0)\n            .with_stdout_contains(\"[..]using Volta to uninstall cowsay\")\n            .with_stdout_contains(\"Removed executable 'cowsay' installed by 'cowsay'\")\n            .with_stdout_contains(\"Removed executable 'cowthink' installed by 'cowsay'\")\n            .with_stdout_contains(\"[..]package 'cowsay' uninstalled\")\n    );\n\n    // check that everything is deleted\n    assert!(!Sandbox::package_config_exists(\"cowsay\"));\n    assert!(!Sandbox::bin_config_exists(\"cowsay\"));\n    assert!(!Sandbox::bin_config_exists(\"cowthink\"));\n    assert!(!Sandbox::shim_exists(\"cowsay\"));\n    assert!(!Sandbox::shim_exists(\"cowthink\"));\n    assert!(!Sandbox::package_image_exists(\"cowsay\"));\n}\n\n#[test]\nfn yarn_remove_supports_multiples() {\n    let s = sandbox()\n        .platform(&platform_with_node(\"10.99.1040\"))\n        .package_config(\"cowsay\", PKG_CONFIG_COWSAY)\n        .binary_config(\"cowsay\", &bin_config(\"cowsay\", \"cowsay\"))\n        .binary_config(\"cowthink\", &bin_config(\"cowthink\", \"cowsay\"))\n        .shim(\"cowsay\")\n        .shim(\"cowthink\")\n        .package_image(\"cowsay\", \"1.4.0\", None)\n        .package_config(\"typescript\", PKG_CONFIG_TYPESCRIPT)\n        .binary_config(\"tsc\", &bin_config(\"tsc\", \"typescript\"))\n        .binary_config(\"tsserver\", &bin_config(\"tsserver\", \"typescript\"))\n        .shim(\"tsc\")\n        .shim(\"tsserver\")\n        .package_image(\"typescript\", \"1.4.0\", None)\n        .env(\"VOLTA_LOGLEVEL\", \"info\")\n        .build();\n\n    assert_that!(\n        s.yarn(\"global remove cowsay typescript\"),\n        execs()\n            .with_status(0)\n            .with_stdout_contains(\"[..]using Volta to uninstall cowsay\")\n            .with_stdout_contains(\"Removed executable 'cowsay' installed by 'cowsay'\")\n            .with_stdout_contains(\"Removed executable 'cowthink' installed by 'cowsay'\")\n            .with_stdout_contains(\"[..]package 'cowsay' uninstalled\")\n            .with_stdout_contains(\"[..]using Volta to uninstall typescript\")\n            .with_stdout_contains(\"Removed executable 'tsc' installed by 'typescript'\")\n            .with_stdout_contains(\"Removed executable 'tsserver' installed by 'typescript'\")\n            .with_stdout_contains(\"[..]package 'typescript' uninstalled\")\n    );\n\n    // check that everything is deleted\n    assert!(!Sandbox::package_config_exists(\"cowsay\"));\n    assert!(!Sandbox::bin_config_exists(\"cowsay\"));\n    assert!(!Sandbox::bin_config_exists(\"cowthink\"));\n    assert!(!Sandbox::shim_exists(\"cowsay\"));\n    assert!(!Sandbox::shim_exists(\"cowthink\"));\n    assert!(!Sandbox::package_image_exists(\"cowsay\"));\n\n    assert!(!Sandbox::package_config_exists(\"typescript\"));\n    assert!(!Sandbox::bin_config_exists(\"tsc\"));\n    assert!(!Sandbox::bin_config_exists(\"tsserver\"));\n    assert!(!Sandbox::shim_exists(\"tsc\"));\n    assert!(!Sandbox::shim_exists(\"tsserver\"));\n    assert!(!Sandbox::package_image_exists(\"typescript\"));\n}\n\n#[test]\nfn yarn_remove_without_packages_skips_volta_logic() {\n    let s = sandbox()\n        .platform(&platform_with_node_yarn(\"10.99.1040\", \"1.2.42\"))\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .distro_mocks::<Yarn1Fixture>(&YARN_1_VERSION_FIXTURES)\n        .env(\"VOLTA_LOGLEVEL\", \"info\")\n        .build();\n\n    assert_that!(\n        s.yarn(\"global remove\"),\n        execs()\n            .with_status(0)\n            .with_stdout_does_not_contain(\"[..]Volta is processing each package separately\")\n    );\n}\n"
  },
  {
    "path": "tests/acceptance/execute_binary.rs",
    "content": "use std::path::PathBuf;\n\nuse crate::support::sandbox::{sandbox, PackageBinInfo};\nuse cfg_if::cfg_if;\nuse hamcrest2::assert_that;\nuse hamcrest2::prelude::*;\nuse test_support::matchers::execs;\n\nconst PKG_CONFIG_BASIC: &str = r#\"{\n  \"name\": \"cowsay\",\n  \"version\": \"1.4.0\",\n  \"platform\": {\n    \"node\": \"11.10.1\",\n    \"npm\": \"6.7.0\",\n    \"yarn\": null\n  },\n  \"bins\": [\n    \"cowsay\",\n    \"cowthink\"\n  ],\n  \"manager\": \"Npm\"\n}\"#;\n\nfn node_bin(version: &str) -> String {\n    cfg_if! {\n            if #[cfg(target_os = \"windows\")] {\n                format!(\n                    r#\"@echo off\necho Node version {}\necho node args: %*\n\"#,\n    version\n                )\n            } else {\n                format!(\n                    r#\"#!/bin/sh\necho \"Node version {}\"\necho \"node args: $@\"\n\"#,\n    version\n                )\n            }\n        }\n}\n\nfn npm_bin(version: &str) -> String {\n    cfg_if! {\n            if #[cfg(target_os = \"windows\")] {\n                format!(\n                    r#\"@echo off\necho Npm version {}\necho npm args: %*\n\"#,\n    version\n                )\n            } else {\n                format!(\n                    r#\"#!/bin/sh\necho \"Npm version {}\"\necho \"npm args: $@\"\n\"#,\n    version\n                )\n            }\n        }\n}\n\nfn pnpm_bin(version: &str) -> String {\n    cfg_if! {\n            if #[cfg(target_os = \"windows\")] {\n                format!(\n                    r#\"@echo off\necho pnpm version {}\necho pnpm args: %*\n\"#,\n    version\n                )\n            } else {\n                format!(\n                    r#\"#!/bin/sh\necho \"pnpm version {}\"\necho \"pnpm args: $@\"\n\"#,\n    version\n                )\n            }\n        }\n}\n\nfn yarn_bin(version: &str) -> String {\n    cfg_if! {\n            if #[cfg(target_os = \"windows\")] {\n                format!(\n                    r#\"@echo off\necho Yarn version {}\necho yarn args: %*\n\"#,\n    version\n                )\n            } else {\n                format!(\n                    r#\"#!/bin/sh\necho \"Yarn version {}\"\necho \"yarn args: $@\"\n\"#,\n    version\n                )\n            }\n        }\n}\n\nfn cowsay_bin(name: &str, version: &str) -> String {\n    cfg_if! {\n        if #[cfg(target_os = \"windows\")] {\n            format!(\n                r#\"@echo off\necho {} version {}\necho {} args: %*\n\"#,\n    name, version, name\n            )\n        } else {\n            format!(\n                r#\"#!/bin/sh\necho \"{} version {}\"\necho \"{} args: $@\"\n\"#,\n    name, version, name\n            )\n        }\n    }\n}\n\nfn cowsay_bin_info(version: &str) -> Vec<PackageBinInfo> {\n    vec![\n        PackageBinInfo {\n            name: \"cowsay\".to_string(),\n            contents: cowsay_bin(\"cowsay\", version),\n        },\n        PackageBinInfo {\n            name: \"cowthink\".to_string(),\n            contents: cowsay_bin(\"cowthink\", version),\n        },\n    ]\n}\n\nfn bin_config(name: &str) -> String {\n    format!(\n        r#\"{{\n  \"name\": \"{}\",\n  \"package\": \"cowsay\",\n  \"version\": \"1.4.0\",\n  \"platform\": {{\n    \"node\": \"11.10.1\",\n    \"npm\": \"6.7.0\",\n    \"yarn\": null\n  }},\n  \"manager\": \"Npm\"\n}}\"#,\n        name\n    )\n}\n\nconst PACKAGE_JSON_NPM_NO_DEP: &str = r#\"{\n    \"name\": \"no-deps\",\n    \"volta\": {\n        \"node\": \"10.99.1040\"\n    }\n}\"#;\n\nconst PACKAGE_JSON_NPM_WITH_DEP: &str = r#\"{\n    \"name\": \"with-deps\",\n    \"dependencies\": {\n        \"cowsay\": \"1.5.0\"\n    },\n    \"volta\": {\n        \"node\": \"10.99.1040\"\n    }\n}\"#;\n\nconst PACKAGE_JSON_YARN_PNP_WITH_DEP: &str = r#\"{\n    \"name\": \"with-deps\",\n    \"dependencies\": {\n        \"cowsay\": \"1.5.0\"\n    },\n    \"volta\": {\n        \"node\": \"10.99.1040\",\n        \"yarn\": \"3.12.1092\"\n    }\n}\"#;\n\nconst PLATFORM_NODE_NPM: &str = r#\"{\n    \"node\":{\n        \"runtime\":\"11.10.1\",\n        \"npm\":\"6.7.0\"\n    }\n}\"#;\n\n#[test]\nfn default_binary_no_project() {\n    // platform node is 11.10.1, npm is 6.7.0\n    // package cowsay is 1.4.0, installed with platform node\n    // default yarn is 1.23.483\n    // default pnpm is 7.7.1\n    // there is no local project, so it should run the default bin\n    let s = sandbox()\n        .platform(PLATFORM_NODE_NPM)\n        .package_config(\"cowsay\", PKG_CONFIG_BASIC)\n        .binary_config(\"cowsay\", &bin_config(\"cowsay\"))\n        .binary_config(\"cowthink\", &bin_config(\"cowthink\"))\n        .shim(\"cowsay\")\n        .shim(\"cowthink\")\n        .package_image(\"cowsay\", \"1.4.0\", Some(cowsay_bin_info(\"1.4.0\")))\n        .setup_node_binary(\"11.10.1\", \"6.7.0\", &node_bin(\"11.10.1\"))\n        .setup_npm_binary(\"6.7.0\", &npm_bin(\"6.7.0\"))\n        .setup_yarn_binary(\"1.23.483\", &yarn_bin(\"1.23.483\"))\n        .setup_pnpm_binary(\"7.7.1\", &pnpm_bin(\"7.7.1\"))\n        .add_dir_to_path(PathBuf::from(\"/bin\"))\n        .build();\n\n    // control should be passed directly to the bin\n    assert_that!(\n        s.exec_shim(\"cowsay\", \"foo\"),\n        execs()\n            .with_status(0)\n            .with_stdout_contains(\"cowsay version 1.4.0\")\n            .with_stdout_contains(\"cowsay args: foo\")\n            .with_stdout_does_not_contain(\"Node version\")\n            .with_stdout_does_not_contain(\"Npm version\")\n            .with_stdout_does_not_contain(\"Yarn version\")\n            .with_stdout_does_not_contain(\"pnpm version\")\n    );\n}\n\n#[test]\nfn default_binary_no_project_dep() {\n    // platform node is 11.10.1, npm is 6.7.0\n    // package cowsay is 1.4.0, installed with platform node\n    // default yarn is 1.23.483\n    // default pnpm is 7.7.1\n    // local project does not have cowsay dep, so it should run the default bin\n    let s = sandbox()\n        .platform(PLATFORM_NODE_NPM)\n        .package_json(PACKAGE_JSON_NPM_NO_DEP)\n        .package_config(\"cowsay\", PKG_CONFIG_BASIC)\n        .binary_config(\"cowsay\", &bin_config(\"cowsay\"))\n        .binary_config(\"cowthink\", &bin_config(\"cowthink\"))\n        .shim(\"cowsay\")\n        .shim(\"cowthink\")\n        .package_image(\"cowsay\", \"1.4.0\", Some(cowsay_bin_info(\"1.4.0\")))\n        .setup_node_binary(\"11.10.1\", \"6.7.0\", &node_bin(\"11.10.1\"))\n        .setup_npm_binary(\"6.7.0\", &npm_bin(\"6.7.0\"))\n        .setup_yarn_binary(\"1.23.483\", &yarn_bin(\"1.23.483\"))\n        .setup_pnpm_binary(\"7.7.1\", &pnpm_bin(\"7.7.1\"))\n        .add_dir_to_path(PathBuf::from(\"/bin\"))\n        .build();\n\n    assert_that!(\n        s.exec_shim(\"cowsay\", \"foo\"),\n        execs()\n            .with_status(0)\n            .with_stdout_contains(\"cowsay version 1.4.0\")\n            .with_stdout_contains(\"cowsay args: foo\")\n            .with_stdout_does_not_contain(\"Node version\")\n            .with_stdout_does_not_contain(\"Npm version\")\n            .with_stdout_does_not_contain(\"Yarn version\")\n            .with_stdout_does_not_contain(\"pnpm version\")\n    );\n}\n\n#[test]\nfn project_local_binary() {\n    // platform node is 11.10.1, npm is 6.7.0\n    // package cowsay is 1.4.0, installed with platform node\n    // default yarn is 1.23.483\n    // default pnpm is 7.7.1\n    // local project has cowsay as a dep, so it should run that binary\n    let s = sandbox()\n        .platform(PLATFORM_NODE_NPM)\n        .package_json(PACKAGE_JSON_NPM_WITH_DEP)\n        .package_config(\"cowsay\", PKG_CONFIG_BASIC)\n        .binary_config(\"cowsay\", &bin_config(\"cowsay\"))\n        .binary_config(\"cowthink\", &bin_config(\"cowthink\"))\n        .shim(\"cowsay\")\n        .shim(\"cowthink\")\n        .package_image(\"cowsay\", \"1.4.0\", Some(cowsay_bin_info(\"1.4.0\")))\n        .setup_node_binary(\"11.10.1\", \"6.7.0\", &node_bin(\"11.10.1\"))\n        .setup_node_binary(\"10.99.1040\", \"6.7.0\", &node_bin(\"10.99.1040\"))\n        .setup_npm_binary(\"6.7.0\", &npm_bin(\"6.7.0\"))\n        .setup_yarn_binary(\"1.23.483\", &yarn_bin(\"1.23.483\"))\n        .setup_pnpm_binary(\"7.7.1\", &pnpm_bin(\"7.7.1\"))\n        .project_bins(cowsay_bin_info(\"1.5.0\"))\n        .add_dir_to_path(PathBuf::from(\"/bin\"))\n        .build();\n\n    // control should be passed directly to the local bin\n    assert_that!(\n        s.exec_shim(\"cowsay\", \"bar\"),\n        execs()\n            .with_status(0)\n            .with_stdout_contains(\"cowsay version 1.5.0\")\n            .with_stdout_contains(\"cowsay args: bar\")\n            .with_stdout_does_not_contain(\"Node version\")\n            .with_stdout_does_not_contain(\"Npm version\")\n            .with_stdout_does_not_contain(\"Yarn version\")\n            .with_stdout_does_not_contain(\"pnpm version\")\n    );\n}\n\n#[test]\nfn project_local_binary_pnp() {\n    // platform node is 11.10.1, npm is 6.7.0\n    // package cowsay is 1.4.0, installed with platform node\n    // default yarn is 1.23.483\n    // project is Yarn PnP, with cowsay as a dep, so it should run 'yarn cowsay'\n    let s = sandbox()\n        .platform(PLATFORM_NODE_NPM)\n        .package_json(PACKAGE_JSON_YARN_PNP_WITH_DEP)\n        .package_config(\"cowsay\", PKG_CONFIG_BASIC)\n        .binary_config(\"cowsay\", &bin_config(\"cowsay\"))\n        .binary_config(\"cowthink\", &bin_config(\"cowthink\"))\n        .shim(\"cowsay\")\n        .shim(\"cowthink\")\n        .package_image(\"cowsay\", \"1.4.0\", Some(cowsay_bin_info(\"1.4.0\")))\n        .setup_node_binary(\"11.10.1\", \"6.7.0\", &node_bin(\"11.10.1\"))\n        .setup_node_binary(\"10.99.1040\", \"6.7.0\", &node_bin(\"10.99.1040\"))\n        .setup_npm_binary(\"6.7.0\", &npm_bin(\"6.7.0\"))\n        .setup_yarn_binary(\"1.23.483\", &yarn_bin(\"1.23.483\"))\n        .setup_yarn_binary(\"3.12.1092\", &yarn_bin(\"3.12.1092\"))\n        .project_pnp()\n        .add_dir_to_path(PathBuf::from(\"/bin\"))\n        .build();\n\n    // this should run 'yarn cowsay' to execute the binary\n    assert_that!(\n        s.exec_shim(\"cowsay\", \"baz\"),\n        execs()\n            .with_status(0)\n            .with_stdout_contains(\"Yarn version 3.12.1092\")\n            .with_stdout_contains(\"yarn args: cowsay baz\")\n            .with_stdout_does_not_contain(\"cowsay version\")\n            .with_stdout_does_not_contain(\"cowsay args\")\n            .with_stdout_does_not_contain(\"Node version\")\n            .with_stdout_does_not_contain(\"Npm version\")\n            .with_stdout_does_not_contain(\"Yarn version 1.23.483\")\n    );\n}\n"
  },
  {
    "path": "tests/acceptance/hooks.rs",
    "content": "use std::path::PathBuf;\nuse std::{thread, time};\n\nuse crate::support::events_helpers::{\n    assert_events, match_args, match_end, match_error, match_start,\n};\nuse crate::support::sandbox::sandbox;\nuse hamcrest2::assert_that;\nuse hamcrest2::prelude::*;\nuse mockito::mock;\nuse test_support::matchers::execs;\nuse volta_core::error::ExitCode;\n\nconst WORKSPACE_PACKAGE_JSON: &str = r#\"\n{\n    \"volta\": {\n        \"node\": \"10.11.12\"\n    }\n}\"#;\n\nconst PROJECT_PACKAGE_JSON: &str = r#\"\n{\n    \"volta\": {\n        \"extends\": \"./workspace/package.json\"\n    }\n}\"#;\n\n// scripts that write events to file 'events.json'\ncfg_if::cfg_if! {\n    if #[cfg(windows)] {\n        // have not been able to read events from stdin with batch, powershell, etc.\n        // so just copy the tempfile (path in EVENTS_FILE env var) to events.json\n        const EVENTS_EXECUTABLE: &str = r#\"@echo off\ncopy %EVENTS_FILE% events.json\n:: executables should clean up the temp file\ndel %EVENTS_FILE%\n\"#;\n        const SCRIPT_FILENAME: &str = \"write-events.bat\";\n        const VOLTA_BINARY: &str = \"volta.exe\";\n    } else if #[cfg(unix)] {\n        // read events from stdin\n        const EVENTS_EXECUTABLE: &str = r#\"#!/bin/bash\n# read Volta events from stdin, and write to events.json\n# (but first clear it out)\necho -n \"\" >events.json\nwhile read line\ndo\n  echo \"$line\" >>events.json\ndone\n# executables should clean up the temp file\n/bin/rm \"$EVENTS_FILE\"\n\"#;\n        const SCRIPT_FILENAME: &str = \"write-events.sh\";\n        const VOLTA_BINARY: &str = \"volta\";\n    } else {\n        compile_error!(\"Unsupported platform for tests (expected 'unix' or 'windows').\");\n    }\n}\n\nfn default_hooks_json() -> String {\n    format!(\n        r#\"\n{{\n    \"node\": {{\n        \"distro\": {{\n            \"template\": \"{}/hook/default/node/{{{{version}}}}\"\n        }}\n    }},\n    \"npm\": {{\n        \"distro\": {{\n            \"template\": \"{0}/hook/default/npm/{{{{version}}}}\"\n        }}\n    }},\n    \"yarn\": {{\n        \"distro\": {{\n            \"template\": \"{0}/hook/default/yarn/{{{{version}}}}\"\n        }}\n    }},\n    \"events\": {{\n        \"publish\": {{\n            \"bin\": \"{}\"\n        }}\n    }}\n}}\"#,\n        mockito::server_url(),\n        SCRIPT_FILENAME\n    )\n}\n\nfn project_hooks_json() -> String {\n    format!(\n        r#\"\n{{\n    \"yarn\": {{\n        \"distro\": {{\n            \"template\": \"{0}/hook/project/yarn/{{{{version}}}}\"\n        }}\n    }}\n}}\"#,\n        mockito::server_url()\n    )\n}\n\nfn workspace_hooks_json() -> String {\n    format!(\n        r#\"\n{{\n    \"npm\": {{\n        \"distro\": {{\n            \"template\": \"{0}/hook/workspace/npm/{{{{version}}}}\"\n        }}\n    }},\n    \"yarn\": {{\n        \"distro\": {{\n            \"template\": \"{0}/hook/workspace/yarn/{{{{version}}}}\"\n        }}\n    }}\n}}\"#,\n        mockito::server_url()\n    )\n}\n\nfn pnpm_hooks_json() -> String {\n    format!(\n        r#\"\n{{\n    \"pnpm\": {{\n        \"index\": {{\n            \"template\": \"{0}/pnpm/index\"\n        }},\n        \"distro\": {{\n            \"template\": \"{0}/pnpm/{{{{version}}}}\"\n        }}\n    }}\n}}\"#,\n        mockito::server_url()\n    )\n}\n\nfn yarn_hooks_json() -> String {\n    format!(\n        r#\"\n{{\n    \"yarn\": {{\n        \"latest\": {{\n            \"template\": \"{0}/yarn-old/latest\"\n        }},\n        \"index\": {{\n            \"template\": \"{0}/yarn-old/index\"\n        }}\n    }}\n}}\"#,\n        mockito::server_url()\n    )\n}\n\nfn yarn_hooks_format_json(format: &str) -> String {\n    format!(\n        r#\"\n{{\n    \"yarn\": {{\n        \"latest\": {{\n            \"template\": \"{0}/yarn-new/latest\"\n        }},\n        \"index\": {{\n            \"template\": \"{0}/yarn-new/index\",\n            \"format\": \"{1}\"\n        }}\n    }}\n}}\"#,\n        mockito::server_url(),\n        format\n    )\n}\n\n#[test]\nfn redirects_download() {\n    let s = sandbox()\n        .default_hooks(&default_hooks_json())\n        .env(\"VOLTA_WRITE_EVENTS_FILE\", \"true\")\n        .executable_file(SCRIPT_FILENAME, EVENTS_EXECUTABLE)\n        .build();\n\n    assert_that!(\n        s.volta(\"install node@1.2.3\"),\n        execs()\n            .with_status(ExitCode::NetworkError as i32)\n            .with_stderr_contains(\"[..]Could not download node@1.2.3\")\n            .with_stderr_contains(\"[..]/hook/default/node/1.2.3\")\n    );\n\n    thread::sleep(time::Duration::from_millis(500));\n    assert_events(\n        &s,\n        vec![\n            (\"volta\", match_start()),\n            (\"install\", match_start()),\n            (\"volta\", match_error(5, \"Could not download node\")),\n            (\"volta\", match_end(5)),\n            (\n                \"args\",\n                match_args(format!(\"{} install node@1.2.3\", VOLTA_BINARY).as_str()),\n            ),\n        ],\n    );\n}\n\n#[test]\nfn merges_project_and_default_hooks() {\n    let local_hooks: PathBuf = [\".volta\", \"hooks.json\"].iter().collect();\n    let s = sandbox()\n        .package_json(\"{}\")\n        .default_hooks(&default_hooks_json())\n        .project_file(&local_hooks.to_string_lossy(), &project_hooks_json())\n        .env(\"VOLTA_WRITE_EVENTS_FILE\", \"true\")\n        .executable_file(SCRIPT_FILENAME, EVENTS_EXECUTABLE)\n        .build();\n\n    // Project defines yarn hooks, so those should be used\n    assert_that!(\n        s.volta(\"install yarn@3.2.1\"),\n        execs()\n            .with_status(ExitCode::NetworkError as i32)\n            .with_stderr_contains(\"[..]Could not download yarn@3.2.1\")\n            .with_stderr_contains(\"[..]/hook/project/yarn/3.2.1\")\n    );\n    thread::sleep(time::Duration::from_millis(500));\n    assert_events(\n        &s,\n        vec![\n            (\"volta\", match_start()),\n            (\"install\", match_start()),\n            (\"volta\", match_error(5, \"Could not download yarn\")),\n            (\"volta\", match_end(5)),\n            (\n                \"args\",\n                match_args(format!(\"{} install yarn@3.2.1\", VOLTA_BINARY).as_str()),\n            ),\n        ],\n    );\n\n    // Project doesn't define node hooks, so should inherit from the default\n    assert_that!(\n        s.volta(\"install node@10.12.1\"),\n        execs()\n            .with_status(ExitCode::NetworkError as i32)\n            .with_stderr_contains(\"[..]Could not download node@10.12.1\")\n            .with_stderr_contains(\"[..]/hook/default/node/10.12.1\")\n    );\n    thread::sleep(time::Duration::from_millis(500));\n    assert_events(\n        &s,\n        vec![\n            (\"volta\", match_start()),\n            (\"install\", match_start()),\n            (\"volta\", match_error(5, \"Could not download node\")),\n            (\"volta\", match_end(5)),\n            (\n                \"args\",\n                match_args(format!(\"{} install node@10.12.1\", VOLTA_BINARY).as_str()),\n            ),\n        ],\n    );\n}\n\n#[test]\nfn merges_workspace_hooks() {\n    let workspace_hooks: PathBuf = [\"workspace\", \".volta\", \"hooks.json\"].iter().collect();\n    let workspace_package_json: PathBuf = [\"workspace\", \"package.json\"].iter().collect();\n    let project_hooks: PathBuf = [\".volta\", \"hooks.json\"].iter().collect();\n    let s = sandbox()\n        .default_hooks(&default_hooks_json())\n        .package_json(PROJECT_PACKAGE_JSON)\n        .project_file(&project_hooks.to_string_lossy(), &project_hooks_json())\n        .project_file(\n            &workspace_package_json.to_string_lossy(),\n            WORKSPACE_PACKAGE_JSON,\n        )\n        .project_file(&workspace_hooks.to_string_lossy(), &workspace_hooks_json())\n        .env(\"VOLTA_WRITE_EVENTS_FILE\", \"true\")\n        .executable_file(SCRIPT_FILENAME, EVENTS_EXECUTABLE)\n        .build();\n\n    // Project defines yarn hooks, so those should be used\n    assert_that!(\n        s.volta(\"pin yarn@3.1.4\"),\n        execs()\n            .with_status(ExitCode::NetworkError as i32)\n            .with_stderr_contains(\"[..]Could not download yarn@3.1.4\")\n            .with_stderr_contains(\"[..]/hook/project/yarn/3.1.4\")\n    );\n    thread::sleep(time::Duration::from_millis(500));\n    assert_events(\n        &s,\n        vec![\n            (\"volta\", match_start()),\n            (\"pin\", match_start()),\n            (\"volta\", match_error(5, \"Could not download yarn\")),\n            (\"volta\", match_end(5)),\n            (\n                \"args\",\n                match_args(format!(\"{} pin yarn@3.1.4\", VOLTA_BINARY).as_str()),\n            ),\n        ],\n    );\n\n    // Workspace defines npm hooks, so those should be inherited\n    assert_that!(\n        s.volta(\"pin npm@5.6.7\"),\n        execs()\n            .with_status(ExitCode::NetworkError as i32)\n            .with_stderr_contains(\"[..]Could not download npm@5.6.7\")\n            .with_stderr_contains(\"[..]/hook/workspace/npm/5.6.7\")\n    );\n    thread::sleep(time::Duration::from_millis(500));\n    assert_events(\n        &s,\n        vec![\n            (\"volta\", match_start()),\n            (\"pin\", match_start()),\n            (\"volta\", match_error(5, \"Could not download npm\")),\n            (\"volta\", match_end(5)),\n            (\n                \"args\",\n                match_args(format!(\"{} pin npm@5.6.7\", VOLTA_BINARY).as_str()),\n            ),\n        ],\n    );\n\n    // Neither project nor workspace defines node hooks, so should inherit from the default\n    assert_that!(\n        s.volta(\"install node@11.11.2\"),\n        execs()\n            .with_status(ExitCode::NetworkError as i32)\n            .with_stderr_contains(\"[..]Could not download node@11.11.2\")\n            .with_stderr_contains(\"[..]/hook/default/node/11.11.2\")\n    );\n}\n\n#[test]\nfn pnpm_latest_with_hook_reads_index() {\n    let s = sandbox()\n        .default_hooks(&pnpm_hooks_json())\n        .env(\"VOLTA_LOGLEVEL\", \"debug\")\n        .env(\"VOLTA_FEATURE_PNPM\", \"1\")\n        .build();\n    let _mock = mock(\"GET\", \"/pnpm/index\")\n        .with_status(200)\n        .with_header(\"Content-Type\", \"application/json\")\n        .with_body(\n            // Npm format for pnpm\n            r#\"{\n    \"name\":\"pnpm\",\n    \"dist-tags\": { \"latest\":\"7.7.1\" },\n    \"versions\": {\n        \"0.0.1\": { \"version\":\"0.0.1\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }},\n        \"6.34.0\": { \"version\":\"6.34.0\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }},\n        \"7.7.1\": { \"version\":\"7.7.1\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }}\n    }\n}\"#,\n        )\n        .create();\n\n    assert_that!(\n        s.volta(\"install pnpm@latest\"),\n        execs()\n            .with_status(ExitCode::NetworkError as i32)\n            .with_stderr_contains(\"[..]Using pnpm.index hook to determine pnpm index URL\")\n            .with_stderr_contains(\"[..]Found pnpm@7.7.1 matching tag 'latest'[..]\")\n            .with_stderr_contains(\"[..]Downloading pnpm@7.7.1 from[..]/pnpm/7.7.1[..]\")\n            .with_stderr_contains(\"[..]Could not download pnpm@7.7.1\")\n    );\n}\n\n#[test]\nfn pnpm_no_version_with_hook_reads_index() {\n    let s = sandbox()\n        .default_hooks(&pnpm_hooks_json())\n        .env(\"VOLTA_LOGLEVEL\", \"debug\")\n        .env(\"VOLTA_FEATURE_PNPM\", \"1\")\n        .build();\n    let _mock = mock(\"GET\", \"/pnpm/index\")\n        .with_status(200)\n        .with_header(\"Content-Type\", \"application/json\")\n        .with_body(\n            // Npm format for pnpm\n            r#\"{\n    \"name\":\"pnpm\",\n    \"dist-tags\": { \"latest\":\"7.7.1\" },\n    \"versions\": {\n        \"0.0.1\": { \"version\":\"0.0.1\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }},\n        \"6.34.0\": { \"version\":\"6.34.0\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }},\n        \"7.7.1\": { \"version\":\"7.7.1\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }}\n    }\n}\"#,\n        )\n        .create();\n\n    assert_that!(\n        s.volta(\"install pnpm\"),\n        execs()\n            .with_status(ExitCode::NetworkError as i32)\n            .with_stderr_contains(\"[..]Using pnpm.index hook to determine pnpm index URL\")\n            .with_stderr_contains(\"[..]Found pnpm@7.7.1 matching tag 'latest'[..]\")\n            .with_stderr_contains(\"[..]Downloading pnpm@7.7.1 from[..]/pnpm/7.7.1[..]\")\n            .with_stderr_contains(\"[..]Could not download pnpm@7.7.1\")\n    );\n}\n\n#[test]\nfn yarn_latest_with_hook_reads_latest() {\n    let s = sandbox()\n        .default_hooks(&yarn_hooks_json())\n        .env(\"VOLTA_LOGLEVEL\", \"debug\")\n        .build();\n    let _mock = mock(\"GET\", \"/yarn-old/latest\")\n        .with_status(200)\n        .with_body(\"4.2.9\")\n        .create();\n\n    assert_that!(\n        s.volta(\"install yarn@latest\"),\n        execs()\n            .with_status(ExitCode::NetworkError as i32)\n            .with_stderr_contains(\"[..]Using yarn.latest hook to determine latest-version URL\")\n            .with_stderr_contains(\"[..]Found yarn latest version (4.2.9)[..]\")\n            .with_stderr_contains(\"[..]Could not download yarn@4.2.9\")\n    );\n}\n\n#[test]\nfn yarn_no_version_with_hook_reads_latest() {\n    let s = sandbox()\n        .default_hooks(&yarn_hooks_json())\n        .env(\"VOLTA_LOGLEVEL\", \"debug\")\n        .build();\n    let _mock = mock(\"GET\", \"/yarn-old/latest\")\n        .with_status(200)\n        .with_body(\"4.2.9\")\n        .create();\n\n    assert_that!(\n        s.volta(\"install yarn\"),\n        execs()\n            .with_status(ExitCode::NetworkError as i32)\n            .with_stderr_contains(\"[..]Using yarn.latest hook to determine latest-version URL\")\n            .with_stderr_contains(\"[..]Found yarn latest version (4.2.9)[..]\")\n            .with_stderr_contains(\"[..]Could not download yarn@4.2.9\")\n    );\n}\n\n#[test]\nfn yarn_semver_with_hook_uses_old_format() {\n    let s = sandbox()\n        .default_hooks(&yarn_hooks_json())\n        .env(\"VOLTA_LOGLEVEL\", \"debug\")\n        .build();\n    let _mock = mock(\"GET\", \"/yarn-old/index\")\n        .with_status(200)\n        .with_header(\"Content-Type\", \"application/json\")\n        .with_body(\n            // Yarn Index hook expects the \"old\" (Github API) format\n            r#\"[\n    {\"tag_name\":\"v1.22.4\",\"assets\":[{\"name\":\"yarn-v1.22.4.tar.gz\"}]},\n    {\"tag_name\":\"v2.0.0\",\"assets\":[{\"name\":\"yarn-v2.0.0.tar.gz\"}]},\n    {\"tag_name\":\"v3.9.2\",\"assets\":[{\"name\":\"yarn-v3.9.2.tar.gz\"}]},\n    {\"tag_name\":\"v4.1.1\",\"assets\":[{\"name\":\"yarn-v4.1.1.tar.gz\"}]}\n]\"#,\n        )\n        .create();\n\n    assert_that!(\n        s.volta(\"install yarn@3\"),\n        execs()\n            .with_status(ExitCode::NetworkError as i32)\n            .with_stderr_contains(\"[..]Using yarn.index hook to determine yarn index URL\")\n            .with_stderr_contains(\"[..]Found yarn@3.9.2 matching requirement[..]\")\n            .with_stderr_contains(\"[..]Could not download yarn@3.9.2\")\n    );\n}\n\n#[test]\nfn yarn_semver_with_hook_uses_configured_format() {\n    let s = sandbox()\n        .default_hooks(&yarn_hooks_format_json(\"npm\"))\n        .env(\"VOLTA_LOGLEVEL\", \"debug\")\n        .build();\n    let _mock = mock(\"GET\", \"/yarn-new/index\")\n        .with_status(200)\n        .with_header(\"Content-Type\", \"application/json\")\n        .with_body(\n            // Should be using the Npm format\n            r#\"{\n    \"name\":\"@yarnpkg/cli-dist\",\n    \"dist-tags\": { \"latest\":\"3.12.99\" },\n    \"versions\": {\n        \"2.4.159\": { \"version\":\"2.4.159\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }},\n        \"3.2.42\": { \"version\":\"3.2.42\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }},\n        \"3.7.71\": { \"version\":\"3.7.71\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }},\n        \"3.12.99\": { \"version\":\"3.12.99\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }}\n    }\n}\"#,\n        )\n        .create();\n\n    assert_that!(\n        s.volta(\"install yarn@3\"),\n        execs()\n            .with_status(ExitCode::NetworkError as i32)\n            .with_stderr_contains(\"[..]Using yarn.index hook to determine yarn index URL\")\n            .with_stderr_contains(\"[..]Found yarn@3.12.99 matching requirement[..]\")\n            .with_stderr_contains(\"[..]Could not download yarn@3.12.99\")\n    );\n}\n"
  },
  {
    "path": "tests/acceptance/main.rs",
    "content": "use cfg_if::cfg_if;\n\ncfg_if! {\n    if #[cfg(feature = \"mock-network\")] {\n        mod support;\n\n        // test files\n        mod corrupted_download;\n        mod direct_install;\n        mod direct_uninstall;\n        mod execute_binary;\n        mod hooks;\n        mod merged_platform;\n        mod migrations;\n        mod run_shim_directly;\n        mod verbose_errors;\n        mod volta_bypass;\n        mod volta_install;\n        mod volta_pin;\n        mod volta_run;\n        mod volta_uninstall;\n    }\n}\n"
  },
  {
    "path": "tests/acceptance/merged_platform.rs",
    "content": "use std::{thread, time};\n\nuse crate::support::events_helpers::{assert_events, match_args, match_start, match_tool_end};\nuse crate::support::sandbox::{\n    sandbox, DistroMetadata, NodeFixture, NpmFixture, PnpmFixture, Yarn1Fixture,\n};\nuse hamcrest2::assert_that;\nuse hamcrest2::prelude::*;\nuse test_support::matchers::execs;\n\nuse volta_core::error::ExitCode;\n\nconst PACKAGE_JSON_NODE_ONLY: &str = r#\"{\n    \"name\": \"node-only\",\n    \"volta\": {\n        \"node\": \"10.99.1040\"\n    }\n}\"#;\n\nconst PACKAGE_JSON_WITH_NPM: &str = r#\"{\n    \"name\": \"with-npm\",\n    \"volta\": {\n        \"node\": \"10.99.1040\",\n        \"npm\": \"4.5.6\"\n    }\n}\"#;\n\nconst PACKAGE_JSON_WITH_PNPM: &str = r#\"{\n    \"name\": \"with-pnpm\",\n    \"volta\": {\n        \"node\": \"10.99.1040\",\n        \"pnpm\": \"7.7.1\"\n    }\n}\"#;\n\nconst PACKAGE_JSON_WITH_YARN: &str = r#\"{\n    \"name\": \"with-yarn\",\n    \"volta\": {\n        \"node\": \"10.99.1040\",\n        \"yarn\": \"1.12.99\"\n    }\n}\"#;\n\nconst PLATFORM_NODE_ONLY: &str = r#\"{\n    \"node\":{\n        \"runtime\":\"9.27.6\",\n        \"npm\":null\n    }\n}\"#;\n\nconst PLATFORM_WITH_NPM: &str = r#\"{\n    \"node\":{\n        \"runtime\":\"9.27.6\",\n        \"npm\":\"1.2.3\"\n    }\n}\"#;\n\nconst PLATFORM_WITH_PNPM: &str = r#\"{\n    \"node\":{\n        \"runtime\":\"9.27.6\",\n        \"npm\":null\n    },\n    \"pnpm\": \"7.7.1\"\n}\"#;\n\nconst PLATFORM_WITH_YARN: &str = r#\"{\n    \"node\":{\n        \"runtime\":\"9.27.6\",\n        \"npm\":null\n    },\n    \"yarn\": \"1.7.71\"\n}\"#;\n\ncfg_if::cfg_if! {\n    if #[cfg(windows)] {\n        // copy the tempfile (path in EVENTS_FILE env var) to events.json\n        const EVENTS_EXECUTABLE: &str = r#\"@echo off\ncopy %EVENTS_FILE% events.json\n:: executables should clean up the temp file\ndel %EVENTS_FILE%\n\"#;\n        const SCRIPT_FILENAME: &str = \"write-events.bat\";\n        const PNPM_SHIM: &str = \"pnpm.exe\";\n        const YARN_SHIM: &str = \"yarn.exe\";\n    } else if #[cfg(unix)] {\n        // copy the tempfile (path in EVENTS_FILE env var) to events.json\n        const EVENTS_EXECUTABLE: &str = r#\"#!/bin/bash\n/bin/cp \"$EVENTS_FILE\" events.json\n# executables should clean up the temp file\n/bin/rm \"$EVENTS_FILE\"\n\"#;\n        const SCRIPT_FILENAME: &str = \"write-events.sh\";\n        const PNPM_SHIM: &str = \"pnpm\";\n        const YARN_SHIM: &str = \"yarn\";\n    } else {\n        compile_error!(\"Unsupported platform for tests (expected 'unix' or 'windows').\");\n    }\n}\n\nfn events_hooks_json() -> String {\n    format!(\n        r#\"\n{{\n    \"events\": {{\n        \"publish\": {{\n            \"bin\": \"{}\"\n        }}\n    }}\n}}\"#,\n        SCRIPT_FILENAME\n    )\n}\n\ncfg_if::cfg_if! {\n    if #[cfg(target_os = \"macos\")] {\n        const NODE_VERSION_FIXTURES: [DistroMetadata; 2] = [\n            DistroMetadata {\n                version: \"10.99.1040\",\n                compressed_size: 273,\n                uncompressed_size: Some(0x0028_0000),\n            },\n            DistroMetadata {\n                version: \"9.27.6\",\n                compressed_size: 272,\n                uncompressed_size: Some(0x0028_0000),\n            },\n        ];\n    } else if #[cfg(target_os = \"linux\")] {\n        const NODE_VERSION_FIXTURES: [DistroMetadata; 2] = [\n            DistroMetadata {\n                version: \"10.99.1040\",\n                compressed_size: 273,\n                uncompressed_size: Some(0x0028_0000),\n            },\n            DistroMetadata {\n                version: \"9.27.6\",\n                compressed_size: 272,\n                uncompressed_size: Some(0x0028_0000),\n            },\n        ];\n    } else if #[cfg(target_os = \"windows\")] {\n        const NODE_VERSION_FIXTURES: [DistroMetadata; 2] = [\n            DistroMetadata {\n                version: \"10.99.1040\",\n                compressed_size: 1096,\n                uncompressed_size: None,\n            },\n            DistroMetadata {\n                version: \"9.27.6\",\n                compressed_size: 1068,\n                uncompressed_size: None,\n            },\n        ];\n    } else {\n        compile_error!(\"Unsupported target_os for tests (expected 'macos', 'linux', or 'windows').\");\n    }\n}\n\nconst NPM_VERSION_FIXTURES: [DistroMetadata; 2] = [\n    DistroMetadata {\n        version: \"1.2.3\",\n        compressed_size: 239,\n        uncompressed_size: Some(0x0028_0000),\n    },\n    DistroMetadata {\n        version: \"4.5.6\",\n        compressed_size: 239,\n        uncompressed_size: Some(0x0028_0000),\n    },\n];\n\nconst PNPM_VERSION_FIXTURES: [DistroMetadata; 2] = [\n    DistroMetadata {\n        version: \"6.34.0\",\n        compressed_size: 500,\n        uncompressed_size: Some(0x0028_0000),\n    },\n    DistroMetadata {\n        version: \"7.7.1\",\n        compressed_size: 518,\n        uncompressed_size: Some(0x0028_0000),\n    },\n];\n\nconst YARN_1_VERSION_FIXTURES: [DistroMetadata; 2] = [\n    DistroMetadata {\n        version: \"1.12.99\",\n        compressed_size: 178,\n        uncompressed_size: Some(0x0028_0000),\n    },\n    DistroMetadata {\n        version: \"1.7.71\",\n        compressed_size: 176,\n        uncompressed_size: Some(0x0028_0000),\n    },\n];\n\n#[test]\nfn uses_project_npm_if_available() {\n    let s = sandbox()\n        .platform(PLATFORM_WITH_NPM)\n        .package_json(PACKAGE_JSON_WITH_NPM)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .distro_mocks::<NpmFixture>(&NPM_VERSION_FIXTURES)\n        .env(\"VOLTA_LOGLEVEL\", \"debug\")\n        .build();\n\n    assert_that!(\n        s.npm(\"--version\"),\n        execs()\n            .with_status(ExitCode::Success as i32)\n            .with_stderr_contains(\"[..]Node: 10.99.1040 from project configuration\")\n            .with_stderr_contains(\"[..]npm: 4.5.6 from project configuration\")\n    );\n}\n\n#[test]\nfn uses_bundled_npm_in_project_without_npm() {\n    let s = sandbox()\n        .platform(PLATFORM_WITH_NPM)\n        .package_json(PACKAGE_JSON_NODE_ONLY)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .distro_mocks::<NpmFixture>(&NPM_VERSION_FIXTURES)\n        .env(\"VOLTA_LOGLEVEL\", \"debug\")\n        .build();\n\n    assert_that!(\n        s.npm(\"--version\"),\n        execs()\n            .with_status(ExitCode::Success as i32)\n            .with_stderr_contains(\"[..]Node: 10.99.1040 from project configuration\")\n            .with_stderr_contains(\"[..]npm: 6.2.26 from project configuration\")\n    );\n}\n\n#[test]\nfn uses_default_npm_outside_project() {\n    let s = sandbox()\n        .platform(PLATFORM_WITH_NPM)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .distro_mocks::<NpmFixture>(&NPM_VERSION_FIXTURES)\n        .env(\"VOLTA_LOGLEVEL\", \"debug\")\n        .build();\n\n    assert_that!(\n        s.npm(\"--version\"),\n        execs()\n            .with_status(ExitCode::Success as i32)\n            .with_stderr_contains(\"[..]Node: 9.27.6 from default configuration\")\n            .with_stderr_contains(\"[..]npm: 1.2.3 from default configuration\")\n    );\n}\n\n#[test]\nfn uses_bundled_npm_outside_project() {\n    let s = sandbox()\n        .platform(PLATFORM_NODE_ONLY)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .distro_mocks::<NpmFixture>(&NPM_VERSION_FIXTURES)\n        .env(\"VOLTA_LOGLEVEL\", \"debug\")\n        .build();\n\n    assert_that!(\n        s.npm(\"--version\"),\n        execs()\n            .with_status(ExitCode::Success as i32)\n            .with_stderr_contains(\"[..]Node: 9.27.6 from default configuration\")\n            .with_stderr_contains(\"[..]npm: 5.6.17 from default configuration\")\n    );\n}\n\n#[test]\nfn uses_project_yarn_if_available() {\n    let s = sandbox()\n        .platform(PLATFORM_WITH_YARN)\n        .package_json(PACKAGE_JSON_WITH_YARN)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .distro_mocks::<Yarn1Fixture>(&YARN_1_VERSION_FIXTURES)\n        .env(\"VOLTA_LOGLEVEL\", \"debug\")\n        .env(\"VOLTA_WRITE_EVENTS_FILE\", \"true\")\n        .default_hooks(&events_hooks_json())\n        .executable_file(SCRIPT_FILENAME, EVENTS_EXECUTABLE)\n        .build();\n\n    assert_that!(\n        s.yarn(\"--version\"),\n        execs()\n            .with_status(ExitCode::Success as i32)\n            .with_stderr_does_not_contain(\"[..]Yarn is not available.\")\n            .with_stderr_does_not_contain(\"[..]No Yarn version found in this project.\")\n            .with_stderr_contains(\"[..]Yarn: 1.12.99 from project configuration\")\n    );\n\n    thread::sleep(time::Duration::from_millis(500));\n    assert_events(\n        &s,\n        vec![\n            (\"tool\", match_start()),\n            (\"yarn\", match_start()),\n            (\"tool\", match_tool_end(0)),\n            (\n                \"args\",\n                match_args(format!(\"{} --version\", YARN_SHIM).as_str()),\n            ),\n        ],\n    );\n}\n\n#[test]\nfn uses_default_yarn_in_project_without_yarn() {\n    let s = sandbox()\n        .platform(PLATFORM_WITH_YARN)\n        .package_json(PACKAGE_JSON_NODE_ONLY)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .distro_mocks::<Yarn1Fixture>(&YARN_1_VERSION_FIXTURES)\n        .env(\"VOLTA_LOGLEVEL\", \"debug\")\n        .build();\n\n    assert_that!(\n        s.yarn(\"--version\"),\n        execs()\n            .with_status(ExitCode::Success as i32)\n            .with_stderr_does_not_contain(\"[..]Yarn is not available.\")\n            .with_stderr_does_not_contain(\"[..]No Yarn version found in this project.\")\n            .with_stderr_contains(\"[..]Yarn: 1.7.71 from default configuration\")\n    );\n}\n\n#[test]\nfn uses_default_yarn_outside_project() {\n    let s = sandbox()\n        .platform(PLATFORM_WITH_YARN)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .distro_mocks::<Yarn1Fixture>(&YARN_1_VERSION_FIXTURES)\n        .env(\"VOLTA_LOGLEVEL\", \"debug\")\n        .build();\n\n    assert_that!(\n        s.yarn(\"--version\"),\n        execs()\n            .with_status(ExitCode::Success as i32)\n            .with_stderr_does_not_contain(\"[..]Yarn is not available.\")\n            .with_stderr_does_not_contain(\"[..]No Yarn version found in this project.\")\n            .with_stderr_contains(\"[..]Yarn: 1.7.71 from default configuration\")\n    );\n}\n\n#[test]\nfn throws_project_error_in_project() {\n    let s = sandbox()\n        .platform(PLATFORM_NODE_ONLY)\n        .package_json(PACKAGE_JSON_NODE_ONLY)\n        .build();\n\n    assert_that!(\n        s.yarn(\"--version\"),\n        execs()\n            .with_status(ExitCode::ExecutionFailure as i32)\n            .with_stderr_contains(\"[..]No Yarn version found in this project.\")\n    );\n}\n\n#[test]\nfn throws_default_error_outside_project() {\n    let s = sandbox().platform(PLATFORM_NODE_ONLY).build();\n\n    assert_that!(\n        s.yarn(\"--version\"),\n        execs()\n            .with_status(ExitCode::ExecutionFailure as i32)\n            .with_stderr_contains(\"[..]Yarn is not available.\")\n    );\n}\n\n#[test]\nfn uses_project_pnpm_if_available() {\n    let s = sandbox()\n        .platform(PLATFORM_WITH_PNPM)\n        .package_json(PACKAGE_JSON_WITH_PNPM)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .distro_mocks::<PnpmFixture>(&PNPM_VERSION_FIXTURES)\n        .env(\"VOLTA_LOGLEVEL\", \"debug\")\n        .env(\"VOLTA_WRITE_EVENTS_FILE\", \"true\")\n        .env(\"VOLTA_FEATURE_PNPM\", \"1\")\n        .default_hooks(&events_hooks_json())\n        .executable_file(SCRIPT_FILENAME, EVENTS_EXECUTABLE)\n        .build();\n\n    assert_that!(\n        s.pnpm(\"--version\"),\n        execs()\n            .with_status(ExitCode::Success as i32)\n            .with_stderr_does_not_contain(\"[..]pnpm is not available.\")\n            .with_stderr_does_not_contain(\"[..]No pnpm version found in this project.\")\n            .with_stderr_contains(\"[..]pnpm: 7.7.1 from project configuration\")\n    );\n\n    thread::sleep(time::Duration::from_millis(500));\n    assert_events(\n        &s,\n        vec![\n            (\"tool\", match_start()),\n            (\"pnpm\", match_start()),\n            (\"tool\", match_tool_end(0)),\n            (\n                \"args\",\n                match_args(format!(\"{} --version\", PNPM_SHIM).as_str()),\n            ),\n        ],\n    );\n}\n\n#[test]\nfn uses_default_pnpm_in_project_without_pnpm() {\n    let s = sandbox()\n        .platform(PLATFORM_WITH_PNPM)\n        .package_json(PACKAGE_JSON_NODE_ONLY)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .distro_mocks::<PnpmFixture>(&PNPM_VERSION_FIXTURES)\n        .env(\"VOLTA_LOGLEVEL\", \"debug\")\n        .env(\"VOLTA_FEATURE_PNPM\", \"1\")\n        .build();\n\n    assert_that!(\n        s.pnpm(\"--version\"),\n        execs()\n            .with_status(ExitCode::Success as i32)\n            .with_stderr_does_not_contain(\"[..]pnpm is not available.\")\n            .with_stderr_does_not_contain(\"[..]No pnpm version found in this project.\")\n            .with_stderr_contains(\"[..]pnpm: 7.7.1 from default configuration\")\n    );\n}\n\n#[test]\nfn uses_default_pnpm_outside_project() {\n    let s = sandbox()\n        .platform(PLATFORM_WITH_PNPM)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .distro_mocks::<PnpmFixture>(&PNPM_VERSION_FIXTURES)\n        .env(\"VOLTA_LOGLEVEL\", \"debug\")\n        .env(\"VOLTA_FEATURE_PNPM\", \"1\")\n        .build();\n\n    assert_that!(\n        s.pnpm(\"--version\"),\n        execs()\n            .with_status(ExitCode::Success as i32)\n            .with_stderr_does_not_contain(\"[..]pnpm is not available.\")\n            .with_stderr_does_not_contain(\"[..]No pnpm version found in this project.\")\n            .with_stderr_contains(\"[..]pnpm: 7.7.1 from default configuration\")\n    );\n}\n\n#[test]\nfn uses_pnpm_throws_project_error_in_project() {\n    let s = sandbox()\n        .platform(PLATFORM_NODE_ONLY)\n        .package_json(PACKAGE_JSON_NODE_ONLY)\n        .env(\"VOLTA_FEATURE_PNPM\", \"1\")\n        .build();\n\n    assert_that!(\n        s.pnpm(\"--version\"),\n        execs()\n            .with_status(ExitCode::ExecutionFailure as i32)\n            .with_stderr_contains(\"[..]No pnpm version found in this project.\")\n    );\n}\n"
  },
  {
    "path": "tests/acceptance/migrations.rs",
    "content": "use crate::support::sandbox::{sandbox, Sandbox};\nuse hamcrest2::assert_that;\nuse hamcrest2::prelude::*;\nuse test_support::matchers::execs;\n\n#[test]\nfn empty_volta_home_is_created() {\n    let s = sandbox().build();\n\n    // clear out the .volta dir\n    s.remove_volta_home();\n\n    // VOLTA_HOME starts out non-existent, with no shims\n    assert!(!Sandbox::path_exists(\".volta\"));\n    assert!(!Sandbox::shim_exists(\"node\"));\n\n    // running volta triggers automatic creation\n    assert_that!(s.volta(\"--version\"), execs().with_status(0));\n\n    // home directories should all be created\n    assert!(Sandbox::path_exists(\".volta\"));\n    assert!(Sandbox::path_exists(\".volta/bin\"));\n    assert!(Sandbox::path_exists(\".volta/cache/node\"));\n    assert!(Sandbox::path_exists(\".volta/log\"));\n    assert!(Sandbox::path_exists(\".volta/tmp\"));\n    assert!(Sandbox::path_exists(\".volta/tools/image/node\"));\n    assert!(Sandbox::path_exists(\".volta/tools/image/yarn\"));\n    assert!(Sandbox::path_exists(\".volta/tools/inventory/node\"));\n    assert!(Sandbox::path_exists(\".volta/tools/inventory/yarn\"));\n    assert!(Sandbox::path_exists(\".volta/tools/user\"));\n\n    // Layout file should now exist\n    assert!(Sandbox::path_exists(\".volta/layout.v4\"));\n\n    // shims should all be created\n    // NOTE: this doesn't work in Windows, because the default shims are stored separately\n    #[cfg(unix)]\n    {\n        assert!(Sandbox::shim_exists(\"node\"));\n        assert!(Sandbox::shim_exists(\"yarn\"));\n        assert!(Sandbox::shim_exists(\"npm\"));\n        assert!(Sandbox::shim_exists(\"npx\"));\n    }\n}\n\n#[test]\nfn legacy_v0_volta_home_is_upgraded() {\n    let s = sandbox().build();\n\n    // directories that are already created by the test framework\n    assert!(Sandbox::path_exists(\".volta\"));\n    assert!(Sandbox::path_exists(\".volta/cache/node\"));\n    assert!(Sandbox::path_exists(\".volta/tmp\"));\n    assert!(Sandbox::path_exists(\".volta/tools/inventory/node\"));\n    assert!(Sandbox::path_exists(\".volta/tools/inventory/packages\"));\n    assert!(Sandbox::path_exists(\".volta/tools/inventory/yarn\"));\n\n    // Layout file is not there\n    assert!(!Sandbox::path_exists(\".volta/layout.v1\"));\n    assert!(!Sandbox::path_exists(\".volta/layout.v2\"));\n    assert!(!Sandbox::path_exists(\".volta/layout.v3\"));\n\n    // running volta should not create anything else\n    assert_that!(s.volta(\"--version\"), execs().with_status(0));\n\n    // Layout should be updated to the most recent\n    assert!(Sandbox::path_exists(\".volta\"));\n    assert!(Sandbox::path_exists(\".volta/cache/node\"));\n    assert!(Sandbox::path_exists(\".volta/tmp\"));\n    assert!(Sandbox::path_exists(\".volta/tools/inventory/node\"));\n    assert!(!Sandbox::path_exists(\".volta/tools/inventory/packages\"));\n    assert!(Sandbox::path_exists(\".volta/tools/inventory/yarn\"));\n\n    // Most recent layout file should exist, others should not\n    assert!(!Sandbox::path_exists(\".volta/layout.v1\"));\n    assert!(!Sandbox::path_exists(\".volta/layout.v2\"));\n    assert!(!Sandbox::path_exists(\".volta/layout.v3\"));\n    assert!(Sandbox::path_exists(\".volta/layout.v4\"));\n\n    // shims should all be created\n    // NOTE: this doesn't work in Windows, because the default shims are stored separately\n    #[cfg(unix)]\n    {\n        assert!(Sandbox::shim_exists(\"node\"));\n        assert!(Sandbox::shim_exists(\"yarn\"));\n        assert!(Sandbox::shim_exists(\"npm\"));\n        assert!(Sandbox::shim_exists(\"npx\"));\n    }\n}\n\n#[test]\nfn tagged_v1_volta_home_is_upgraded() {\n    let s = sandbox()\n        .layout_file(\"v1\")\n        .file(\n            \".volta/tools/image/node/10.6.0/6.1.0/README.md\",\n            \"Irrelevant Contents\",\n        )\n        .node_npm_version_file(\"10.6.0\", \"6.1.0\")\n        .platform(\n            r#\"{\n            \"node\": {\n                \"runtime\": \"10.6.0\",\n                \"npm\": \"6.1.0\"\n            },\n            \"yarn\": null\n        }\"#,\n        )\n        .build();\n\n    // We are already tagged as a v1 layout\n    assert!(Sandbox::path_exists(\".volta/layout.v1\"));\n\n    // Node image directory exists\n    assert!(Sandbox::path_exists(\n        \".volta/tools/image/node/10.6.0/6.1.0/README.md\"\n    ));\n    assert!(Sandbox::path_exists(\n        \".volta/tools/inventory/node/node-v10.6.0-npm\"\n    ));\n\n    // Default platform includes npm version\n    assert!(Sandbox::read_default_platform().contains(r#\"\"npm\": \"6.1.0\"\"#));\n\n    // running volta should run the migration\n    assert_that!(s.volta(\"--version\"), execs().with_status(0));\n\n    // Default platform should not include an npm version\n    assert!(Sandbox::read_default_platform().contains(r#\"\"npm\": null\"#));\n\n    // Node image directory should be moved up and no longer contain the npm version\n    assert!(Sandbox::path_exists(\n        \".volta/tools/image/node/10.6.0/README.md\"\n    ));\n\n    // Directory structure should exist\n    assert!(Sandbox::path_exists(\".volta\"));\n    assert!(Sandbox::path_exists(\".volta/cache/node\"));\n    assert!(Sandbox::path_exists(\".volta/tmp\"));\n    assert!(Sandbox::path_exists(\".volta/tools/inventory/node\"));\n    assert!(Sandbox::path_exists(\".volta/tools/inventory/yarn\"));\n\n    // Most recent layout file should exist, others should not\n    assert!(!Sandbox::path_exists(\".volta/layout.v1\"));\n    assert!(!Sandbox::path_exists(\".volta/layout.v2\"));\n    assert!(!Sandbox::path_exists(\".volta/layout.v3\"));\n    assert!(Sandbox::path_exists(\".volta/layout.v4\"));\n\n    // shims should all be created\n    // NOTE: this doesn't work in Windows, because the default shims are stored separately\n    #[cfg(unix)]\n    {\n        assert!(Sandbox::shim_exists(\"node\"));\n        assert!(Sandbox::shim_exists(\"yarn\"));\n        assert!(Sandbox::shim_exists(\"npm\"));\n        assert!(Sandbox::shim_exists(\"npx\"));\n    }\n}\n\n#[test]\nfn tagged_v1_to_v2_keeps_custom_npm() {\n    let s = sandbox()\n        .layout_file(\"v1\")\n        .node_npm_version_file(\"10.6.0\", \"6.1.0\")\n        .platform(\n            r#\"{\n            \"node\": {\n                \"runtime\": \"10.6.0\",\n                \"npm\": \"6.3.0\"\n            },\n            \"yarn\": null\n        }\"#,\n        )\n        .build();\n\n    // Default platform includes npm version\n    assert!(Sandbox::read_default_platform().contains(r#\"\"npm\": \"6.3.0\"\"#));\n\n    // running volta should run the migration\n    assert_that!(s.volta(\"--version\"), execs().with_status(0));\n\n    // Default platform still includes custom npm version\n    assert!(Sandbox::read_default_platform().contains(r#\"\"npm\": \"6.3.0\"\"#));\n}\n\n#[test]\nfn tagged_v1_to_v2_keeps_migrated_node_images() {\n    let s = sandbox()\n        .layout_file(\"v1\")\n        .file(\n            \".volta/tools/image/node/10.6.0/README.md\",\n            \"Irrelevant Contents\",\n        )\n        .node_npm_version_file(\"10.6.0\", \"6.1.0\")\n        .build();\n\n    // Migrated Node image directory exists\n    assert!(Sandbox::path_exists(\n        \".volta/tools/image/node/10.6.0/README.md\"\n    ));\n\n    // running volta should run the migration\n    assert_that!(s.volta(\"--version\"), execs().with_status(0));\n\n    // Migrated Node image directory is unchanged\n    assert!(Sandbox::path_exists(\n        \".volta/tools/image/node/10.6.0/README.md\"\n    ));\n}\n\n#[test]\nfn current_v4_volta_home_is_unchanged() {\n    let s = sandbox().layout_file(\"v4\").build();\n\n    // directories that are already created by the test framework\n    assert!(Sandbox::path_exists(\".volta\"));\n    assert!(Sandbox::path_exists(\".volta/layout.v4\"));\n    assert!(Sandbox::path_exists(\".volta/cache/node\"));\n    assert!(Sandbox::path_exists(\".volta/tmp\"));\n    assert!(Sandbox::path_exists(\".volta/tools/inventory/node\"));\n    assert!(Sandbox::path_exists(\".volta/tools/inventory/yarn\"));\n\n    // running volta should not create anything else\n    assert_that!(s.volta(\"--version\"), execs().with_status(0));\n\n    // everything should be the same as before running the command\n    assert!(Sandbox::path_exists(\".volta\"));\n    assert!(Sandbox::path_exists(\".volta/layout.v4\"));\n    assert!(Sandbox::path_exists(\".volta/cache/node\"));\n    assert!(Sandbox::path_exists(\".volta/tmp\"));\n    assert!(Sandbox::path_exists(\".volta/tools/inventory/node\"));\n    assert!(Sandbox::path_exists(\".volta/tools/inventory/yarn\"));\n}\n"
  },
  {
    "path": "tests/acceptance/run_shim_directly.rs",
    "content": "use crate::support::sandbox::{sandbox, shim_exe};\nuse hamcrest2::assert_that;\nuse hamcrest2::prelude::*;\nuse test_support::matchers::execs;\n\nuse volta_core::error::ExitCode;\n\n#[test]\nfn shows_pretty_error_when_calling_shim_directly() {\n    let s = sandbox().build();\n\n    assert_that!(\n        s.process(shim_exe()),\n        execs()\n            .with_status(ExitCode::ExecutionFailure as i32)\n            .with_stderr_contains(\"[..]should not be called directly[..]\")\n    );\n}\n"
  },
  {
    "path": "tests/acceptance/support/events_helpers.rs",
    "content": "use std::fs::File;\n\nuse crate::support::sandbox::Sandbox;\nuse hamcrest2::assert_that;\nuse hamcrest2::prelude::*;\n\nuse volta_core::event::{Event, EventKind};\n\npub enum EventKindMatcher<'a> {\n    Start,\n    End { exit_code: i32 },\n    Error { exit_code: i32, error: &'a str },\n    ToolEnd { exit_code: i32 },\n    Args { argv: &'a str },\n}\n\npub fn match_start() -> EventKindMatcher<'static> {\n    EventKindMatcher::Start\n}\n\npub fn match_error(exit_code: i32, error: &str) -> EventKindMatcher {\n    EventKindMatcher::Error { exit_code, error }\n}\n\npub fn match_end(exit_code: i32) -> EventKindMatcher<'static> {\n    EventKindMatcher::End { exit_code }\n}\n\npub fn match_tool_end(exit_code: i32) -> EventKindMatcher<'static> {\n    EventKindMatcher::ToolEnd { exit_code }\n}\n\npub fn match_args(argv: &str) -> EventKindMatcher {\n    EventKindMatcher::Args { argv }\n}\n\npub fn assert_events(sandbox: &Sandbox, matchers: Vec<(&str, EventKindMatcher)>) {\n    let events_path = sandbox.root().join(\"events.json\");\n    assert_that!(&events_path, file_exists());\n\n    let events_file = File::open(events_path).expect(\"Error reading 'events.json' file in sandbox\");\n    let events: Vec<Event> = serde_json::de::from_reader(events_file)\n        .expect(\"Error parsing 'events.json' file in sandbox\");\n    assert_that!(events.len(), eq(matchers.len()));\n\n    for (i, matcher) in matchers.iter().enumerate() {\n        assert_that!(&events[i].name, eq(matcher.0));\n        match matcher.1 {\n            EventKindMatcher::Start => {\n                assert_that!(&events[i].event, eq(&EventKind::Start));\n            }\n            EventKindMatcher::End {\n                exit_code: expected_exit_code,\n            } => {\n                if let EventKind::End { exit_code } = &events[i].event {\n                    assert_that!(*exit_code, eq(expected_exit_code));\n                } else {\n                    panic!(\n                        \"Expected: End {{ exit_code: {} }}, Got: {:?}\",\n                        expected_exit_code, events[i].event\n                    );\n                }\n            }\n            EventKindMatcher::Error {\n                exit_code: expected_exit_code,\n                error: expected_error,\n            } => {\n                if let EventKind::Error {\n                    exit_code, error, ..\n                } = &events[i].event\n                {\n                    assert_that!(*exit_code, eq(expected_exit_code));\n                    assert_that!(error.clone(), matches_regex(expected_error));\n                } else {\n                    panic!(\n                        \"Expected: Error {{ exit_code: {}, error: {} }}, Got: {:?}\",\n                        expected_exit_code, expected_error, events[i].event\n                    );\n                }\n            }\n            EventKindMatcher::ToolEnd {\n                exit_code: expected_exit_code,\n            } => {\n                if let EventKind::End { exit_code } = &events[i].event {\n                    assert_that!(*exit_code, eq(expected_exit_code));\n                } else {\n                    panic!(\n                        \"Expected: ToolEnd {{ exit_code: {} }}, Got: {:?}\",\n                        expected_exit_code, events[i].event\n                    );\n                }\n            }\n            EventKindMatcher::Args {\n                argv: expected_argv,\n            } => {\n                if let EventKind::Args { argv } = &events[i].event {\n                    assert_that!(argv.clone(), matches_regex(expected_argv));\n                } else {\n                    panic!(\n                        \"Expected: Args {{ argv: {} }}, Got: {:?}\",\n                        expected_argv, events[i].event\n                    );\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "tests/acceptance/support/mod.rs",
    "content": "pub mod events_helpers;\npub mod sandbox;\n"
  },
  {
    "path": "tests/acceptance/support/sandbox.rs",
    "content": "use std::env;\nuse std::ffi::{OsStr, OsString};\nuse std::fs::{self, File};\nuse std::io::{Read, Write};\nuse std::path::{Path, PathBuf};\nuse std::time::{Duration, SystemTime};\n\nuse cfg_if::cfg_if;\nuse headers::{Expires, Header};\nuse mockito::{self, mock, Matcher};\nuse node_semver::Version;\nuse test_support::{self, ok_or_panic, paths, paths::PathExt, process::ProcessBuilder};\nuse volta_core::fs::{set_executable, symlink_file};\nuse volta_core::tool::{Node, Pnpm, Yarn};\n\n// version cache for node and yarn\n#[derive(PartialEq, Clone)]\nstruct CacheBuilder {\n    path: PathBuf,\n    expiry_path: PathBuf,\n    contents: String,\n    expired: bool,\n}\n\nimpl CacheBuilder {\n    #[allow(dead_code)]\n    pub fn new(path: PathBuf, expiry_path: PathBuf, contents: &str, expired: bool) -> CacheBuilder {\n        CacheBuilder {\n            path,\n            expiry_path,\n            contents: contents.to_string(),\n            expired,\n        }\n    }\n\n    fn build(&self) {\n        self.dirname().mkdir_p();\n\n        // write cache file\n        let mut cache_file = File::create(&self.path).unwrap_or_else(|e| {\n            panic!(\"could not create cache file {}: {}\", self.path.display(), e)\n        });\n        ok_or_panic! { cache_file.write_all(self.contents.as_bytes()) };\n\n        // write expiry file\n        let one_day = Duration::from_secs(24 * 60 * 60);\n        let expiry_date = Expires::from(if self.expired {\n            SystemTime::now() - one_day\n        } else {\n            SystemTime::now() + one_day\n        });\n\n        let mut header_values = Vec::with_capacity(1);\n        expiry_date.encode(&mut header_values);\n        // Since we just `.encode()`d into `header_values, it is guaranteed to\n        // have a `.first()`.\n        let encoded_expiry_date = header_values.first().unwrap();\n\n        let mut expiry_file = File::create(&self.expiry_path).unwrap_or_else(|e| {\n            panic!(\n                \"could not create cache expiry file {}: {}\",\n                self.expiry_path.display(),\n                e\n            )\n        });\n        ok_or_panic! { expiry_file.write_all(encoded_expiry_date.as_bytes()) };\n    }\n\n    fn dirname(&self) -> &Path {\n        self.path.parent().unwrap()\n    }\n}\n\n// environment variables\npub struct EnvVar {\n    name: String,\n    value: String,\n}\n\nimpl EnvVar {\n    pub fn new(name: &str, value: &str) -> Self {\n        EnvVar {\n            name: name.to_string(),\n            value: value.to_string(),\n        }\n    }\n}\n\n// used to construct sandboxed files like package.json, platform.json, etc.\n#[derive(PartialEq, Eq, Clone)]\npub struct FileBuilder {\n    path: PathBuf,\n    contents: String,\n    executable: bool,\n}\n\nimpl FileBuilder {\n    pub fn new(path: PathBuf, contents: &str) -> FileBuilder {\n        FileBuilder {\n            path,\n            contents: contents.to_string(),\n            executable: false,\n        }\n    }\n\n    pub fn make_executable(mut self) -> Self {\n        self.executable = true;\n        self\n    }\n\n    pub fn build(&self) {\n        self.dirname().mkdir_p();\n\n        let mut file = File::create(&self.path)\n            .unwrap_or_else(|e| panic!(\"could not create file {}: {}\", self.path.display(), e));\n\n        ok_or_panic! { file.write_all(self.contents.as_bytes()) };\n        if self.executable {\n            ok_or_panic! { set_executable(&self.path) };\n        }\n    }\n\n    fn dirname(&self) -> &Path {\n        self.path.parent().unwrap()\n    }\n}\n\nstruct ShimBuilder {\n    name: String,\n}\n\nimpl ShimBuilder {\n    fn new(name: String) -> ShimBuilder {\n        ShimBuilder { name }\n    }\n\n    fn build(&self) {\n        ok_or_panic! { symlink_file(shim_exe(), shim_file(&self.name)) };\n    }\n}\n\n// used to setup executable binaries in installed packages\npub struct PackageBinInfo {\n    pub name: String,\n    pub contents: String,\n}\n\n#[must_use]\npub struct SandboxBuilder {\n    root: Sandbox,\n    files: Vec<FileBuilder>,\n    caches: Vec<CacheBuilder>,\n    path_dirs: Vec<PathBuf>,\n    shims: Vec<ShimBuilder>,\n    has_exec_path: bool,\n}\n\npub trait DistroFixture: From<DistroMetadata> {\n    fn server_path(&self) -> String;\n    fn fixture_path(&self) -> String;\n    fn metadata(&self) -> &DistroMetadata;\n}\n\n#[derive(Clone)]\npub struct DistroMetadata {\n    pub version: &'static str,\n    pub compressed_size: u32,\n    pub uncompressed_size: Option<u32>,\n}\n\npub struct NodeFixture {\n    pub metadata: DistroMetadata,\n}\n\npub struct NpmFixture {\n    pub metadata: DistroMetadata,\n}\n\npub struct PnpmFixture {\n    pub metadata: DistroMetadata,\n}\n\npub struct Yarn1Fixture {\n    pub metadata: DistroMetadata,\n}\n\npub struct YarnBerryFixture {\n    pub metadata: DistroMetadata,\n}\n\nimpl From<DistroMetadata> for NodeFixture {\n    fn from(metadata: DistroMetadata) -> Self {\n        Self { metadata }\n    }\n}\n\nimpl From<DistroMetadata> for NpmFixture {\n    fn from(metadata: DistroMetadata) -> Self {\n        Self { metadata }\n    }\n}\n\nimpl From<DistroMetadata> for PnpmFixture {\n    fn from(metadata: DistroMetadata) -> Self {\n        Self { metadata }\n    }\n}\n\nimpl From<DistroMetadata> for Yarn1Fixture {\n    fn from(metadata: DistroMetadata) -> Self {\n        Self { metadata }\n    }\n}\n\nimpl From<DistroMetadata> for YarnBerryFixture {\n    fn from(metadata: DistroMetadata) -> Self {\n        Self { metadata }\n    }\n}\n\nimpl DistroFixture for NodeFixture {\n    fn server_path(&self) -> String {\n        let version = Version::parse(self.metadata.version).unwrap();\n        let filename = Node::archive_filename(&version);\n        format!(\"/v{version}/{filename}\")\n    }\n\n    fn fixture_path(&self) -> String {\n        let version = Version::parse(self.metadata.version).unwrap();\n        let filename = Node::archive_filename(&version);\n        format!(\"tests/fixtures/{filename}\")\n    }\n\n    fn metadata(&self) -> &DistroMetadata {\n        &self.metadata\n    }\n}\n\nimpl DistroFixture for NpmFixture {\n    fn server_path(&self) -> String {\n        format!(\"/npm/-/npm-{}.tgz\", self.metadata.version)\n    }\n\n    fn fixture_path(&self) -> String {\n        format!(\"tests/fixtures/npm-{}.tgz\", self.metadata.version)\n    }\n\n    fn metadata(&self) -> &DistroMetadata {\n        &self.metadata\n    }\n}\n\nimpl DistroFixture for PnpmFixture {\n    fn server_path(&self) -> String {\n        format!(\"/pnpm/-/pnpm-{}.tgz\", self.metadata.version)\n    }\n\n    fn fixture_path(&self) -> String {\n        format!(\"tests/fixtures/pnpm-{}.tgz\", self.metadata.version)\n    }\n\n    fn metadata(&self) -> &DistroMetadata {\n        &self.metadata\n    }\n}\n\nimpl DistroFixture for Yarn1Fixture {\n    fn server_path(&self) -> String {\n        format!(\"/yarn/-/yarn-{}.tgz\", self.metadata.version)\n    }\n\n    fn fixture_path(&self) -> String {\n        format!(\"tests/fixtures/yarn-{}.tgz\", self.metadata.version)\n    }\n\n    fn metadata(&self) -> &DistroMetadata {\n        &self.metadata\n    }\n}\n\nimpl DistroFixture for YarnBerryFixture {\n    fn server_path(&self) -> String {\n        format!(\n            \"/@yarnpkg/cli-dist/-/cli-dist-{}.tgz\",\n            self.metadata.version\n        )\n    }\n\n    fn fixture_path(&self) -> String {\n        format!(\"tests/fixtures/cli-dist-{}.tgz\", self.metadata.version)\n    }\n\n    fn metadata(&self) -> &DistroMetadata {\n        &self.metadata\n    }\n}\n\nimpl SandboxBuilder {\n    /// Root of the project, ex: `/path/to/cargo/target/integration_test/t0/foo`\n    pub fn root(&self) -> PathBuf {\n        self.root.root()\n    }\n\n    pub fn new(root: PathBuf) -> SandboxBuilder {\n        SandboxBuilder {\n            root: Sandbox {\n                root,\n                mocks: vec![],\n                env_vars: vec![],\n                env_vars_remove: vec![],\n                path: OsString::new(),\n            },\n            files: vec![],\n            caches: vec![],\n            path_dirs: vec![volta_bin_dir()],\n            shims: vec![\n                ShimBuilder::new(\"npm\".to_string()),\n                ShimBuilder::new(\"pnpm\".to_string()),\n                ShimBuilder::new(\"yarn\".to_string()),\n            ],\n            has_exec_path: false,\n        }\n    }\n\n    #[allow(dead_code)]\n    /// Set the Node cache for the sandbox (chainable)\n    pub fn node_cache(mut self, cache: &str, expired: bool) -> Self {\n        self.caches.push(CacheBuilder::new(\n            node_index_file(),\n            node_index_expiry_file(),\n            cache,\n            expired,\n        ));\n        self\n    }\n\n    /// Set the package.json for the sandbox (chainable)\n    pub fn package_json(mut self, contents: &str) -> Self {\n        let package_file = package_json_file(self.root());\n        self.files.push(FileBuilder::new(package_file, contents));\n        self\n    }\n\n    /// Set the platform.json for the sandbox (chainable)\n    pub fn platform(mut self, contents: &str) -> Self {\n        self.files\n            .push(FileBuilder::new(default_platform_file(), contents));\n        self\n    }\n\n    /// Set the hooks.json for the sandbox\n    pub fn default_hooks(mut self, contents: &str) -> Self {\n        self.files\n            .push(FileBuilder::new(default_hooks_file(), contents));\n        self\n    }\n\n    /// Set a layout version file for the sandbox (chainable)\n    pub fn layout_file(mut self, version: &str) -> Self {\n        self.files.push(FileBuilder::new(layout_file(version), \"\"));\n        self\n    }\n    /// Set an environment variable for the sandbox (chainable)\n    pub fn env(mut self, name: &str, value: &str) -> Self {\n        self.root.env_vars.push(EnvVar::new(name, value));\n        self\n    }\n\n    /// Setup mock to return the available node versions (chainable)\n    pub fn node_available_versions(mut self, body: &str) -> Self {\n        let mock = mock(\"GET\", \"/node-dist/index.json\")\n            .with_status(200)\n            .with_header(\"content-type\", \"application/json\")\n            .with_body(body)\n            .create();\n        self.root.mocks.push(mock);\n\n        self\n    }\n\n    /// Setup mock to return the available Yarn@1 versions (chainable)\n    pub fn yarn_1_available_versions(mut self, body: &str) -> Self {\n        let mock = mock(\"GET\", \"/yarn\")\n            .with_status(200)\n            .with_header(\"content-type\", \"application/json\")\n            .with_body(body)\n            .create();\n        self.root.mocks.push(mock);\n        self\n    }\n\n    /// Setup mock to return the available Yarn@2+ versions (chainable)\n    pub fn yarn_berry_available_versions(mut self, body: &str) -> Self {\n        let mock = mock(\"GET\", \"/@yarnpkg/cli-dist\")\n            .with_status(200)\n            .with_header(\"content-type\", \"application/json\")\n            .with_body(body)\n            .create();\n        self.root.mocks.push(mock);\n        self\n    }\n\n    /// Setup mock to return the available npm versions (chainable)\n    pub fn npm_available_versions(mut self, body: &str) -> Self {\n        let mock = mock(\"GET\", \"/npm\")\n            .with_status(200)\n            .with_header(\"content-type\", \"application/json\")\n            .with_body(body)\n            .create();\n        self.root.mocks.push(mock);\n\n        self\n    }\n\n    /// Setup mock to return the available pnpm versions (chainable)\n    pub fn pnpm_available_versions(mut self, body: &str) -> Self {\n        let mock = mock(\"GET\", \"/pnpm\")\n            .with_status(200)\n            .with_header(\"content-type\", \"application/json\")\n            .with_body(body)\n            .create();\n        self.root.mocks.push(mock);\n\n        self\n    }\n\n    /// Setup mock to return a 404 for any GET request\n    /// Note: Mocks are matched in reverse order, so any created _after_ this will work\n    ///       While those created before will not\n    pub fn mock_not_found(mut self) -> Self {\n        let mock = mock(\"GET\", Matcher::Any).with_status(404).create();\n        self.root.mocks.push(mock);\n        self\n    }\n\n    fn distro_mock<T: DistroFixture>(mut self, fx: &T) -> Self {\n        // ISSUE(#145): this should actually use a real http server instead of these mocks\n\n        let server_path = fx.server_path();\n        let fixture_path = fx.fixture_path();\n\n        let metadata = fx.metadata();\n\n        if let Some(uncompressed_size) = metadata.uncompressed_size {\n            // This can be abstracted when https://github.com/rust-lang/rust/issues/52963 lands.\n            let uncompressed_size_bytes: [u8; 4] = [\n                ((uncompressed_size & 0xff00_0000) >> 24) as u8,\n                ((uncompressed_size & 0x00ff_0000) >> 16) as u8,\n                ((uncompressed_size & 0x0000_ff00) >> 8) as u8,\n                (uncompressed_size & 0x0000_00ff) as u8,\n            ];\n\n            let range_mock = mock(\"GET\", &server_path[..])\n                .match_header(\"Range\", Matcher::Any)\n                .with_body(uncompressed_size_bytes)\n                .create();\n            self.root.mocks.push(range_mock);\n        }\n\n        let file_mock = mock(\"GET\", &server_path[..])\n            .match_header(\"Range\", Matcher::Missing)\n            .with_header(\"Accept-Ranges\", \"bytes\")\n            .with_body_from_file(fixture_path)\n            .create();\n        self.root.mocks.push(file_mock);\n\n        self\n    }\n\n    pub fn distro_mocks<T: DistroFixture>(self, fixtures: &[DistroMetadata]) -> Self {\n        let mut this = self;\n        for fixture in fixtures {\n            this = this.distro_mock::<T>(&fixture.clone().into());\n        }\n        this\n    }\n\n    /// Add an arbitrary file to the sandbox (chainable)\n    pub fn file(mut self, path: &str, contents: &str) -> Self {\n        let file_name = sandbox_path(path);\n        self.files.push(FileBuilder::new(file_name, contents));\n        self\n    }\n\n    /// Add an arbitrary file to the test project within the sandbox (chainable)\n    pub fn project_file(mut self, path: &str, contents: &str) -> Self {\n        let file_name = self.root().join(path);\n        self.files.push(FileBuilder::new(file_name, contents));\n        self\n    }\n\n    /// Add an arbitrary file to the test project within the sandbox,\n    /// give it executable permissions,\n    /// and add its directory to the PATH\n    /// (chainable)\n    pub fn executable_file(mut self, path: &str, contents: &str) -> Self {\n        let file_name = self.root().join(\"exec\").join(path);\n        self.files\n            .push(FileBuilder::new(file_name, contents).make_executable());\n        self.add_exec_dir_to_path()\n    }\n\n    /// Prepend executable directory to the beginning of the PATH (chainable)\n    ///\n    /// This is useful to test binaries shadowing volta shims.\n    ///\n    /// Cannot be used in combination with `add_exec_dir_to_path`, and will panic if called twice.\n    /// No particular reason except it's likely a programming error.\n    pub fn prepend_exec_dir_to_path(mut self) -> Self {\n        if self.has_exec_path {\n            panic!(\"need to call prepend_exec_dir_to_path before anything else\");\n        }\n\n        let exec_path = self.root().join(\"exec\");\n        self.path_dirs.insert(0, exec_path);\n        self.has_exec_path = true;\n        self\n    }\n\n    /// Set a package config file for the sandbox (chainable)\n    pub fn package_config(mut self, name: &str, contents: &str) -> Self {\n        let package_cfg_file = package_config_file(name);\n        self.files\n            .push(FileBuilder::new(package_cfg_file, contents));\n        self\n    }\n\n    /// Set a bin config file for the sandbox (chainable)\n    pub fn binary_config(mut self, name: &str, contents: &str) -> Self {\n        let bin_cfg_file = binary_config_file(name);\n        self.files.push(FileBuilder::new(bin_cfg_file, contents));\n        self\n    }\n\n    /// Set a shim file for the sandbox (chainable)\n    pub fn shim(mut self, name: &str) -> Self {\n        self.shims.push(ShimBuilder::new(name.to_string()));\n        self\n    }\n\n    /// Set an unpackaged package for the sandbox (chainable)\n    pub fn package_image(\n        mut self,\n        name: &str,\n        version: &str,\n        bins: Option<Vec<PackageBinInfo>>,\n    ) -> Self {\n        let package_img_dir = package_image_dir(name);\n        let package_json = package_img_dir.join(\"package.json\");\n        self.files.push(FileBuilder::new(\n            package_json,\n            &format!(r#\"{{\"name\":\"{}\",\"version\":\"{}\"}}\"#, name, version),\n        ));\n        if let Some(bin_infos) = bins {\n            for bin_info in bin_infos.iter() {\n                cfg_if! {\n                    if #[cfg(target_os = \"windows\")] {\n                        let bin_path = package_img_dir.join(format!(\"{}.cmd\", &bin_info.name));\n                    } else {\n                        let bin_path = package_img_dir.join(\"bin\").join(&bin_info.name);\n                    }\n                }\n                self.files\n                    .push(FileBuilder::new(bin_path, &bin_info.contents).make_executable());\n            }\n        }\n        self\n    }\n\n    /// Write executable project binaries into node_modules/.bin/ (chainable)\n    pub fn project_bins(mut self, bins: Vec<PackageBinInfo>) -> Self {\n        let project_bin_dir = self.root().join(\"node_modules\").join(\".bin\");\n        for bin_info in bins.iter() {\n            cfg_if! {\n                if #[cfg(target_os = \"windows\")] {\n                    // in Windows, binaries have an extra file with an executable extension\n                    let win_bin_path = project_bin_dir.join(format!(\"{}.cmd\", &bin_info.name));\n                    self.files.push(FileBuilder::new(win_bin_path, &bin_info.contents).make_executable());\n                }\n            }\n            // Volta on both Windows and Unix checks for the existence of the binary with no extension\n            let bin_path = project_bin_dir.join(&bin_info.name);\n            self.files\n                .push(FileBuilder::new(bin_path, &bin_info.contents).make_executable());\n        }\n        self\n    }\n\n    /// Write '.pnp.cjs' file in local project to mark as Plug-n-Play (chainable)\n    pub fn project_pnp(mut self) -> Self {\n        let pnp_path = self.root().join(\".pnp.cjs\");\n        self.files.push(FileBuilder::new(pnp_path, \"blegh\"));\n        self\n    }\n\n    /// Write an executable node binary with the input contents (chainable)\n    pub fn setup_node_binary(\n        mut self,\n        node_version: &str,\n        npm_version: &str,\n        contents: &str,\n    ) -> Self {\n        cfg_if! {\n            if #[cfg(target_os = \"windows\")] {\n                let node_file = \"node.cmd\";\n            } else {\n                let node_file = \"node\";\n            }\n        }\n        let node_bin_file = node_image_dir(node_version).join(\"bin\").join(node_file);\n        self.files\n            .push(FileBuilder::new(node_bin_file, contents).make_executable());\n        self.node_npm_version_file(node_version, npm_version)\n    }\n\n    /// Write an executable npm binary with the input contents (chainable)\n    pub fn setup_npm_binary(mut self, version: &str, contents: &str) -> Self {\n        cfg_if! {\n            if #[cfg(target_os = \"windows\")] {\n                let npm_file = \"npm.cmd\";\n            } else {\n                let npm_file = \"npm\";\n            }\n        }\n        let npm_bin_file = npm_image_dir(version).join(\"bin\").join(npm_file);\n        self.files\n            .push(FileBuilder::new(npm_bin_file, contents).make_executable());\n        self\n    }\n\n    /// Write an executable pnpm binary with the input contents (chainable)\n    pub fn setup_pnpm_binary(mut self, version: &str, contents: &str) -> Self {\n        cfg_if! {\n            if #[cfg(target_os = \"windows\")] {\n                let pnpm_file = \"pnpm.cmd\";\n            } else {\n                let pnpm_file = \"pnpm\";\n            }\n        }\n        let pnpm_bin_file = pnpm_image_dir(version).join(\"bin\").join(pnpm_file);\n        self.files\n            .push(FileBuilder::new(pnpm_bin_file, contents).make_executable());\n        self\n    }\n\n    /// Write an executable yarn binary with the input contents (chainable)\n    pub fn setup_yarn_binary(mut self, version: &str, contents: &str) -> Self {\n        cfg_if! {\n            if #[cfg(target_os = \"windows\")] {\n                let yarn_file = \"yarn.cmd\";\n            } else {\n                let yarn_file = \"yarn\";\n            }\n        }\n        let yarn_bin_file = yarn_image_dir(version).join(\"bin\").join(yarn_file);\n        self.files\n            .push(FileBuilder::new(yarn_bin_file, contents).make_executable());\n        self\n    }\n\n    /// Write the \"default npm\" file for a node version (chainable)\n    pub fn node_npm_version_file(mut self, node_version: &str, npm_version: &str) -> Self {\n        let npm_file = node_npm_version_file(node_version);\n        self.files.push(FileBuilder::new(npm_file, npm_version));\n        self\n    }\n\n    /// Add directory to the PATH (chainable)\n    pub fn add_dir_to_path(mut self, dir: PathBuf) -> Self {\n        self.path_dirs.push(dir);\n        self\n    }\n\n    /// Add executable directory to the PATH (chainable)\n    pub fn add_exec_dir_to_path(mut self) -> Self {\n        if !self.has_exec_path {\n            let exec_path = self.root().join(\"exec\");\n            self.path_dirs.push(exec_path);\n            self.has_exec_path = true;\n        }\n        self\n    }\n\n    /// Create the project\n    pub fn build(mut self) -> Sandbox {\n        // First, clean the directory if it already exists\n        self.rm_root();\n\n        // Create the empty directory\n        self.root.root().mkdir_p();\n\n        // make sure these directories exist\n        ok_or_panic! { fs::create_dir_all(volta_bin_dir()) };\n        ok_or_panic! { fs::create_dir_all(node_cache_dir()) };\n        ok_or_panic! { fs::create_dir_all(node_inventory_dir()) };\n        ok_or_panic! { fs::create_dir_all(package_inventory_dir()) };\n        ok_or_panic! { fs::create_dir_all(pnpm_inventory_dir()) };\n        ok_or_panic! { fs::create_dir_all(yarn_inventory_dir()) };\n        ok_or_panic! { fs::create_dir_all(volta_tmp_dir()) };\n\n        // write node and yarn caches\n        for cache in self.caches.iter() {\n            cache.build();\n        }\n\n        // write files\n        for file_builder in self.files {\n            file_builder.build();\n        }\n\n        // write shims\n        for shim_builder in self.shims {\n            shim_builder.build();\n        }\n\n        // join dirs for the path (volta bin path is already first)\n        self.root.path = env::join_paths(self.path_dirs.iter()).unwrap();\n\n        let SandboxBuilder { root, .. } = self;\n        root\n    }\n\n    fn rm_root(&self) {\n        self.root.root().rm_rf()\n    }\n}\n\n// files and dirs in the sandbox\n\nfn home_dir() -> PathBuf {\n    paths::home()\n}\nfn volta_home() -> PathBuf {\n    home_dir().join(\".volta\")\n}\nfn volta_tmp_dir() -> PathBuf {\n    volta_home().join(\"tmp\")\n}\nfn volta_bin_dir() -> PathBuf {\n    volta_home().join(\"bin\")\n}\nfn volta_log_dir() -> PathBuf {\n    volta_home().join(\"log\")\n}\nfn volta_postscript() -> PathBuf {\n    volta_tmp_dir().join(\"volta_tmp_1234.sh\")\n}\nfn volta_tools_dir() -> PathBuf {\n    volta_home().join(\"tools\")\n}\nfn inventory_dir() -> PathBuf {\n    volta_tools_dir().join(\"inventory\")\n}\nfn user_dir() -> PathBuf {\n    volta_tools_dir().join(\"user\")\n}\nfn image_dir() -> PathBuf {\n    volta_tools_dir().join(\"image\")\n}\nfn node_inventory_dir() -> PathBuf {\n    inventory_dir().join(\"node\")\n}\nfn pnpm_inventory_dir() -> PathBuf {\n    inventory_dir().join(\"pnpm\")\n}\nfn yarn_inventory_dir() -> PathBuf {\n    inventory_dir().join(\"yarn\")\n}\nfn package_inventory_dir() -> PathBuf {\n    inventory_dir().join(\"packages\")\n}\nfn cache_dir() -> PathBuf {\n    volta_home().join(\"cache\")\n}\nfn node_cache_dir() -> PathBuf {\n    cache_dir().join(\"node\")\n}\n#[allow(dead_code)]\nfn node_index_file() -> PathBuf {\n    node_cache_dir().join(\"index.json\")\n}\n#[allow(dead_code)]\nfn node_index_expiry_file() -> PathBuf {\n    node_cache_dir().join(\"index.json.expires\")\n}\nfn package_json_file(mut root: PathBuf) -> PathBuf {\n    root.push(\"package.json\");\n    root\n}\nfn package_config_file(name: &str) -> PathBuf {\n    user_dir().join(\"packages\").join(format!(\"{}.json\", name))\n}\nfn binary_config_file(name: &str) -> PathBuf {\n    user_dir().join(\"bins\").join(format!(\"{}.json\", name))\n}\nfn shim_file(name: &str) -> PathBuf {\n    volta_bin_dir().join(format!(\"{}{}\", name, env::consts::EXE_SUFFIX))\n}\nfn package_image_dir(name: &str) -> PathBuf {\n    image_dir().join(\"packages\").join(name)\n}\nfn node_image_dir(version: &str) -> PathBuf {\n    image_dir().join(\"node\").join(version)\n}\nfn npm_image_dir(version: &str) -> PathBuf {\n    image_dir().join(\"npm\").join(version)\n}\nfn pnpm_image_dir(version: &str) -> PathBuf {\n    image_dir().join(\"pnpm\").join(version)\n}\nfn yarn_image_dir(version: &str) -> PathBuf {\n    image_dir().join(\"yarn\").join(version)\n}\nfn default_platform_file() -> PathBuf {\n    user_dir().join(\"platform.json\")\n}\nfn default_hooks_file() -> PathBuf {\n    volta_home().join(\"hooks.json\")\n}\nfn layout_file(version: &str) -> PathBuf {\n    volta_home().join(format!(\"layout.{}\", version))\n}\nfn node_npm_version_file(node_version: &str) -> PathBuf {\n    node_inventory_dir().join(format!(\"node-v{}-npm\", node_version))\n}\n\nfn sandbox_path(path: &str) -> PathBuf {\n    home_dir().join(path)\n}\n\npub struct Sandbox {\n    root: PathBuf,\n    mocks: Vec<mockito::Mock>,\n    env_vars: Vec<EnvVar>,\n    env_vars_remove: Vec<String>,\n    path: OsString,\n}\n\nimpl Sandbox {\n    /// Root of the project, ex: `/path/to/cargo/target/integration_test/t0/foo`\n    pub fn root(&self) -> PathBuf {\n        self.root.clone()\n    }\n\n    /// Create a `ProcessBuilder` to run a program in the project.\n    /// Example:\n    ///         assert_that(\n    ///             p.process(&p.bin(\"foo\")),\n    ///             execs().with_stdout(\"bar\\n\"),\n    ///         );\n    pub fn process<T: AsRef<OsStr>>(&self, program: T) -> ProcessBuilder {\n        let mut p = test_support::process::process(program);\n        p.cwd(self.root())\n            // sandbox the Volta environment\n            .env(\"VOLTA_HOME\", volta_home())\n            .env(\"VOLTA_INSTALL_DIR\", cargo_dir())\n            .env(\"PATH\", &self.path)\n            .env(\"VOLTA_POSTSCRIPT\", volta_postscript())\n            .env_remove(\"VOLTA_SHELL\")\n            .env_remove(\"MSYSTEM\"); // assume cmd.exe everywhere on windows\n\n        // overrides for env vars\n        for env_var in &self.env_vars {\n            p.env(&env_var.name, &env_var.value);\n        }\n\n        for env_var_name in &self.env_vars_remove {\n            p.env_remove(env_var_name);\n        }\n\n        p\n    }\n\n    /// Create a `ProcessBuilder` to run volta.\n    /// Arguments can be separated by spaces.\n    /// Example:\n    ///     assert_that(p.volta(\"use node 9.5\"), execs());\n    pub fn volta(&self, cmd: &str) -> ProcessBuilder {\n        let mut p = self.process(volta_exe());\n        split_and_add_args(&mut p, cmd);\n        p\n    }\n\n    /// Create a `ProcessBuilder` to run the volta npm shim.\n    /// Arguments can be separated by spaces.\n    /// Example:\n    ///     assert_that(p.npm(\"install ember-cli\"), execs());\n    pub fn npm(&self, cmd: &str) -> ProcessBuilder {\n        self.exec_shim(\"npm\", cmd)\n    }\n\n    /// Create a `ProcessBuilder` to run the volta pnpm shim.\n    /// Arguments can be separated by spaces.\n    /// Example:\n    ///     assert_that(p.pnpm(\"add ember-cli\"), execs());\n    pub fn pnpm(&self, cmd: &str) -> ProcessBuilder {\n        self.exec_shim(\"pnpm\", cmd)\n    }\n\n    /// Create a `ProcessBuilder` to run the volta yarn shim.\n    /// Arguments can be separated by spaces.\n    /// Example:\n    ///     assert_that(p.yarn(\"add ember-cli\"), execs());\n    pub fn yarn(&self, cmd: &str) -> ProcessBuilder {\n        self.exec_shim(\"yarn\", cmd)\n    }\n\n    /// Create a `ProcessBuilder` to run an arbitrary shim.\n    /// Arguments can be separated by spaces.\n    /// Example:\n    ///     assert_that(p.exec_shim(\"cowsay\", \"foo bar\"), execs());\n    pub fn exec_shim(&self, bin: &str, cmd: &str) -> ProcessBuilder {\n        let mut p = self.process(shim_file(bin));\n        split_and_add_args(&mut p, cmd);\n        p\n    }\n\n    pub fn read_package_json(&self) -> String {\n        let package_file = package_json_file(self.root());\n        read_file_to_string(package_file)\n    }\n\n    pub fn read_log_dir(&self) -> Option<fs::ReadDir> {\n        fs::read_dir(volta_log_dir()).ok()\n    }\n\n    pub fn remove_volta_home(&self) {\n        volta_home().rm_rf();\n    }\n\n    // check that files in the sandbox exist\n\n    pub fn node_inventory_archive_exists(&self, version: &Version) -> bool {\n        node_inventory_dir()\n            .join(Node::archive_filename(version))\n            .exists()\n    }\n\n    pub fn pnpm_inventory_archive_exists(&self, version: &str) -> bool {\n        pnpm_inventory_dir()\n            .join(Pnpm::archive_filename(version))\n            .exists()\n    }\n\n    pub fn yarn_inventory_archive_exists(&self, version: &str) -> bool {\n        yarn_inventory_dir()\n            .join(Yarn::archive_filename(version))\n            .exists()\n    }\n\n    pub fn package_config_exists(name: &str) -> bool {\n        package_config_file(name).exists()\n    }\n    pub fn bin_config_exists(name: &str) -> bool {\n        binary_config_file(name).exists()\n    }\n    pub fn shim_exists(name: &str) -> bool {\n        shim_file(name).exists()\n    }\n    pub fn path_exists(path: &str) -> bool {\n        sandbox_path(path).exists()\n    }\n    pub fn package_image_exists(name: &str) -> bool {\n        let package_img_dir = package_image_dir(name);\n        package_img_dir.join(\"package.json\").exists()\n    }\n    pub fn read_default_platform() -> String {\n        read_file_to_string(default_platform_file())\n    }\n}\n\nimpl Drop for Sandbox {\n    fn drop(&mut self) {\n        paths::root().rm_rf();\n    }\n}\n\n// Generates a sandboxed environment\npub fn sandbox() -> SandboxBuilder {\n    SandboxBuilder::new(paths::root().join(\"sandbox\"))\n}\n\n// Path to compiled executables\npub fn cargo_dir() -> PathBuf {\n    env::var_os(\"CARGO_BIN_PATH\")\n        .map(PathBuf::from)\n        .or_else(|| {\n            env::current_exe().ok().map(|mut path| {\n                path.pop();\n                if path.ends_with(\"deps\") {\n                    path.pop();\n                }\n                path\n            })\n        })\n        .unwrap_or_else(|| panic!(\"CARGO_BIN_PATH wasn't set. Cannot continue running test\"))\n}\n\nfn volta_exe() -> PathBuf {\n    cargo_dir().join(format!(\"volta{}\", env::consts::EXE_SUFFIX))\n}\n\npub fn shim_exe() -> PathBuf {\n    cargo_dir().join(format!(\"volta-shim{}\", env::consts::EXE_SUFFIX))\n}\n\nfn split_and_add_args(p: &mut ProcessBuilder, s: &str) {\n    for arg in s.split_whitespace() {\n        if arg.contains('\"') || arg.contains('\\'') {\n            panic!(\"shell-style argument parsing is not supported\")\n        }\n        p.arg(arg);\n    }\n}\n\nfn read_file_to_string(file_path: PathBuf) -> String {\n    let mut contents = String::new();\n    let mut file = ok_or_panic! { File::open(file_path) };\n    ok_or_panic! { file.read_to_string(&mut contents) };\n    contents\n}\n"
  },
  {
    "path": "tests/acceptance/verbose_errors.rs",
    "content": "use crate::support::sandbox::sandbox;\nuse ci_info::types::{CiInfo, Vendor};\nuse hamcrest2::assert_that;\nuse hamcrest2::prelude::*;\nuse test_support::matchers::execs;\n\nuse volta_core::error::ExitCode;\n\nconst NODE_VERSION_INFO: &str = r#\"[\n{\"version\":\"v10.99.1040\",\"npm\":\"6.2.26\",\"lts\": \"Dubnium\",\"files\":[\"linux-x64\",\"osx-x64-tar\",\"win-x64-zip\",\"win-x86-zip\",\"linux-arm64\"]},\n{\"version\":\"v9.27.6\",\"npm\":\"5.6.17\",\"lts\": false,\"files\":[\"linux-x64\",\"osx-x64-tar\",\"win-x64-zip\",\"win-x86-zip\",\"linux-arm64\"]},\n{\"version\":\"v8.9.10\",\"npm\":\"5.6.7\",\"lts\": false,\"files\":[\"linux-x64\",\"osx-x64-tar\",\"win-x64-zip\",\"win-x86-zip\",\"linux-arm64\"]},\n{\"version\":\"v6.19.62\",\"npm\":\"3.10.1066\",\"lts\": false,\"files\":[\"linux-x64\",\"osx-x64-tar\",\"win-x64-zip\",\"win-x86-zip\",\"linux-arm64\"]}\n]\n\"#;\n\n#[test]\nfn no_cause_shown_if_no_verbose_flag() {\n    let s = sandbox().node_available_versions(NODE_VERSION_INFO).build();\n\n    // Mock `is_ci` to false so that this works even when running in Volta's CI Test Suite\n    ci_info::mock_ci(&CiInfo::new());\n\n    assert_that!(\n        s.volta(\"install node@10\"),\n        execs()\n            .with_status(ExitCode::NetworkError as i32)\n            .with_stderr_does_not_contain(\"[..]Error cause[..]\")\n    );\n}\n\n#[test]\nfn cause_shown_if_verbose_flag() {\n    let s = sandbox().node_available_versions(NODE_VERSION_INFO).build();\n\n    // Mock `is_ci` to false so that this correctly tests the verbose flag\n    ci_info::mock_ci(&CiInfo::new());\n\n    assert_that!(\n        s.volta(\"install node@10 --verbose\"),\n        execs()\n            .with_status(ExitCode::NetworkError as i32)\n            .with_stderr_contains(\"[..]Error cause[..]\")\n    );\n}\n\n#[test]\nfn no_cause_if_no_underlying_error() {\n    let s = sandbox().build();\n\n    assert_that!(\n        s.volta(\"use --verbose\"),\n        execs()\n            .with_status(ExitCode::InvalidArguments as i32)\n            .with_stderr_does_not_contain(\"[..]Error cause[..]\")\n    );\n}\n\n#[test]\nfn error_log_if_underlying_cause() {\n    let s = sandbox().node_available_versions(NODE_VERSION_INFO).build();\n\n    // Mock `is_ci` to false so that this works even when running Volta's CI Test Suite\n    ci_info::mock_ci(&CiInfo::new());\n\n    assert_that!(\n        s.volta(\"install node@10\"),\n        execs()\n            .with_status(ExitCode::NetworkError as i32)\n            .with_stderr_contains(\"Error details written to[..]\")\n    );\n\n    let mut log_dir_contents = s.read_log_dir().expect(\"Could not read log directory\");\n    assert_that!(log_dir_contents.next(), some());\n}\n\n#[test]\nfn no_error_log_if_no_underlying_cause() {\n    let s = sandbox().build();\n\n    assert_that!(\n        s.volta(\"use\"),\n        execs()\n            .with_status(ExitCode::InvalidArguments as i32)\n            .with_stderr_does_not_contain(\"Error details written to[..]\")\n    );\n\n    // The log directory may not exist at all. If so, we know we didn't write to it\n    if let Some(mut log_dir_contents) = s.read_log_dir() {\n        assert_that!(log_dir_contents.next(), none());\n    }\n}\n\n#[test]\nfn cause_shown_in_ci() {\n    let s = sandbox()\n        .node_available_versions(NODE_VERSION_INFO)\n        .env(\"VOLTA_LOGLEVEL\", \"error\")\n        .build();\n\n    // Mock a CI environment so this works even when running locally\n    let mut ci_mock = CiInfo::new();\n    ci_mock.vendor = Some(Vendor::GitHubActions);\n    ci_mock.ci = true;\n    ci_info::mock_ci(&ci_mock);\n\n    assert_that!(\n        s.volta(\"install node@10\"),\n        execs()\n            .with_status(ExitCode::NetworkError as i32)\n            .with_stderr_contains(\"[..]Error cause[..]\")\n    );\n}\n\n#[test]\nfn no_error_log_in_ci() {\n    let s = sandbox().node_available_versions(NODE_VERSION_INFO).build();\n\n    // Mock a CI environment so this works even when running locally\n    let mut ci_mock = CiInfo::new();\n    ci_mock.vendor = Some(Vendor::GitHubActions);\n    ci_mock.ci = true;\n    ci_info::mock_ci(&ci_mock);\n\n    assert_that!(\n        s.volta(\"install node@10\"),\n        execs()\n            .with_status(ExitCode::NetworkError as i32)\n            .with_stderr_does_not_contain(\"Error details written to[..]\")\n    );\n\n    // The log directory may not exist at all. If so, we know we didn't write to it\n    if let Some(mut log_dir_contents) = s.read_log_dir() {\n        assert_that!(log_dir_contents.next(), none());\n    }\n}\n"
  },
  {
    "path": "tests/acceptance/volta_bypass.rs",
    "content": "use crate::support::sandbox::{sandbox, shim_exe};\nuse hamcrest2::assert_that;\nuse hamcrest2::prelude::*;\nuse test_support::matchers::execs;\n\nuse volta_core::error::ExitCode;\n\n#[test]\nfn shim_skips_platform_checks_on_bypass() {\n    let s = sandbox()\n        .env(\"VOLTA_BYPASS\", \"1\")\n        .env(\n            \"VOLTA_INSTALL_DIR\",\n            &shim_exe().parent().unwrap().to_string_lossy(),\n        )\n        .build();\n\n    #[cfg(unix)]\n    assert_that!(\n        s.process(shim_exe()),\n        execs()\n            .with_status(ExitCode::ExecutionFailure as i32)\n            .with_stderr_contains(\"VOLTA_BYPASS is enabled[..]\")\n    );\n\n    #[cfg(windows)]\n    assert_that!(\n        s.process(shim_exe()),\n        execs()\n            .with_status(ExitCode::UnknownError as i32)\n            .with_stderr_contains(\"[..]is not recognized as an internal or external command[..]\")\n    );\n}\n"
  },
  {
    "path": "tests/acceptance/volta_install.rs",
    "content": "use crate::support::sandbox::{\n    sandbox, DistroMetadata, NodeFixture, NpmFixture, PnpmFixture, Sandbox, Yarn1Fixture,\n    YarnBerryFixture,\n};\nuse hamcrest2::assert_that;\nuse hamcrest2::prelude::*;\nuse test_support::matchers::execs;\n\nuse volta_core::error::ExitCode;\n\nfn platform_with_node(node: &str) -> String {\n    format!(\n        r#\"{{\n  \"node\": {{\n    \"runtime\": \"{}\",\n    \"npm\": null\n  }},\n  \"pnpm\": null,\n  \"yarn\": null\n}}\"#,\n        node\n    )\n}\n\nfn platform_with_node_npm(node: &str, npm: &str) -> String {\n    format!(\n        r#\"{{\n  \"node\": {{\n    \"runtime\": \"{}\",\n    \"npm\": \"{}\"\n  }},\n  \"pnpm\": null,\n  \"yarn\": null\n}}\"#,\n        node, npm\n    )\n}\n\nconst NODE_VERSION_INFO: &str = r#\"[\n{\"version\":\"v10.99.1040\",\"npm\":\"6.2.26\",\"lts\": \"Dubnium\",\"files\":[\"linux-x64\",\"osx-x64-tar\",\"win-x64-zip\",\"win-x86-zip\", \"linux-arm64\"]},\n{\"version\":\"v9.27.6\",\"npm\":\"5.6.17\",\"lts\": false,\"files\":[\"linux-x64\",\"osx-x64-tar\",\"win-x64-zip\",\"win-x86-zip\", \"linux-arm64\"]},\n{\"version\":\"v8.9.10\",\"npm\":\"5.6.7\",\"lts\": false,\"files\":[\"linux-x64\",\"osx-x64-tar\",\"win-x64-zip\",\"win-x86-zip\", \"linux-arm64\"]},\n{\"version\":\"v6.19.62\",\"npm\":\"3.10.1066\",\"lts\": false,\"files\":[\"linux-x64\",\"osx-x64-tar\",\"win-x64-zip\",\"win-x86-zip\", \"linux-arm64\"]}\n]\n\"#;\n\ncfg_if::cfg_if! {\n    if #[cfg(target_os = \"macos\")] {\n        const NODE_VERSION_FIXTURES: [DistroMetadata; 4] = [\n            DistroMetadata {\n                version: \"10.99.1040\",\n                compressed_size: 273,\n                uncompressed_size: Some(0x0028_0000),\n            },\n            DistroMetadata {\n                version: \"9.27.6\",\n                compressed_size: 272,\n                uncompressed_size: Some(0x0028_0000),\n            },\n            DistroMetadata {\n                version: \"8.9.10\",\n                compressed_size: 272,\n                uncompressed_size: Some(0x0028_0000),\n            },\n            DistroMetadata {\n                version: \"6.19.62\",\n                compressed_size: 273,\n                uncompressed_size: Some(0x0028_0000),\n            },\n        ];\n    } else if #[cfg(target_os = \"linux\")] {\n        const NODE_VERSION_FIXTURES: [DistroMetadata; 4] = [\n            DistroMetadata {\n                version: \"10.99.1040\",\n                compressed_size: 273,\n                uncompressed_size: Some(0x0028_0000),\n            },\n            DistroMetadata {\n                version: \"9.27.6\",\n                compressed_size: 272,\n                uncompressed_size: Some(0x0028_0000),\n            },\n            DistroMetadata {\n                version: \"8.9.10\",\n                compressed_size: 270,\n                uncompressed_size: Some(0x0028_0000),\n            },\n            DistroMetadata {\n                version: \"6.19.62\",\n                compressed_size: 273,\n                uncompressed_size: Some(0x0028_0000),\n            },\n        ];\n    } else if #[cfg(target_os = \"windows\")] {\n        const NODE_VERSION_FIXTURES: [DistroMetadata; 4] = [\n            DistroMetadata {\n                version: \"10.99.1040\",\n                compressed_size: 1096,\n                uncompressed_size: None,\n            },\n            DistroMetadata {\n                version: \"9.27.6\",\n                compressed_size: 1068,\n                uncompressed_size: None,\n            },\n            DistroMetadata {\n                version: \"8.9.10\",\n                compressed_size: 1055,\n                uncompressed_size: None,\n            },\n            DistroMetadata {\n                version: \"6.19.62\",\n                compressed_size: 1056,\n                uncompressed_size: None,\n            },\n        ];\n    } else {\n        compile_error!(\"Unsupported target_os for tests (expected 'macos', 'linux', or 'windows').\");\n    }\n}\n\nconst YARN_1_VERSION_INFO: &str = r#\"[\n{\"tag_name\":\"v1.2.42\",\"assets\":[{\"name\":\"yarn-v1.2.42.tar.gz\"}]},\n{\"tag_name\":\"v1.3.1\",\"assets\":[{\"name\":\"yarn-v1.3.1.msi\"}]},\n{\"tag_name\":\"v1.4.159\",\"assets\":[{\"name\":\"yarn-v1.4.159.tar.gz\"}]},\n{\"tag_name\":\"v1.7.71\",\"assets\":[{\"name\":\"yarn-v1.7.71.tar.gz\"}]},\n{\"tag_name\":\"v1.12.99\",\"assets\":[{\"name\":\"yarn-v1.12.99.tar.gz\"}]}\n]\"#;\n\nconst YARN_1_VERSION_FIXTURES: [DistroMetadata; 4] = [\n    DistroMetadata {\n        version: \"1.12.99\",\n        compressed_size: 178,\n        uncompressed_size: Some(0x0028_0000),\n    },\n    DistroMetadata {\n        version: \"1.7.71\",\n        compressed_size: 176,\n        uncompressed_size: Some(0x0028_0000),\n    },\n    DistroMetadata {\n        version: \"1.4.159\",\n        compressed_size: 177,\n        uncompressed_size: Some(0x0028_0000),\n    },\n    DistroMetadata {\n        version: \"1.2.42\",\n        compressed_size: 174,\n        uncompressed_size: Some(0x0028_0000),\n    },\n];\n\nconst YARN_BERRY_VERSION_INFO: &str = r#\"{\n    \"name\":\"@yarnpkg/cli-dist\",\n    \"dist-tags\": { \"latest\":\"3.12.99\" },\n    \"versions\": {\n        \"2.4.159\": { \"version\":\"2.4.159\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }},\n        \"3.2.42\": { \"version\":\"3.2.42\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }},\n        \"3.7.71\": { \"version\":\"3.7.71\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }},\n        \"3.12.99\": { \"version\":\"3.12.99\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }}\n    }\n}\"#;\n\nconst YARN_BERRY_VERSION_FIXTURES: [DistroMetadata; 4] = [\n    DistroMetadata {\n        version: \"2.4.159\",\n        compressed_size: 177,\n        uncompressed_size: Some(0x0028_0000),\n    },\n    DistroMetadata {\n        version: \"3.12.99\",\n        compressed_size: 178,\n        uncompressed_size: Some(0x0028_0000),\n    },\n    DistroMetadata {\n        version: \"3.7.71\",\n        compressed_size: 176,\n        uncompressed_size: Some(0x0028_0000),\n    },\n    DistroMetadata {\n        version: \"3.2.42\",\n        compressed_size: 174,\n        uncompressed_size: Some(0x0028_0000),\n    },\n];\n\nconst PNPM_VERSION_INFO: &str = r#\"\n{\n    \"name\":\"pnpm\",\n    \"dist-tags\": { \"latest\":\"7.7.1\" },\n    \"versions\": {\n        \"0.0.1\": { \"version\":\"0.0.1\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }},\n        \"6.34.0\": { \"version\":\"6.34.0\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }},\n        \"7.7.1\": { \"version\":\"7.7.1\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }}\n    }\n}\n\"#;\n\nconst PNPM_VERSION_FIXTURES: [DistroMetadata; 3] = [\n    DistroMetadata {\n        version: \"0.0.1\",\n        compressed_size: 10,\n        uncompressed_size: Some(0x0028_0000),\n    },\n    DistroMetadata {\n        version: \"6.34.0\",\n        compressed_size: 500,\n        uncompressed_size: Some(0x0028_0000),\n    },\n    DistroMetadata {\n        version: \"7.7.1\",\n        compressed_size: 518,\n        uncompressed_size: Some(0x0028_0000),\n    },\n];\n\nconst NPM_VERSION_INFO: &str = r#\"\n{\n    \"name\":\"npm\",\n    \"dist-tags\": { \"latest\":\"8.1.5\" },\n    \"versions\": {\n        \"1.2.3\": { \"version\":\"1.2.3\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }},\n        \"4.5.6\": { \"version\":\"4.5.6\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }},\n        \"8.1.5\": { \"version\":\"8.1.5\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }}\n    }\n}\n\"#;\n\nconst NPM_VERSION_FIXTURES: [DistroMetadata; 3] = [\n    DistroMetadata {\n        version: \"1.2.3\",\n        compressed_size: 239,\n        uncompressed_size: Some(0x0028_0000),\n    },\n    DistroMetadata {\n        version: \"4.5.6\",\n        compressed_size: 239,\n        uncompressed_size: Some(0x0028_0000),\n    },\n    DistroMetadata {\n        version: \"8.1.5\",\n        compressed_size: 239,\n        uncompressed_size: Some(0x0028_0000),\n    },\n];\n\n#[test]\nfn install_node_informs_newer_npm() {\n    let s = sandbox()\n        .platform(&platform_with_node_npm(\"8.9.10\", \"5.6.17\"))\n        .node_available_versions(NODE_VERSION_INFO)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .env(\"VOLTA_LOGLEVEL\", \"info\")\n        .build();\n\n    assert_that!(\n        s.volta(\"install node@10.99.1040\"),\n        execs()\n            .with_status(ExitCode::Success as i32)\n            .with_stdout_contains(\"[..]this version of Node includes npm@6.2.26, which is higher than your default version (5.6.17).\")\n            .with_stdout_contains(\"[..]`volta install npm@bundled`[..]\")\n    );\n}\n\n#[test]\nfn install_node_with_npm_hides_bundled_version() {\n    let s = sandbox()\n        .platform(&platform_with_node_npm(\"8.9.10\", \"6.2.26\"))\n        .node_available_versions(NODE_VERSION_INFO)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .env(\"VOLTA_LOGLEVEL\", \"info\")\n        .build();\n\n    assert_that!(\n        s.volta(\"install node@9.27.6\"),\n        execs()\n            .with_status(ExitCode::Success as i32)\n            .with_stdout_does_not_contain(\"[..](with npm@5.6.17)[..]\")\n    );\n}\n\n#[test]\nfn install_npm_bundled_clears_npm() {\n    let s = sandbox()\n        .platform(&platform_with_node_npm(\"8.9.10\", \"6.2.26\"))\n        .node_npm_version_file(\"8.9.10\", \"5.6.7\")\n        .build();\n\n    assert_that!(\n        s.volta(\"install npm@bundled\"),\n        execs().with_status(ExitCode::Success as i32)\n    );\n\n    assert_eq!(\n        Sandbox::read_default_platform(),\n        platform_with_node(\"8.9.10\")\n    );\n}\n\n#[test]\nfn install_npm_bundled_reports_info() {\n    let s = sandbox()\n        .platform(&platform_with_node_npm(\"8.9.10\", \"6.2.26\"))\n        .node_npm_version_file(\"8.9.10\", \"5.6.7\")\n        .env(\"VOLTA_LOGLEVEL\", \"info\")\n        .build();\n\n    assert_that!(\n        s.volta(\"install npm@bundled\"),\n        execs()\n            .with_status(ExitCode::Success as i32)\n            .with_stdout_contains(\"[..]set bundled npm (currently 5.6.7)[..]\")\n    );\n}\n\n#[test]\nfn install_npm_without_node_errors() {\n    let s = sandbox()\n        .npm_available_versions(NPM_VERSION_INFO)\n        .distro_mocks::<NpmFixture>(&NPM_VERSION_FIXTURES)\n        .build();\n\n    assert_that!(\n        s.volta(\"install npm@4.5.6\"),\n        execs()\n            .with_status(ExitCode::ConfigurationError as i32)\n            .with_stderr_contains(\n                \"[..]Cannot install npm because the default Node version is not set.\"\n            )\n    );\n}\n\n#[test]\nfn install_pnpm_without_node_errors() {\n    let s = sandbox()\n        .pnpm_available_versions(PNPM_VERSION_INFO)\n        .distro_mocks::<PnpmFixture>(&PNPM_VERSION_FIXTURES)\n        .env(\"VOLTA_FEATURE_PNPM\", \"1\")\n        .build();\n\n    assert_that!(\n        s.volta(\"install pnpm@7.7.1\"),\n        execs()\n            .with_status(ExitCode::ConfigurationError as i32)\n            .with_stderr_contains(\n                \"[..]Cannot install pnpm because the default Node version is not set.\"\n            )\n    );\n}\n\n#[test]\nfn install_yarn_without_node_errors() {\n    let s = sandbox()\n        .yarn_1_available_versions(YARN_1_VERSION_INFO)\n        .distro_mocks::<Yarn1Fixture>(&YARN_1_VERSION_FIXTURES)\n        .build();\n\n    assert_that!(\n        s.volta(\"install yarn@1.2.42\"),\n        execs()\n            .with_status(ExitCode::ConfigurationError as i32)\n            .with_stderr_contains(\n                \"[..]Cannot install Yarn because the default Node version is not set.\"\n            )\n    );\n}\n\n#[test]\nfn install_yarn_3_without_node_errors() {\n    let s = sandbox()\n        .yarn_1_available_versions(YARN_1_VERSION_INFO)\n        .yarn_berry_available_versions(YARN_BERRY_VERSION_INFO)\n        .distro_mocks::<Yarn1Fixture>(&YARN_1_VERSION_FIXTURES)\n        .distro_mocks::<YarnBerryFixture>(&YARN_BERRY_VERSION_FIXTURES)\n        .build();\n\n    assert_that!(\n        s.volta(\"install yarn@3.2.42\"),\n        execs()\n            .with_status(ExitCode::ConfigurationError as i32)\n            .with_stderr_contains(\n                \"[..]Cannot install Yarn because the default Node version is not set.\"\n            )\n    );\n}\n\n#[test]\nfn install_node_with_shadowed_binary() {\n    #[cfg(windows)]\n    const SCRIPT_FILENAME: &str = \"node.bat\";\n    #[cfg(not(windows))]\n    const SCRIPT_FILENAME: &str = \"node\";\n\n    let s = sandbox()\n        .node_available_versions(NODE_VERSION_INFO)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .env(\"VOLTA_LOGLEVEL\", \"info\")\n        .prepend_exec_dir_to_path()\n        .executable_file(SCRIPT_FILENAME, \"echo hello world\")\n        .build();\n\n    assert_that!(\n        s.volta(\"install node\"),\n        execs()\n            .with_status(ExitCode::Success as i32)\n            .with_stdout_contains(\"[..]is shadowed by another binary of the same name at [..]\")\n    );\n}\n"
  },
  {
    "path": "tests/acceptance/volta_pin.rs",
    "content": "use crate::support::sandbox::{\n    sandbox, DistroMetadata, NodeFixture, NpmFixture, PnpmFixture, Yarn1Fixture, YarnBerryFixture,\n};\nuse hamcrest2::assert_that;\nuse hamcrest2::prelude::*;\nuse test_support::matchers::execs;\n\nuse volta_core::error::ExitCode;\n\nconst BASIC_PACKAGE_JSON: &str = r#\"{\n  \"name\": \"test-package\"\n}\"#;\nconst PACKAGE_JSON_WITH_EMPTY_LINE: &str = r#\"{\n  \"name\": \"test-package\"\n}\n\"#;\nconst PACKAGE_JSON_WITH_EXTENDS: &str = r#\"{\n  \"name\": \"test-package\",\n  \"volta\": {\n    \"node\": \"8.9.10\",\n    \"extends\": \"./basic.json\"\n  }\n}\"#;\n\nfn package_json_with_pinned_node(node: &str) -> String {\n    format!(\n        r#\"{{\n  \"name\": \"test-package\",\n  \"volta\": {{\n    \"node\": \"{}\"\n  }}\n}}\"#,\n        node\n    )\n}\n\nfn package_json_with_pinned_node_npm(node: &str, npm: &str) -> String {\n    format!(\n        r#\"{{\n  \"name\": \"test-package\",\n  \"volta\": {{\n    \"node\": \"{}\",\n    \"npm\": \"{}\"\n  }}\n}}\"#,\n        node, npm\n    )\n}\n\nfn package_json_with_pinned_node_pnpm(node_version: &str, pnpm_version: &str) -> String {\n    format!(\n        r#\"{{\n  \"name\": \"test-package\",\n  \"volta\": {{\n    \"node\": \"{}\",\n    \"pnpm\": \"{}\"\n  }}\n}}\"#,\n        node_version, pnpm_version\n    )\n}\n\nfn package_json_with_pinned_node_npm_pnpm(\n    node_version: &str,\n    npm_version: &str,\n    pnpm_version: &str,\n) -> String {\n    format!(\n        r#\"{{\n  \"name\": \"test-package\",\n  \"volta\": {{\n    \"node\": \"{}\",\n    \"npm\": \"{}\",\n    \"pnpm\": \"{}\"\n  }}\n}}\"#,\n        node_version, npm_version, pnpm_version\n    )\n}\n\nfn package_json_with_pinned_node_yarn(node_version: &str, yarn_version: &str) -> String {\n    format!(\n        r#\"{{\n  \"name\": \"test-package\",\n  \"volta\": {{\n    \"node\": \"{}\",\n    \"yarn\": \"{}\"\n  }}\n}}\"#,\n        node_version, yarn_version\n    )\n}\n\nfn package_json_with_pinned_node_npm_yarn(\n    node_version: &str,\n    npm_version: &str,\n    yarn_version: &str,\n) -> String {\n    format!(\n        r#\"{{\n  \"name\": \"test-package\",\n  \"volta\": {{\n    \"node\": \"{}\",\n    \"npm\": \"{}\",\n    \"yarn\": \"{}\"\n  }}\n}}\"#,\n        node_version, npm_version, yarn_version\n    )\n}\n\nconst NODE_VERSION_INFO: &str = r#\"[\n{\"version\":\"v10.99.1040\",\"npm\":\"6.2.26\",\"lts\": \"Dubnium\",\"files\":[\"linux-x64\",\"osx-x64-tar\",\"win-x64-zip\",\"win-x86-zip\", \"linux-arm64\"]},\n{\"version\":\"v9.27.6\",\"npm\":\"5.6.17\",\"lts\": false,\"files\":[\"linux-x64\",\"osx-x64-tar\",\"win-x64-zip\",\"win-x86-zip\", \"linux-arm64\"]},\n{\"version\":\"v8.9.10\",\"npm\":\"5.6.7\",\"lts\": false,\"files\":[\"linux-x64\",\"osx-x64-tar\",\"win-x64-zip\",\"win-x86-zip\", \"linux-arm64\"]},\n{\"version\":\"v6.19.62\",\"npm\":\"3.10.1066\",\"lts\": false,\"files\":[\"linux-x64\",\"osx-x64-tar\",\"win-x64-zip\",\"win-x86-zip\", \"linux-arm64\"]}\n]\n\"#;\n\ncfg_if::cfg_if! {\n    if #[cfg(target_os = \"macos\")] {\n        const NODE_VERSION_FIXTURES: [DistroMetadata; 4] = [\n            DistroMetadata {\n                version: \"10.99.1040\",\n                compressed_size: 273,\n                uncompressed_size: Some(0x0028_0000),\n            },\n            DistroMetadata {\n                version: \"9.27.6\",\n                compressed_size: 272,\n                uncompressed_size: Some(0x0028_0000),\n            },\n            DistroMetadata {\n                version: \"8.9.10\",\n                compressed_size: 272,\n                uncompressed_size: Some(0x0028_0000),\n            },\n            DistroMetadata {\n                version: \"6.19.62\",\n                compressed_size: 273,\n                uncompressed_size: Some(0x0028_0000),\n            },\n        ];\n    } else if #[cfg(target_os = \"linux\")] {\n        const NODE_VERSION_FIXTURES: [DistroMetadata; 4] = [\n            DistroMetadata {\n                version: \"10.99.1040\",\n                compressed_size: 273,\n                uncompressed_size: Some(0x0028_0000),\n            },\n            DistroMetadata {\n                version: \"9.27.6\",\n                compressed_size: 272,\n                uncompressed_size: Some(0x0028_0000),\n            },\n            DistroMetadata {\n                version: \"8.9.10\",\n                compressed_size: 270,\n                uncompressed_size: Some(0x0028_0000),\n            },\n            DistroMetadata {\n                version: \"6.19.62\",\n                compressed_size: 273,\n                uncompressed_size: Some(0x0028_0000),\n            },\n        ];\n    } else if #[cfg(target_os = \"windows\")] {\n        const NODE_VERSION_FIXTURES: [DistroMetadata; 4] = [\n            DistroMetadata {\n                version: \"10.99.1040\",\n                compressed_size: 1096,\n                uncompressed_size: None,\n            },\n            DistroMetadata {\n                version: \"9.27.6\",\n                compressed_size: 1068,\n                uncompressed_size: None,\n            },\n            DistroMetadata {\n                version: \"8.9.10\",\n                compressed_size: 1055,\n                uncompressed_size: None,\n            },\n            DistroMetadata {\n                version: \"6.19.62\",\n                compressed_size: 1056,\n                uncompressed_size: None,\n            },\n        ];\n    } else {\n        compile_error!(\"Unsupported target_os for tests (expected 'macos', 'linux', or 'windows').\");\n    }\n}\n\nconst YARN_1_VERSION_INFO: &str = r#\"{\n    \"name\":\"yarn\",\n    \"dist-tags\": { \"latest\":\"1.12.99\" },\n    \"versions\": {\n        \"1.2.42\": { \"version\":\"1.2.42\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }},\n        \"1.4.159\": { \"version\":\"1.4.159\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }},\n        \"1.7.71\": { \"version\":\"1.7.71\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }},\n        \"1.12.99\": { \"version\":\"1.12.99\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }}\n    }\n}\"#;\n\nconst YARN_BERRY_VERSION_INFO: &str = r#\"{\n    \"name\":\"@yarnpkg/cli-dist\",\n    \"dist-tags\": { \"latest\":\"3.12.99\" },\n    \"versions\": {\n        \"2.4.159\": { \"version\":\"2.4.159\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }},\n        \"3.2.42\": { \"version\":\"3.2.42\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }},\n        \"3.7.71\": { \"version\":\"3.7.71\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }},\n        \"3.12.99\": { \"version\":\"3.12.99\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }}\n    }\n}\"#;\n\nconst YARN_1_VERSION_FIXTURES: [DistroMetadata; 4] = [\n    DistroMetadata {\n        version: \"1.12.99\",\n        compressed_size: 178,\n        uncompressed_size: Some(0x0028_0000),\n    },\n    DistroMetadata {\n        version: \"1.7.71\",\n        compressed_size: 176,\n        uncompressed_size: Some(0x0028_0000),\n    },\n    DistroMetadata {\n        version: \"1.4.159\",\n        compressed_size: 177,\n        uncompressed_size: Some(0x0028_0000),\n    },\n    DistroMetadata {\n        version: \"1.2.42\",\n        compressed_size: 174,\n        uncompressed_size: Some(0x0028_0000),\n    },\n];\n\nconst YARN_BERRY_VERSION_FIXTURES: [DistroMetadata; 4] = [\n    DistroMetadata {\n        version: \"2.4.159\",\n        compressed_size: 177,\n        uncompressed_size: Some(0x0028_0000),\n    },\n    DistroMetadata {\n        version: \"3.12.99\",\n        compressed_size: 178,\n        uncompressed_size: Some(0x0028_0000),\n    },\n    DistroMetadata {\n        version: \"3.7.71\",\n        compressed_size: 176,\n        uncompressed_size: Some(0x0028_0000),\n    },\n    DistroMetadata {\n        version: \"3.2.42\",\n        compressed_size: 174,\n        uncompressed_size: Some(0x0028_0000),\n    },\n];\n\nconst PNPM_VERSION_INFO: &str = r#\"\n{\n    \"name\":\"pnpm\",\n    \"dist-tags\": { \"latest\":\"7.7.1\" },\n    \"versions\": {\n        \"0.0.1\": { \"version\":\"0.0.1\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }},\n        \"6.34.0\": { \"version\":\"6.34.0\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }},\n        \"7.7.1\": { \"version\":\"7.7.1\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }}\n    }\n}\n\"#;\n\nconst PNPM_VERSION_FIXTURES: [DistroMetadata; 3] = [\n    DistroMetadata {\n        version: \"0.0.1\",\n        compressed_size: 10,\n        uncompressed_size: Some(0x0028_0000),\n    },\n    DistroMetadata {\n        version: \"6.34.0\",\n        compressed_size: 500,\n        uncompressed_size: Some(0x0028_0000),\n    },\n    DistroMetadata {\n        version: \"7.7.1\",\n        compressed_size: 518,\n        uncompressed_size: Some(0x0028_0000),\n    },\n];\n\nconst NPM_VERSION_FIXTURES: [DistroMetadata; 3] = [\n    DistroMetadata {\n        version: \"1.2.3\",\n        compressed_size: 239,\n        uncompressed_size: Some(0x0028_0000),\n    },\n    DistroMetadata {\n        version: \"4.5.6\",\n        compressed_size: 239,\n        uncompressed_size: Some(0x0028_0000),\n    },\n    DistroMetadata {\n        version: \"8.1.5\",\n        compressed_size: 239,\n        uncompressed_size: Some(0x0028_0000),\n    },\n];\n\nconst NPM_VERSION_INFO: &str = r#\"\n{\n    \"name\":\"npm\",\n    \"dist-tags\": { \"latest\":\"8.1.5\" },\n    \"versions\": {\n        \"1.2.3\": { \"version\":\"1.2.3\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }},\n        \"4.5.6\": { \"version\":\"4.5.6\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }},\n        \"8.1.5\": { \"version\":\"8.1.5\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }}\n    }\n}\n\"#;\n\nconst VOLTA_LOGLEVEL: &str = \"VOLTA_LOGLEVEL\";\n\n#[test]\nfn pin_node() {\n    let s = sandbox()\n        .package_json(BASIC_PACKAGE_JSON)\n        .node_available_versions(NODE_VERSION_INFO)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .build();\n\n    assert_that!(\n        s.volta(\"pin node@6\"),\n        execs().with_status(ExitCode::Success as i32)\n    );\n\n    assert_eq!(\n        s.read_package_json(),\n        package_json_with_pinned_node(\"6.19.62\"),\n    )\n}\n\n#[test]\nfn pin_node_reports_info() {\n    let s = sandbox()\n        .package_json(BASIC_PACKAGE_JSON)\n        .node_available_versions(NODE_VERSION_INFO)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .env(VOLTA_LOGLEVEL, \"info\")\n        .build();\n\n    assert_that!(\n        s.volta(\"pin node@6\"),\n        execs()\n            .with_status(ExitCode::Success as i32)\n            .with_stdout_contains(\"[..]pinned node@6.19.62 (with npm@3.10.1066) in package.json\")\n    );\n}\n\n#[test]\nfn pin_node_latest() {\n    let s = sandbox()\n        .package_json(BASIC_PACKAGE_JSON)\n        .node_available_versions(NODE_VERSION_INFO)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .build();\n\n    assert_that!(\n        s.volta(\"pin node@latest\"),\n        execs().with_status(ExitCode::Success as i32)\n    );\n\n    assert_eq!(\n        s.read_package_json(),\n        package_json_with_pinned_node(\"10.99.1040\"),\n    )\n}\n\n#[test]\nfn pin_node_no_version() {\n    let s = sandbox()\n        .package_json(BASIC_PACKAGE_JSON)\n        .node_available_versions(NODE_VERSION_INFO)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .build();\n\n    assert_that!(\n        s.volta(\"pin node\"),\n        execs().with_status(ExitCode::Success as i32)\n    );\n\n    assert_eq!(\n        s.read_package_json(),\n        package_json_with_pinned_node(\"10.99.1040\"),\n    )\n}\n\n#[test]\nfn pin_node_informs_newer_npm() {\n    let s = sandbox()\n        .package_json(&package_json_with_pinned_node_npm(\"8.9.10\", \"5.6.17\"))\n        .node_available_versions(NODE_VERSION_INFO)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .env(\"VOLTA_LOGLEVEL\", \"info\")\n        .build();\n\n    assert_that!(\n        s.volta(\"pin node@10.99.1040\"),\n        execs()\n            .with_status(ExitCode::Success as i32)\n            .with_stdout_contains(\"[..]this version of Node includes npm@6.2.26, which is higher than your pinned version (5.6.17).\")\n            .with_stdout_contains(\"[..]`volta pin npm@bundled`[..]\")\n    );\n}\n\n#[test]\nfn pin_node_with_npm_hides_bundled_version() {\n    let s = sandbox()\n        .package_json(&package_json_with_pinned_node_npm(\"8.9.10\", \"6.2.26\"))\n        .node_available_versions(NODE_VERSION_INFO)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .env(\"VOLTA_LOGLEVEL\", \"info\")\n        .build();\n\n    assert_that!(\n        s.volta(\"pin node@9.27.6\"),\n        execs()\n            .with_status(ExitCode::Success as i32)\n            .with_stdout_does_not_contain(\"[..](with npm@5.6.17)[..]\")\n    );\n}\n\n#[test]\nfn pin_yarn_no_node() {\n    let s = sandbox()\n        .package_json(BASIC_PACKAGE_JSON)\n        .yarn_1_available_versions(YARN_1_VERSION_INFO)\n        .yarn_berry_available_versions(YARN_BERRY_VERSION_INFO)\n        .distro_mocks::<Yarn1Fixture>(&YARN_1_VERSION_FIXTURES)\n        .build();\n\n    assert_that!(\n        s.volta(\"pin yarn@1.4\"),\n        execs()\n            .with_status(ExitCode::ConfigurationError as i32)\n            .with_stderr_contains(\n                \"[..]Cannot pin Yarn because the Node version is not pinned in this project.\"\n            )\n    );\n\n    assert_eq!(s.read_package_json(), BASIC_PACKAGE_JSON)\n}\n\n#[test]\nfn pin_yarn_1() {\n    let s = sandbox()\n        .package_json(&package_json_with_pinned_node(\"1.2.3\"))\n        .yarn_1_available_versions(YARN_1_VERSION_INFO)\n        .yarn_berry_available_versions(YARN_BERRY_VERSION_INFO)\n        .distro_mocks::<Yarn1Fixture>(&YARN_1_VERSION_FIXTURES)\n        .build();\n\n    assert_that!(\n        s.volta(\"pin yarn@1.4\"),\n        execs().with_status(ExitCode::Success as i32)\n    );\n\n    assert_eq!(\n        s.read_package_json(),\n        package_json_with_pinned_node_yarn(\"1.2.3\", \"1.4.159\"),\n    )\n}\n\n#[test]\nfn pin_yarn_2_is_error() {\n    let s = sandbox()\n        .package_json(&package_json_with_pinned_node(\"1.2.3\"))\n        .yarn_1_available_versions(YARN_1_VERSION_INFO)\n        .yarn_berry_available_versions(YARN_BERRY_VERSION_INFO)\n        .distro_mocks::<Yarn1Fixture>(&YARN_1_VERSION_FIXTURES)\n        .distro_mocks::<YarnBerryFixture>(&YARN_BERRY_VERSION_FIXTURES)\n        .build();\n\n    assert_that!(\n        s.volta(\"pin yarn@2\"),\n        execs()\n            .with_status(ExitCode::NoVersionMatch as i32)\n            .with_stderr_contains(\n                \"[..]Yarn version 2 is not recommended for use, and not supported by Volta[..]\"\n            )\n    );\n\n    assert_eq!(\n        s.read_package_json(),\n        package_json_with_pinned_node(\"1.2.3\"),\n    )\n}\n\n#[test]\nfn pin_yarn_3() {\n    let s = sandbox()\n        .package_json(&package_json_with_pinned_node(\"1.2.3\"))\n        .yarn_1_available_versions(YARN_1_VERSION_INFO)\n        .yarn_berry_available_versions(YARN_BERRY_VERSION_INFO)\n        .distro_mocks::<Yarn1Fixture>(&YARN_1_VERSION_FIXTURES)\n        .distro_mocks::<YarnBerryFixture>(&YARN_BERRY_VERSION_FIXTURES)\n        .build();\n\n    assert_that!(\n        s.volta(\"pin yarn@3\"),\n        execs().with_status(ExitCode::Success as i32)\n    );\n\n    assert_eq!(\n        s.read_package_json(),\n        package_json_with_pinned_node_yarn(\"1.2.3\", \"3.12.99\"),\n    )\n}\n\n#[test]\nfn pin_yarn_reports_info() {\n    let s = sandbox()\n        .package_json(&package_json_with_pinned_node(\"1.2.3\"))\n        .yarn_1_available_versions(YARN_1_VERSION_INFO)\n        .yarn_berry_available_versions(YARN_BERRY_VERSION_INFO)\n        .distro_mocks::<Yarn1Fixture>(&YARN_1_VERSION_FIXTURES)\n        .env(VOLTA_LOGLEVEL, \"info\")\n        .build();\n\n    assert_that!(\n        s.volta(\"pin yarn@1.4\"),\n        execs()\n            .with_status(ExitCode::Success as i32)\n            .with_stdout_contains(\"[..]pinned yarn@1.4.159 in package.json\")\n    );\n}\n\n#[test]\nfn pin_yarn_latest() {\n    let s = sandbox()\n        .package_json(&package_json_with_pinned_node(\"1.2.3\"))\n        .yarn_1_available_versions(YARN_1_VERSION_INFO)\n        .yarn_berry_available_versions(YARN_BERRY_VERSION_INFO)\n        .distro_mocks::<Yarn1Fixture>(&YARN_1_VERSION_FIXTURES)\n        .distro_mocks::<YarnBerryFixture>(&YARN_BERRY_VERSION_FIXTURES)\n        .build();\n\n    assert_that!(\n        s.volta(\"pin yarn@latest\"),\n        execs().with_status(ExitCode::Success as i32)\n    );\n\n    assert_eq!(\n        s.read_package_json(),\n        package_json_with_pinned_node_yarn(\"1.2.3\", \"3.12.99\"),\n    )\n}\n\n#[test]\nfn pin_yarn_1_no_version() {\n    let s = sandbox()\n        .package_json(&package_json_with_pinned_node(\"1.2.3\"))\n        .yarn_1_available_versions(YARN_1_VERSION_INFO)\n        .yarn_berry_available_versions(YARN_BERRY_VERSION_INFO)\n        .distro_mocks::<Yarn1Fixture>(&YARN_1_VERSION_FIXTURES)\n        .distro_mocks::<YarnBerryFixture>(&YARN_BERRY_VERSION_FIXTURES)\n        .build();\n\n    assert_that!(\n        s.volta(\"pin yarn@1\"),\n        execs().with_status(ExitCode::Success as i32)\n    );\n\n    assert_eq!(\n        s.read_package_json(),\n        package_json_with_pinned_node_yarn(\"1.2.3\", \"1.12.99\"),\n    )\n}\n\n#[test]\nfn pin_yarn_3_no_version() {\n    let s = sandbox()\n        .package_json(&package_json_with_pinned_node(\"1.2.3\"))\n        .yarn_1_available_versions(YARN_1_VERSION_INFO)\n        .yarn_berry_available_versions(YARN_BERRY_VERSION_INFO)\n        .distro_mocks::<Yarn1Fixture>(&YARN_1_VERSION_FIXTURES)\n        .distro_mocks::<YarnBerryFixture>(&YARN_BERRY_VERSION_FIXTURES)\n        .build();\n\n    assert_that!(\n        s.volta(\"pin yarn@3\"),\n        execs().with_status(ExitCode::Success as i32)\n    );\n\n    assert_eq!(\n        s.read_package_json(),\n        package_json_with_pinned_node_yarn(\"1.2.3\", \"3.12.99\"),\n    )\n}\n\n#[test]\nfn pin_yarn_no_version() {\n    let s = sandbox()\n        .package_json(&package_json_with_pinned_node(\"1.2.3\"))\n        .yarn_1_available_versions(YARN_1_VERSION_INFO)\n        .yarn_berry_available_versions(YARN_BERRY_VERSION_INFO)\n        .distro_mocks::<Yarn1Fixture>(&YARN_1_VERSION_FIXTURES)\n        .distro_mocks::<YarnBerryFixture>(&YARN_BERRY_VERSION_FIXTURES)\n        .build();\n\n    assert_that!(\n        s.volta(\"pin yarn\"),\n        execs().with_status(ExitCode::Success as i32)\n    );\n\n    assert_eq!(\n        s.read_package_json(),\n        package_json_with_pinned_node_yarn(\"1.2.3\", \"3.12.99\"),\n    )\n}\n\n#[test]\nfn pin_yarn_1_missing_release() {\n    let s = sandbox()\n        .package_json(&package_json_with_pinned_node(\"1.2.3\"))\n        .mock_not_found()\n        .build();\n\n    assert_that!(\n        s.volta(\"pin yarn@1.3.1\"),\n        execs()\n            .with_status(ExitCode::NetworkError as i32)\n            .with_stderr_contains(\"[..]Could not download yarn@1.3.1\")\n    );\n\n    assert_eq!(\n        s.read_package_json(),\n        package_json_with_pinned_node(\"1.2.3\"),\n    )\n}\n\n#[test]\nfn pin_yarn_1_missing_release_v2() {\n    let s = sandbox()\n        .package_json(&package_json_with_pinned_node(\"1.2.3\"))\n        .mock_not_found()\n        .build();\n\n    assert_that!(\n        s.volta(\"pin yarn@1\"),\n        execs()\n            .with_status(ExitCode::NetworkError as i32)\n            .with_stderr_contains(\"[..]Could not download Yarn version registry\")\n    );\n\n    assert_eq!(\n        s.read_package_json(),\n        package_json_with_pinned_node(\"1.2.3\"),\n    )\n}\n\n#[test]\nfn pin_yarn_3_missing_release() {\n    let s = sandbox()\n        .package_json(&package_json_with_pinned_node(\"1.2.3\"))\n        .mock_not_found()\n        .build();\n\n    assert_that!(\n        s.volta(\"pin yarn@3.3.1\"),\n        execs()\n            .with_status(ExitCode::NetworkError as i32)\n            .with_stderr_contains(\"[..]Could not download yarn@3.3.1\")\n    );\n\n    assert_eq!(\n        s.read_package_json(),\n        package_json_with_pinned_node(\"1.2.3\"),\n    )\n}\n\n#[test]\nfn pin_yarn_3_missing_release_v2() {\n    let s = sandbox()\n        .package_json(&package_json_with_pinned_node(\"1.2.3\"))\n        .mock_not_found()\n        .build();\n\n    assert_that!(\n        s.volta(\"pin yarn@3\"),\n        execs()\n            .with_status(ExitCode::NetworkError as i32)\n            .with_stderr_contains(\"[..]Could not download Yarn version registry\")\n    );\n\n    assert_eq!(\n        s.read_package_json(),\n        package_json_with_pinned_node(\"1.2.3\"),\n    )\n}\n\n#[test]\nfn pin_yarn_leaves_npm() {\n    let s = sandbox()\n        .package_json(&package_json_with_pinned_node_npm(\"1.2.3\", \"3.4.5\"))\n        .yarn_1_available_versions(YARN_1_VERSION_INFO)\n        .yarn_berry_available_versions(YARN_BERRY_VERSION_INFO)\n        .distro_mocks::<Yarn1Fixture>(&YARN_1_VERSION_FIXTURES)\n        .build();\n\n    assert_that!(\n        s.volta(\"pin yarn@1.4\"),\n        execs().with_status(ExitCode::Success as i32)\n    );\n\n    assert_eq!(\n        s.read_package_json(),\n        package_json_with_pinned_node_npm_yarn(\"1.2.3\", \"3.4.5\", \"1.4.159\"),\n    )\n}\n\n#[test]\nfn pin_npm_no_node() {\n    let s = sandbox()\n        .package_json(BASIC_PACKAGE_JSON)\n        .npm_available_versions(NPM_VERSION_INFO)\n        .distro_mocks::<NpmFixture>(&NPM_VERSION_FIXTURES)\n        .build();\n\n    assert_that!(\n        s.volta(\"pin npm@1.2.3\"),\n        execs()\n            .with_status(ExitCode::ConfigurationError as i32)\n            .with_stderr_contains(\n                \"[..]Cannot pin npm because the Node version is not pinned in this project.\"\n            )\n    );\n\n    assert_eq!(s.read_package_json(), BASIC_PACKAGE_JSON)\n}\n\n#[test]\nfn pin_npm() {\n    let s = sandbox()\n        .package_json(&package_json_with_pinned_node(\"1.2.3\"))\n        .npm_available_versions(NPM_VERSION_INFO)\n        .distro_mocks::<NpmFixture>(&NPM_VERSION_FIXTURES)\n        .build();\n\n    assert_that!(\n        s.volta(\"pin npm@4.5\"),\n        execs().with_status(ExitCode::Success as i32)\n    );\n\n    assert_eq!(\n        s.read_package_json(),\n        package_json_with_pinned_node_npm(\"1.2.3\", \"4.5.6\"),\n    )\n}\n\n#[test]\nfn pin_npm_reports_info() {\n    let s = sandbox()\n        .package_json(&package_json_with_pinned_node(\"1.2.3\"))\n        .npm_available_versions(NPM_VERSION_INFO)\n        .distro_mocks::<NpmFixture>(&NPM_VERSION_FIXTURES)\n        .env(VOLTA_LOGLEVEL, \"info\")\n        .build();\n\n    assert_that!(\n        s.volta(\"pin npm@4.5\"),\n        execs()\n            .with_status(ExitCode::Success as i32)\n            .with_stdout_contains(\"[..]pinned npm@4.5.6 in package.json\")\n    );\n}\n\n#[test]\nfn pin_npm_latest() {\n    let s = sandbox()\n        .package_json(&package_json_with_pinned_node(\"1.2.3\"))\n        .npm_available_versions(NPM_VERSION_INFO)\n        .distro_mocks::<NpmFixture>(&NPM_VERSION_FIXTURES)\n        .build();\n\n    assert_that!(\n        s.volta(\"pin npm@latest\"),\n        execs().with_status(ExitCode::Success as i32)\n    );\n\n    assert_eq!(\n        s.read_package_json(),\n        package_json_with_pinned_node_npm(\"1.2.3\", \"8.1.5\"),\n    );\n}\n\n#[test]\nfn pin_npm_no_version() {\n    let s = sandbox()\n        .package_json(&package_json_with_pinned_node(\"1.2.3\"))\n        .npm_available_versions(NPM_VERSION_INFO)\n        .distro_mocks::<NpmFixture>(&NPM_VERSION_FIXTURES)\n        .build();\n\n    assert_that!(\n        s.volta(\"pin npm\"),\n        execs().with_status(ExitCode::Success as i32)\n    );\n\n    assert_eq!(\n        s.read_package_json(),\n        package_json_with_pinned_node_npm(\"1.2.3\", \"8.1.5\"),\n    )\n}\n\n#[test]\nfn pin_npm_missing_release() {\n    let s = sandbox()\n        .package_json(&package_json_with_pinned_node(\"1.2.3\"))\n        .mock_not_found()\n        .build();\n\n    assert_that!(\n        s.volta(\"pin npm@8.1.5\"),\n        execs()\n            .with_status(ExitCode::NetworkError as i32)\n            .with_stderr_contains(\"[..]Could not download npm@8.1.5\")\n    );\n\n    assert_eq!(\n        s.read_package_json(),\n        package_json_with_pinned_node(\"1.2.3\"),\n    );\n}\n\n#[test]\nfn pin_npm_bundled_removes_npm() {\n    let s = sandbox()\n        .package_json(&package_json_with_pinned_node_npm(\"1.2.3\", \"4.5.6\"))\n        .node_npm_version_file(\"1.2.3\", \"3.2.1\")\n        .build();\n\n    assert_that!(\n        s.volta(\"pin npm@bundled\"),\n        execs().with_status(ExitCode::Success as i32)\n    );\n\n    assert_eq!(\n        s.read_package_json(),\n        package_json_with_pinned_node(\"1.2.3\"),\n    );\n}\n\n#[test]\nfn pin_npm_bundled_reports_info() {\n    let s = sandbox()\n        .package_json(&package_json_with_pinned_node_npm(\"1.2.3\", \"4.5.6\"))\n        .node_npm_version_file(\"1.2.3\", \"3.2.1\")\n        .env(\"VOLTA_LOGLEVEL\", \"info\")\n        .build();\n\n    assert_that!(\n        s.volta(\"pin npm@bundled\"),\n        execs()\n            .with_status(ExitCode::Success as i32)\n            .with_stdout_contains(\"[..]set package.json to use bundled npm (currently 3.2.1)[..]\")\n    );\n}\n\n#[test]\nfn pin_node_and_yarn1() {\n    let s = sandbox()\n        .package_json(BASIC_PACKAGE_JSON)\n        .node_available_versions(NODE_VERSION_INFO)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .yarn_1_available_versions(YARN_1_VERSION_INFO)\n        .yarn_berry_available_versions(YARN_BERRY_VERSION_INFO)\n        .distro_mocks::<Yarn1Fixture>(&YARN_1_VERSION_FIXTURES)\n        .build();\n\n    assert_that!(\n        s.volta(\"pin node@6 yarn@1.4\"),\n        execs().with_status(ExitCode::Success as i32)\n    );\n\n    assert_eq!(\n        s.read_package_json(),\n        package_json_with_pinned_node_yarn(\"6.19.62\", \"1.4.159\"),\n    )\n}\n\n#[test]\nfn pin_node_and_yarn3() {\n    let s = sandbox()\n        .package_json(BASIC_PACKAGE_JSON)\n        .node_available_versions(NODE_VERSION_INFO)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .yarn_1_available_versions(YARN_1_VERSION_INFO)\n        .yarn_berry_available_versions(YARN_BERRY_VERSION_INFO)\n        .distro_mocks::<Yarn1Fixture>(&YARN_1_VERSION_FIXTURES)\n        .distro_mocks::<YarnBerryFixture>(&YARN_BERRY_VERSION_FIXTURES)\n        .build();\n\n    assert_that!(\n        s.volta(\"pin node@6 yarn@3\"),\n        execs().with_status(ExitCode::Success as i32)\n    );\n\n    assert_eq!(\n        s.read_package_json(),\n        package_json_with_pinned_node_yarn(\"6.19.62\", \"3.12.99\"),\n    )\n}\n\n#[test]\nfn pin_node_does_not_remove_trailing_newline() {\n    let s = sandbox()\n        .package_json(PACKAGE_JSON_WITH_EMPTY_LINE)\n        .node_available_versions(NODE_VERSION_INFO)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .build();\n\n    assert_that!(\n        s.volta(\"pin node@6\"),\n        execs().with_status(ExitCode::Success as i32)\n    );\n\n    assert!(s.read_package_json().ends_with('\\n'))\n}\n\n#[test]\nfn pin_node_does_not_overwrite_extends() {\n    let s = sandbox()\n        .package_json(PACKAGE_JSON_WITH_EXTENDS)\n        .node_available_versions(NODE_VERSION_INFO)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .project_file(\"basic.json\", BASIC_PACKAGE_JSON)\n        .build();\n\n    assert_that!(\n        s.volta(\"pin node@6\"),\n        execs().with_status(ExitCode::Success as i32)\n    );\n\n    assert!(s\n        .read_package_json()\n        .contains(r#\"\"extends\": \"./basic.json\"\"#));\n}\n\n#[test]\nfn pin_pnpm_no_node() {\n    let s = sandbox()\n        .package_json(BASIC_PACKAGE_JSON)\n        .pnpm_available_versions(PNPM_VERSION_INFO)\n        .distro_mocks::<PnpmFixture>(&PNPM_VERSION_FIXTURES)\n        .env(\"VOLTA_FEATURE_PNPM\", \"1\")\n        .build();\n\n    assert_that!(\n        s.volta(\"pin pnpm@7\"),\n        execs()\n            .with_status(ExitCode::ConfigurationError as i32)\n            .with_stderr_contains(\n                \"[..]Cannot pin pnpm because the Node version is not pinned in this project.\"\n            )\n    );\n\n    assert_eq!(s.read_package_json(), BASIC_PACKAGE_JSON)\n}\n\n#[test]\nfn pin_pnpm() {\n    let s = sandbox()\n        .package_json(&package_json_with_pinned_node(\"1.2.3\"))\n        .pnpm_available_versions(PNPM_VERSION_INFO)\n        .distro_mocks::<PnpmFixture>(&PNPM_VERSION_FIXTURES)\n        .env(\"VOLTA_FEATURE_PNPM\", \"1\")\n        .build();\n\n    assert_that!(\n        s.volta(\"pin pnpm@7\"),\n        execs().with_status(ExitCode::Success as i32)\n    );\n\n    assert_eq!(\n        s.read_package_json(),\n        package_json_with_pinned_node_pnpm(\"1.2.3\", \"7.7.1\"),\n    )\n}\n\n#[test]\nfn pin_pnpm_reports_info() {\n    let s = sandbox()\n        .package_json(&package_json_with_pinned_node(\"1.2.3\"))\n        .pnpm_available_versions(PNPM_VERSION_INFO)\n        .distro_mocks::<PnpmFixture>(&PNPM_VERSION_FIXTURES)\n        .env(VOLTA_LOGLEVEL, \"info\")\n        .env(\"VOLTA_FEATURE_PNPM\", \"1\")\n        .build();\n\n    assert_that!(\n        s.volta(\"pin pnpm@6\"),\n        execs()\n            .with_status(ExitCode::Success as i32)\n            .with_stdout_contains(\"[..]pinned pnpm@6.34.0 in package.json\")\n    );\n}\n\n#[test]\nfn pin_pnpm_latest() {\n    let s = sandbox()\n        .package_json(&package_json_with_pinned_node(\"1.2.3\"))\n        .pnpm_available_versions(PNPM_VERSION_INFO)\n        .distro_mocks::<PnpmFixture>(&PNPM_VERSION_FIXTURES)\n        .env(\"VOLTA_FEATURE_PNPM\", \"1\")\n        .build();\n\n    assert_that!(\n        s.volta(\"pin pnpm@latest\"),\n        execs().with_status(ExitCode::Success as i32)\n    );\n\n    assert_eq!(\n        s.read_package_json(),\n        package_json_with_pinned_node_pnpm(\"1.2.3\", \"7.7.1\"),\n    )\n}\n\n#[test]\nfn pin_pnpm_no_version() {\n    let s = sandbox()\n        .package_json(&package_json_with_pinned_node(\"1.2.3\"))\n        .pnpm_available_versions(PNPM_VERSION_INFO)\n        .distro_mocks::<PnpmFixture>(&PNPM_VERSION_FIXTURES)\n        .env(\"VOLTA_FEATURE_PNPM\", \"1\")\n        .build();\n\n    assert_that!(\n        s.volta(\"pin pnpm\"),\n        execs().with_status(ExitCode::Success as i32)\n    );\n\n    assert_eq!(\n        s.read_package_json(),\n        package_json_with_pinned_node_pnpm(\"1.2.3\", \"7.7.1\"),\n    )\n}\n\n#[test]\nfn pin_pnpm_missing_release() {\n    let s = sandbox()\n        .package_json(&package_json_with_pinned_node(\"1.2.3\"))\n        .env(\"VOLTA_FEATURE_PNPM\", \"1\")\n        .mock_not_found()\n        .build();\n\n    assert_that!(\n        s.volta(\"pin pnpm@3.3.1\"),\n        execs()\n            .with_status(ExitCode::NetworkError as i32)\n            .with_stderr_contains(\"[..]Could not download pnpm@3.3.1\")\n    );\n\n    assert_eq!(\n        s.read_package_json(),\n        package_json_with_pinned_node(\"1.2.3\"),\n    )\n}\n\n#[test]\nfn pin_node_and_pnpm() {\n    let s = sandbox()\n        .package_json(BASIC_PACKAGE_JSON)\n        .node_available_versions(NODE_VERSION_INFO)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .pnpm_available_versions(PNPM_VERSION_INFO)\n        .distro_mocks::<PnpmFixture>(&PNPM_VERSION_FIXTURES)\n        .env(\"VOLTA_FEATURE_PNPM\", \"1\")\n        .build();\n\n    assert_that!(\n        s.volta(\"pin node@10 pnpm@6\"),\n        execs().with_status(ExitCode::Success as i32)\n    );\n\n    assert_eq!(\n        s.read_package_json(),\n        package_json_with_pinned_node_pnpm(\"10.99.1040\", \"6.34.0\"),\n    )\n}\n\n#[test]\nfn pin_pnpm_leaves_npm() {\n    let s = sandbox()\n        .package_json(&package_json_with_pinned_node_npm(\"1.2.3\", \"3.4.5\"))\n        .pnpm_available_versions(PNPM_VERSION_INFO)\n        .distro_mocks::<PnpmFixture>(&PNPM_VERSION_FIXTURES)\n        .env(\"VOLTA_FEATURE_PNPM\", \"1\")\n        .build();\n\n    assert_that!(\n        s.volta(\"pin pnpm@6.34.0\"),\n        execs().with_status(ExitCode::Success as i32)\n    );\n\n    assert_eq!(\n        s.read_package_json(),\n        package_json_with_pinned_node_npm_pnpm(\"1.2.3\", \"3.4.5\", \"6.34.0\"),\n    )\n}\n"
  },
  {
    "path": "tests/acceptance/volta_run.rs",
    "content": "use crate::support::sandbox::{\n    sandbox, DistroMetadata, NodeFixture, NpmFixture, PnpmFixture, Yarn1Fixture, YarnBerryFixture,\n};\nuse hamcrest2::assert_that;\nuse hamcrest2::prelude::*;\nuse test_support::matchers::execs;\n\nuse volta_core::error::ExitCode;\n\nfn package_json_with_pinned_node(node: &str) -> String {\n    format!(\n        r#\"{{\n  \"name\": \"test-package\",\n  \"volta\": {{\n    \"node\": \"{}\"\n  }}\n}}\"#,\n        node\n    )\n}\n\nfn package_json_with_pinned_node_npm(node: &str, npm: &str) -> String {\n    format!(\n        r#\"{{\n  \"name\": \"test-package\",\n  \"volta\": {{\n    \"node\": \"{}\",\n    \"npm\": \"{}\"\n  }}\n}}\"#,\n        node, npm\n    )\n}\n\nfn package_json_with_pinned_node_pnpm(node_version: &str, pnpm_version: &str) -> String {\n    format!(\n        r#\"{{\n  \"name\": \"test-package\",\n  \"volta\": {{\n    \"node\": \"{}\",\n    \"pnpm\": \"{}\"\n  }}\n}}\"#,\n        node_version, pnpm_version\n    )\n}\n\nfn package_json_with_pinned_node_yarn(node_version: &str, yarn_version: &str) -> String {\n    format!(\n        r#\"{{\n  \"name\": \"test-package\",\n  \"volta\": {{\n    \"node\": \"{}\",\n    \"yarn\": \"{}\"\n  }}\n}}\"#,\n        node_version, yarn_version\n    )\n}\n\nconst NODE_VERSION_INFO: &str = r#\"[\n{\"version\":\"v10.99.1040\",\"npm\":\"6.2.26\",\"lts\": \"Dubnium\",\"files\":[\"linux-x64\",\"osx-x64-tar\",\"win-x64-zip\",\"win-x86-zip\", \"linux-arm64\"]},\n{\"version\":\"v9.27.6\",\"npm\":\"5.6.17\",\"lts\": false,\"files\":[\"linux-x64\",\"osx-x64-tar\",\"win-x64-zip\",\"win-x86-zip\", \"linux-arm64\"]},\n{\"version\":\"v8.9.10\",\"npm\":\"5.6.7\",\"lts\": false,\"files\":[\"linux-x64\",\"osx-x64-tar\",\"win-x64-zip\",\"win-x86-zip\", \"linux-arm64\"]},\n{\"version\":\"v6.19.62\",\"npm\":\"3.10.1066\",\"lts\": false,\"files\":[\"linux-x64\",\"osx-x64-tar\",\"win-x64-zip\",\"win-x86-zip\", \"linux-arm64\"]}\n]\n\"#;\n\ncfg_if::cfg_if! {\n    if #[cfg(target_os = \"macos\")] {\n        const NODE_VERSION_FIXTURES: [DistroMetadata; 4] = [\n            DistroMetadata {\n                version: \"10.99.1040\",\n                compressed_size: 273,\n                uncompressed_size: Some(0x0028_0000),\n            },\n            DistroMetadata {\n                version: \"9.27.6\",\n                compressed_size: 272,\n                uncompressed_size: Some(0x0028_0000),\n            },\n            DistroMetadata {\n                version: \"8.9.10\",\n                compressed_size: 272,\n                uncompressed_size: Some(0x0028_0000),\n            },\n            DistroMetadata {\n                version: \"6.19.62\",\n                compressed_size: 273,\n                uncompressed_size: Some(0x0028_0000),\n            },\n        ];\n    } else if #[cfg(target_os = \"linux\")] {\n        const NODE_VERSION_FIXTURES: [DistroMetadata; 4] = [\n            DistroMetadata {\n                version: \"10.99.1040\",\n                compressed_size: 273,\n                uncompressed_size: Some(0x0028_0000),\n            },\n            DistroMetadata {\n                version: \"9.27.6\",\n                compressed_size: 272,\n                uncompressed_size: Some(0x0028_0000),\n            },\n            DistroMetadata {\n                version: \"8.9.10\",\n                compressed_size: 270,\n                uncompressed_size: Some(0x0028_0000),\n            },\n            DistroMetadata {\n                version: \"6.19.62\",\n                compressed_size: 273,\n                uncompressed_size: Some(0x0028_0000),\n            },\n        ];\n    } else if #[cfg(target_os = \"windows\")] {\n        const NODE_VERSION_FIXTURES: [DistroMetadata; 4] = [\n            DistroMetadata {\n                version: \"10.99.1040\",\n                compressed_size: 1096,\n                uncompressed_size: None,\n            },\n            DistroMetadata {\n                version: \"9.27.6\",\n                compressed_size: 1068,\n                uncompressed_size: None,\n            },\n            DistroMetadata {\n                version: \"8.9.10\",\n                compressed_size: 1055,\n                uncompressed_size: None,\n            },\n            DistroMetadata {\n                version: \"6.19.62\",\n                compressed_size: 1056,\n                uncompressed_size: None,\n            },\n        ];\n    } else {\n        compile_error!(\"Unsupported target_os for tests (expected 'macos', 'linux', or 'windows').\");\n    }\n}\n\nconst PNPM_VERSION_INFO: &str = r#\"\n{\n    \"name\":\"pnpm\",\n    \"dist-tags\": { \"latest\":\"7.7.1\" },\n    \"versions\": {\n        \"6.34.0\": { \"version\":\"6.34.0\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }},\n        \"7.7.1\": { \"version\":\"7.7.1\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }}\n    }\n}\n\"#;\n\nconst PNPM_VERSION_FIXTURES: [DistroMetadata; 2] = [\n    DistroMetadata {\n        version: \"6.34.0\",\n        compressed_size: 500,\n        uncompressed_size: Some(0x0028_0000),\n    },\n    DistroMetadata {\n        version: \"7.7.1\",\n        compressed_size: 518,\n        uncompressed_size: Some(0x0028_0000),\n    },\n];\n\nconst YARN_1_VERSION_INFO: &str = r#\"{\n    \"name\":\"yarn\",\n    \"dist-tags\": { \"latest\":\"1.12.99\" },\n    \"versions\": {\n        \"1.2.42\": { \"version\":\"1.2.42\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }},\n        \"1.4.159\": { \"version\":\"1.4.159\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }},\n        \"1.7.71\": { \"version\":\"1.7.71\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }},\n        \"1.12.99\": { \"version\":\"1.12.99\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }}\n    }\n}\"#;\n\nconst YARN_1_VERSION_FIXTURES: [DistroMetadata; 4] = [\n    DistroMetadata {\n        version: \"1.12.99\",\n        compressed_size: 178,\n        uncompressed_size: Some(0x0028_0000),\n    },\n    DistroMetadata {\n        version: \"1.7.71\",\n        compressed_size: 176,\n        uncompressed_size: Some(0x0028_0000),\n    },\n    DistroMetadata {\n        version: \"1.4.159\",\n        compressed_size: 177,\n        uncompressed_size: Some(0x0028_0000),\n    },\n    DistroMetadata {\n        version: \"1.2.42\",\n        compressed_size: 174,\n        uncompressed_size: Some(0x0028_0000),\n    },\n];\n\nconst YARN_BERRY_VERSION_INFO: &str = r#\"{\n    \"name\":\"@yarnpkg/cli-dist\",\n    \"dist-tags\": { \"latest\":\"3.12.99\" },\n    \"versions\": {\n        \"2.4.159\": { \"version\":\"2.4.159\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }},\n        \"3.2.42\": { \"version\":\"3.2.42\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }},\n        \"3.7.71\": { \"version\":\"3.7.71\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }},\n        \"3.12.99\": { \"version\":\"3.12.99\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }}\n    }\n}\"#;\n\nconst YARN_BERRY_VERSION_FIXTURES: [DistroMetadata; 4] = [\n    DistroMetadata {\n        version: \"2.4.159\",\n        compressed_size: 177,\n        uncompressed_size: Some(0x0028_0000),\n    },\n    DistroMetadata {\n        version: \"3.12.99\",\n        compressed_size: 178,\n        uncompressed_size: Some(0x0028_0000),\n    },\n    DistroMetadata {\n        version: \"3.7.71\",\n        compressed_size: 176,\n        uncompressed_size: Some(0x0028_0000),\n    },\n    DistroMetadata {\n        version: \"3.2.42\",\n        compressed_size: 174,\n        uncompressed_size: Some(0x0028_0000),\n    },\n];\n\nconst NPM_VERSION_FIXTURES: [DistroMetadata; 3] = [\n    DistroMetadata {\n        version: \"1.2.3\",\n        compressed_size: 239,\n        uncompressed_size: Some(0x0028_0000),\n    },\n    DistroMetadata {\n        version: \"4.5.6\",\n        compressed_size: 239,\n        uncompressed_size: Some(0x0028_0000),\n    },\n    DistroMetadata {\n        version: \"8.1.5\",\n        compressed_size: 239,\n        uncompressed_size: Some(0x0028_0000),\n    },\n];\n\nconst NPM_VERSION_INFO: &str = r#\"\n{\n    \"name\":\"npm\",\n    \"dist-tags\": { \"latest\":\"8.1.5\" },\n    \"versions\": {\n        \"1.2.3\": { \"version\":\"1.2.3\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }},\n        \"4.5.6\": { \"version\":\"4.5.6\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }},\n        \"8.1.5\": { \"version\":\"8.1.5\", \"dist\": { \"shasum\":\"\", \"tarball\":\"\" }}\n    }\n}\n\"#;\n\nconst VOLTA_LOGLEVEL: &str = \"VOLTA_LOGLEVEL\";\n\n#[test]\nfn command_line_node() {\n    let s = sandbox()\n        .node_available_versions(NODE_VERSION_INFO)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .env(VOLTA_LOGLEVEL, \"debug\")\n        .build();\n\n    assert_that!(\n        s.volta(\"run --node 10.99.1040 node --version\"),\n        execs()\n            .with_status(ExitCode::Success as i32)\n            .with_stderr_contains(\"[..]Node: 10.99.1040 from command-line configuration\")\n    );\n}\n\n#[test]\nfn inherited_node() {\n    let s = sandbox()\n        .node_available_versions(NODE_VERSION_INFO)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .package_json(&package_json_with_pinned_node(\"9.27.6\"))\n        .env(VOLTA_LOGLEVEL, \"debug\")\n        .build();\n\n    assert_that!(\n        s.volta(\"run node --version\"),\n        execs()\n            .with_status(ExitCode::Success as i32)\n            .with_stderr_contains(\"[..]Node: 9.27.6 from project configuration\")\n    );\n}\n\n#[test]\nfn command_line_npm() {\n    let s = sandbox()\n        .node_available_versions(NODE_VERSION_INFO)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .npm_available_versions(NPM_VERSION_INFO)\n        .distro_mocks::<NpmFixture>(&NPM_VERSION_FIXTURES)\n        .env(VOLTA_LOGLEVEL, \"debug\")\n        .build();\n\n    assert_that!(\n        s.volta(\"run --node 10.99.1040 --npm 8.1.5 npm --version\"),\n        execs()\n            .with_status(ExitCode::Success as i32)\n            .with_stderr_contains(\"[..]npm: 8.1.5 from command-line configuration\")\n    );\n}\n\n#[test]\nfn inherited_npm() {\n    let s = sandbox()\n        .node_available_versions(NODE_VERSION_INFO)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .npm_available_versions(NPM_VERSION_INFO)\n        .distro_mocks::<NpmFixture>(&NPM_VERSION_FIXTURES)\n        .package_json(&package_json_with_pinned_node_npm(\"9.27.6\", \"4.5.6\"))\n        .env(VOLTA_LOGLEVEL, \"debug\")\n        .build();\n\n    assert_that!(\n        s.volta(\"run --node 10.99.1040 npm --version\"),\n        execs()\n            .with_status(ExitCode::Success as i32)\n            .with_stderr_contains(\"[..]npm: 4.5.6 from project configuration\")\n    );\n}\n\n#[test]\nfn force_bundled_npm() {\n    let s = sandbox()\n        .node_available_versions(NODE_VERSION_INFO)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .npm_available_versions(NPM_VERSION_INFO)\n        .distro_mocks::<NpmFixture>(&NPM_VERSION_FIXTURES)\n        .package_json(&package_json_with_pinned_node_npm(\"9.27.6\", \"4.5.6\"))\n        .env(VOLTA_LOGLEVEL, \"debug\")\n        .build();\n\n    assert_that!(\n        s.volta(\"run --bundled-npm npm --version\"),\n        execs()\n            .with_status(ExitCode::Success as i32)\n            .with_stderr_contains(\"[..]npm: 5.6.17[..]\")\n    );\n}\n\n#[test]\nfn command_line_yarn_1() {\n    let s = sandbox()\n        .node_available_versions(NODE_VERSION_INFO)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .yarn_1_available_versions(YARN_1_VERSION_INFO)\n        .distro_mocks::<Yarn1Fixture>(&YARN_1_VERSION_FIXTURES)\n        .env(VOLTA_LOGLEVEL, \"debug\")\n        .build();\n\n    assert_that!(\n        s.volta(\"run --node 10.99.1040 --yarn 1.7.71 yarn --version\"),\n        execs()\n            .with_status(ExitCode::Success as i32)\n            .with_stderr_contains(\"[..]Yarn: 1.7.71 from command-line configuration\")\n    );\n    assert_that!(\n        s.volta(\"run --node 10.99.1040 --yarn 1.7.71 yarnpkg --version\"),\n        execs()\n            .with_status(ExitCode::Success as i32)\n            .with_stderr_contains(\"[..]Yarn: 1.7.71 from command-line configuration\")\n    );\n}\n\n#[test]\nfn command_line_yarn_3() {\n    let s = sandbox()\n        .node_available_versions(NODE_VERSION_INFO)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .yarn_1_available_versions(YARN_1_VERSION_INFO)\n        .yarn_berry_available_versions(YARN_BERRY_VERSION_INFO)\n        .distro_mocks::<Yarn1Fixture>(&YARN_1_VERSION_FIXTURES)\n        .distro_mocks::<YarnBerryFixture>(&YARN_BERRY_VERSION_FIXTURES)\n        .env(VOLTA_LOGLEVEL, \"debug\")\n        .build();\n\n    assert_that!(\n        s.volta(\"run --node 10.99.1040 --yarn 3.7.71 yarn --version\"),\n        execs()\n            .with_status(ExitCode::Success as i32)\n            .with_stderr_contains(\"[..]Yarn: 3.7.71 from command-line configuration\")\n    );\n}\n\n#[test]\nfn inherited_yarn_1() {\n    let s = sandbox()\n        .node_available_versions(NODE_VERSION_INFO)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .yarn_1_available_versions(YARN_1_VERSION_INFO)\n        .distro_mocks::<Yarn1Fixture>(&YARN_1_VERSION_FIXTURES)\n        .package_json(&package_json_with_pinned_node_yarn(\"10.99.1040\", \"1.2.42\"))\n        .env(VOLTA_LOGLEVEL, \"debug\")\n        .build();\n\n    assert_that!(\n        s.volta(\"run --node 10.99.1040 yarn --version\"),\n        execs()\n            .with_status(ExitCode::Success as i32)\n            .with_stderr_contains(\"[..]Yarn: 1.2.42 from project configuration\")\n    );\n}\n\n#[test]\nfn inherited_yarn_3() {\n    let s = sandbox()\n        .node_available_versions(NODE_VERSION_INFO)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .yarn_1_available_versions(YARN_1_VERSION_INFO)\n        .yarn_berry_available_versions(YARN_BERRY_VERSION_INFO)\n        .distro_mocks::<Yarn1Fixture>(&YARN_1_VERSION_FIXTURES)\n        .distro_mocks::<YarnBerryFixture>(&YARN_BERRY_VERSION_FIXTURES)\n        .package_json(&package_json_with_pinned_node_yarn(\"10.99.1040\", \"3.2.42\"))\n        .env(VOLTA_LOGLEVEL, \"debug\")\n        .build();\n\n    assert_that!(\n        s.volta(\"run --node 10.99.1040 yarn --version\"),\n        execs()\n            .with_status(ExitCode::Success as i32)\n            .with_stderr_contains(\"[..]Yarn: 3.2.42 from project configuration\")\n    );\n}\n\n#[test]\nfn force_no_yarn() {\n    let s = sandbox()\n        .node_available_versions(NODE_VERSION_INFO)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .yarn_1_available_versions(YARN_1_VERSION_INFO)\n        .distro_mocks::<Yarn1Fixture>(&YARN_1_VERSION_FIXTURES)\n        .package_json(&package_json_with_pinned_node_yarn(\"10.99.1040\", \"1.2.42\"))\n        .env(VOLTA_LOGLEVEL, \"debug\")\n        .build();\n\n    assert_that!(\n        s.volta(\"run --no-yarn yarn --version\"),\n        execs()\n            .with_status(ExitCode::ConfigurationError as i32)\n            .with_stderr_contains(\"[..]No Yarn version found in this project.\")\n    );\n}\n\n#[test]\nfn command_line_pnpm() {\n    let s = sandbox()\n        .node_available_versions(NODE_VERSION_INFO)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .pnpm_available_versions(PNPM_VERSION_INFO)\n        .distro_mocks::<PnpmFixture>(&PNPM_VERSION_FIXTURES)\n        .env(VOLTA_LOGLEVEL, \"debug\")\n        .env(\"VOLTA_FEATURE_PNPM\", \"1\")\n        .build();\n\n    assert_that!(\n        s.volta(\"run --node 10.99.1040 --pnpm 6.34.0 pnpm --version\"),\n        execs()\n            .with_status(ExitCode::Success as i32)\n            .with_stderr_contains(\"[..]pnpm: 6.34.0 from command-line configuration\")\n    );\n}\n\n#[test]\nfn inherited_pnpm() {\n    let s = sandbox()\n        .node_available_versions(NODE_VERSION_INFO)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .pnpm_available_versions(PNPM_VERSION_INFO)\n        .distro_mocks::<PnpmFixture>(&PNPM_VERSION_FIXTURES)\n        .package_json(&package_json_with_pinned_node_pnpm(\"10.99.1040\", \"7.7.1\"))\n        .env(VOLTA_LOGLEVEL, \"debug\")\n        .env(\"VOLTA_FEATURE_PNPM\", \"1\")\n        .build();\n\n    assert_that!(\n        s.volta(\"run --node 10.99.1040 pnpm  --version\"),\n        execs()\n            .with_status(ExitCode::Success as i32)\n            .with_stderr_contains(\"[..]pnpm: 7.7.1 from project configuration\")\n    );\n}\n\n#[test]\nfn force_no_pnpm() {\n    let s = sandbox()\n        .node_available_versions(NODE_VERSION_INFO)\n        .distro_mocks::<NodeFixture>(&NODE_VERSION_FIXTURES)\n        .pnpm_available_versions(PNPM_VERSION_INFO)\n        .distro_mocks::<PnpmFixture>(&PNPM_VERSION_FIXTURES)\n        .package_json(&package_json_with_pinned_node_pnpm(\"10.99.1040\", \"7.7.1\"))\n        .env(VOLTA_LOGLEVEL, \"debug\")\n        .env(\"VOLTA_FEATURE_PNPM\", \"1\")\n        .build();\n\n    assert_that!(\n        s.volta(\"run --no-pnpm pnpm --version\"),\n        execs()\n            .with_status(ExitCode::ConfigurationError as i32)\n            .with_stderr_contains(\"[..]No pnpm version found in this project.\")\n    );\n}\n"
  },
  {
    "path": "tests/acceptance/volta_uninstall.rs",
    "content": "//! Tests for `volta uninstall`.\n\nuse crate::support::sandbox::{sandbox, Sandbox};\nuse hamcrest2::assert_that;\nuse hamcrest2::prelude::*;\nuse test_support::matchers::execs;\n\nconst PKG_CONFIG_BASIC: &str = r#\"{\n  \"name\": \"cowsay\",\n  \"version\": \"1.4.0\",\n  \"platform\": {\n    \"node\": \"11.10.1\",\n    \"npm\": \"6.7.0\",\n    \"yarn\": null\n  },\n  \"bins\": [\n    \"cowsay\",\n    \"cowthink\"\n  ],\n  \"manager\": \"Npm\"\n}\"#;\n\nconst PKG_CONFIG_NO_BINS: &str = r#\"{\n  \"name\": \"cowsay\",\n  \"version\": \"1.4.0\",\n  \"platform\": {\n    \"node\": \"11.10.1\",\n    \"npm\": \"6.7.0\",\n    \"yarn\": null\n  },\n  \"bins\": [],\n  \"manager\": \"Npm\"\n}\"#;\n\nfn bin_config(name: &str) -> String {\n    format!(\n        r#\"{{\n  \"name\": \"{}\",\n  \"package\": \"cowsay\",\n  \"version\": \"1.4.0\",\n  \"platform\": {{\n    \"node\": \"11.10.1\",\n    \"npm\": \"6.7.0\",\n    \"yarn\": null\n  }},\n  \"manager\": \"Npm\"\n}}\"#,\n        name\n    )\n}\n\nconst VOLTA_LOGLEVEL: &str = \"VOLTA_LOGLEVEL\";\n\n#[test]\nfn uninstall_nonexistent_pkg() {\n    // if the package doesn't exist, it should just inform the user but not throw an error\n    let s = sandbox().env(VOLTA_LOGLEVEL, \"info\").build();\n\n    assert_that!(\n        s.volta(\"uninstall cowsay\"),\n        execs()\n            .with_status(0)\n            .with_stderr_contains(\"[..]No package 'cowsay' found to uninstall\")\n    );\n}\n\n#[test]\nfn uninstall_package_basic() {\n    // basic uninstall - everything exists, and everything except the cached\n    // inventory files should be deleted\n    let s = sandbox()\n        .package_config(\"cowsay\", PKG_CONFIG_BASIC)\n        .binary_config(\"cowsay\", &bin_config(\"cowsay\"))\n        .binary_config(\"cowthink\", &bin_config(\"cowthink\"))\n        .shim(\"cowsay\")\n        .shim(\"cowthink\")\n        .package_image(\"cowsay\", \"1.4.0\", None)\n        .env(VOLTA_LOGLEVEL, \"info\")\n        .build();\n\n    assert_that!(\n        s.volta(\"uninstall cowsay\"),\n        execs()\n            .with_status(0)\n            .with_stdout_contains(\"Removed executable 'cowsay' installed by 'cowsay'\")\n            .with_stdout_contains(\"Removed executable 'cowthink' installed by 'cowsay'\")\n            .with_stdout_contains(\"[..]package 'cowsay' uninstalled\")\n    );\n\n    // check that everything is deleted\n    assert!(!Sandbox::package_config_exists(\"cowsay\"));\n    assert!(!Sandbox::bin_config_exists(\"cowsay\"));\n    assert!(!Sandbox::bin_config_exists(\"cowthink\"));\n    assert!(!Sandbox::shim_exists(\"cowsay\"));\n    assert!(!Sandbox::shim_exists(\"cowthink\"));\n    assert!(!Sandbox::package_image_exists(\"cowsay\"));\n}\n\n// The setup here is the same as the above, but here we check to make sure that\n// if the user supplies a version, we error correctly.\n#[test]\nfn uninstall_package_basic_with_version() {\n    // basic uninstall - everything exists, and everything except the cached\n    // inventory files should be deleted\n    let s = sandbox()\n        .package_config(\"cowsay\", PKG_CONFIG_BASIC)\n        .binary_config(\"cowsay\", &bin_config(\"cowsay\"))\n        .binary_config(\"cowthink\", &bin_config(\"cowthink\"))\n        .shim(\"cowsay\")\n        .shim(\"cowthink\")\n        .package_image(\"cowsay\", \"1.4.0\", None)\n        .env(VOLTA_LOGLEVEL, \"info\")\n        .build();\n\n    assert_that!(\n        s.volta(\"uninstall cowsay@1.4.0\"),\n        execs().with_status(1).with_stderr_contains(\n            \"[..]error: uninstalling specific versions of tools is not supported yet.\"\n        )\n    );\n}\n\n#[test]\nfn uninstall_package_no_bins() {\n    // the package doesn't contain any executables, it should uninstall without error\n    // (normally installing a package with no executables should not happen)\n    let s = sandbox()\n        .package_config(\"cowsay\", PKG_CONFIG_NO_BINS)\n        .package_image(\"cowsay\", \"1.4.0\", None)\n        .env(VOLTA_LOGLEVEL, \"info\")\n        .build();\n\n    assert_that!(\n        s.volta(\"uninstall cowsay\"),\n        execs()\n            .with_status(0)\n            .with_stdout_contains(\"[..]package 'cowsay' uninstalled\")\n    );\n\n    // check that everything is deleted\n    assert!(!Sandbox::package_config_exists(\"cowsay\"));\n    assert!(!Sandbox::bin_config_exists(\"cowsay\"));\n    assert!(!Sandbox::bin_config_exists(\"cowthink\"));\n    assert!(!Sandbox::shim_exists(\"cowsay\"));\n    assert!(!Sandbox::shim_exists(\"cowthink\"));\n    assert!(!Sandbox::package_image_exists(\"cowsay\"));\n}\n\n#[test]\nfn uninstall_package_no_image() {\n    // there is no unpacked & initialized package, but everything should be removed\n    // (without erroring and failing to remove everything)\n    let s = sandbox()\n        .package_config(\"cowsay\", PKG_CONFIG_BASIC)\n        .binary_config(\"cowsay\", &bin_config(\"cowsay\"))\n        .binary_config(\"cowthink\", &bin_config(\"cowthink\"))\n        .shim(\"cowsay\")\n        .shim(\"cowthink\")\n        .env(VOLTA_LOGLEVEL, \"info\")\n        .build();\n\n    assert_that!(\n        s.volta(\"uninstall cowsay\"),\n        execs()\n            .with_status(0)\n            .with_stdout_contains(\"Removed executable 'cowsay' installed by 'cowsay'\")\n            .with_stdout_contains(\"Removed executable 'cowthink' installed by 'cowsay'\")\n            .with_stdout_contains(\"[..]package 'cowsay' uninstalled\")\n    );\n\n    // check that everything is deleted\n    assert!(!Sandbox::package_config_exists(\"cowsay\"));\n    assert!(!Sandbox::bin_config_exists(\"cowsay\"));\n    assert!(!Sandbox::bin_config_exists(\"cowthink\"));\n    assert!(!Sandbox::shim_exists(\"cowsay\"));\n    assert!(!Sandbox::shim_exists(\"cowthink\"));\n    assert!(!Sandbox::package_image_exists(\"cowsay\"));\n}\n\n#[test]\nfn uninstall_package_orphaned_bins() {\n    // the package config does not exist, but for some reason there are orphaned binaries\n    // those should be removed\n    let s = sandbox()\n        .binary_config(\"cowsay\", &bin_config(\"cowsay\"))\n        .binary_config(\"cowthink\", &bin_config(\"cowthink\"))\n        .shim(\"cowsay\")\n        .shim(\"cowthink\")\n        .env(VOLTA_LOGLEVEL, \"info\")\n        .build();\n\n    assert_that!(\n        s.volta(\"uninstall cowsay\"),\n        execs()\n            .with_status(0)\n            .with_stdout_contains(\"Removed executable 'cowsay' installed by 'cowsay'\")\n            .with_stdout_contains(\"Removed executable 'cowthink' installed by 'cowsay'\")\n            .with_stdout_contains(\"[..]package 'cowsay' uninstalled\")\n    );\n\n    // check that everything is deleted\n    assert!(!Sandbox::bin_config_exists(\"cowsay\"));\n    assert!(!Sandbox::bin_config_exists(\"cowthink\"));\n    assert!(!Sandbox::shim_exists(\"cowsay\"));\n    assert!(!Sandbox::shim_exists(\"cowthink\"));\n}\n\n#[test]\nfn uninstall_runtime() {\n    let s = sandbox().build();\n    assert_that!(\n        s.volta(\"uninstall node\"),\n        execs()\n            .with_status(1)\n            .with_stderr_contains(\"[..]error: Uninstalling node is not supported yet.\")\n    )\n}\n"
  },
  {
    "path": "tests/fixtures/pnpm-0.0.1.tgz",
    "content": "CORRUPTED\n"
  },
  {
    "path": "tests/fixtures/yarn-0.0.1.tgz",
    "content": "CORRUPTED\n"
  },
  {
    "path": "tests/smoke/autodownload.rs",
    "content": "use crate::support::temp_project::temp_project;\n\nuse hamcrest2::assert_that;\nuse hamcrest2::prelude::*;\nuse test_support::matchers::execs;\n\nstatic PACKAGE_JSON_WITH_PINNED_NODE: &str = r#\"{\n    \"name\": \"test-package\",\n    \"volta\": {\n        \"node\": \"14.15.5\"\n    }\n}\"#;\n\nstatic PACKAGE_JSON_WITH_PINNED_NODE_NPM: &str = r#\"{\n    \"name\": \"test-package\",\n    \"volta\": {\n        \"node\": \"17.3.0\",\n        \"npm\": \"8.5.1\"\n    }\n}\"#;\n\nstatic PACKAGE_JSON_WITH_PINNED_NODE_YARN_1: &str = r#\"{\n    \"name\": \"test-package\",\n    \"volta\": {\n        \"node\": \"16.11.1\",\n        \"yarn\": \"1.22.16\"\n    }\n}\"#;\n\nstatic PACKAGE_JSON_WITH_PINNED_NODE_YARN_3: &str = r#\"{\n    \"name\": \"test-package\",\n    \"volta\": {\n        \"node\": \"16.14.0\",\n        \"yarn\": \"3.1.0\"\n    }\n}\"#;\n\n#[test]\nfn autodownload_node() {\n    let p = temp_project()\n        .package_json(PACKAGE_JSON_WITH_PINNED_NODE)\n        .build();\n\n    assert_that!(\n        p.node(\"--version\"),\n        execs().with_status(0).with_stdout_contains(\"v14.15.5\")\n    );\n}\n\n#[test]\nfn autodownload_npm() {\n    let p = temp_project()\n        .package_json(PACKAGE_JSON_WITH_PINNED_NODE_NPM)\n        .build();\n\n    assert_that!(\n        p.npm(\"--version\"),\n        execs().with_status(0).with_stdout_contains(\"8.5.1\")\n    );\n}\n\n#[test]\nfn autodownload_yarn_1() {\n    let p = temp_project()\n        .package_json(PACKAGE_JSON_WITH_PINNED_NODE_YARN_1)\n        .build();\n\n    assert_that!(\n        p.yarn(\"--version\"),\n        execs().with_status(0).with_stdout_contains(\"1.22.16\")\n    );\n}\n\n#[test]\nfn autodownload_yarn_3() {\n    let p = temp_project()\n        .package_json(PACKAGE_JSON_WITH_PINNED_NODE_YARN_3)\n        .build();\n\n    assert_that!(\n        p.yarn(\"--version\"),\n        execs().with_status(0).with_stdout_contains(\"3.1.0\")\n    );\n}\n"
  },
  {
    "path": "tests/smoke/direct_install.rs",
    "content": "use crate::support::temp_project::temp_project;\nuse hamcrest2::assert_that;\nuse hamcrest2::prelude::*;\nuse test_support::matchers::execs;\n\n#[test]\nfn npm_global_install() {\n    let p = temp_project().build();\n\n    // Have to install node to ensure npm is available\n    assert_that!(p.volta(\"install node@14.1.0\"), execs().with_status(0));\n\n    assert_that!(\n        p.npm(\"install --global typescript@3.9.4 yarn@1.16.0 ../../../../tests/fixtures/volta-test-1.0.0.tgz\"),\n        execs().with_status(0)\n    );\n\n    assert!(p.shim_exists(\"tsc\"));\n    assert!(p.shim_exists(\"tsserver\"));\n    assert!(p.package_is_installed(\"typescript\"));\n    assert_that!(\n        p.exec_shim(\"tsc\", \"--version\"),\n        execs().with_status(0).with_stdout_contains(\"Version 3.9.4\")\n    );\n\n    assert!(p.yarn_version_is_fetched(\"1.16.0\"));\n    assert!(p.yarn_version_is_unpacked(\"1.16.0\"));\n    p.assert_yarn_version_is_installed(\"1.16.0\");\n\n    assert_that!(\n        p.yarn(\"--version\"),\n        execs().with_status(0).with_stdout_contains(\"1.16.0\")\n    );\n\n    assert!(p.shim_exists(\"volta-test\"));\n    assert!(p.package_is_installed(\"volta-test\"));\n    assert_that!(\n        p.exec_shim(\"volta-test\", \"\"),\n        execs()\n            .with_status(0)\n            .with_stdout_contains(\"Volta test successful\")\n    );\n}\n\n#[test]\nfn yarn_global_add() {\n    let p = temp_project().build();\n\n    let tarball_path = p\n        .root()\n        .join(\"../../../../tests/fixtures/volta-test-1.0.0.tgz\")\n        .canonicalize()\n        .unwrap();\n\n    // Have to install node and yarn first\n    assert_that!(\n        p.volta(\"install node@14.2.0 yarn@1.22.5\"),\n        execs().with_status(0)\n    );\n\n    assert_that!(\n        p.yarn(&format!(\n            \"global add typescript@4.0.2 npm@6.4.0 file:{}\",\n            tarball_path.display()\n        )),\n        execs().with_status(0)\n    );\n\n    assert!(p.shim_exists(\"tsc\"));\n    assert!(p.shim_exists(\"tsserver\"));\n    assert!(p.package_is_installed(\"typescript\"));\n    assert_that!(\n        p.exec_shim(\"tsc\", \"--version\"),\n        execs().with_status(0).with_stdout_contains(\"Version 4.0.2\")\n    );\n\n    assert!(p.npm_version_is_fetched(\"6.4.0\"));\n    assert!(p.npm_version_is_unpacked(\"6.4.0\"));\n    p.assert_npm_version_is_installed(\"6.4.0\");\n\n    assert_that!(\n        p.npm(\"--version\"),\n        execs().with_status(0).with_stdout_contains(\"6.4.0\")\n    );\n\n    assert!(p.shim_exists(\"volta-test\"));\n    assert!(p.package_is_installed(\"volta-test\"));\n    assert_that!(\n        p.exec_shim(\"volta-test\", \"\"),\n        execs()\n            .with_status(0)\n            .with_stdout_contains(\"Volta test successful\")\n    );\n}\n"
  },
  {
    "path": "tests/smoke/direct_upgrade.rs",
    "content": "use crate::support::temp_project::temp_project;\nuse hamcrest2::assert_that;\nuse hamcrest2::prelude::*;\nuse test_support::matchers::execs;\nuse volta_core::error::ExitCode;\n\n#[test]\nfn npm_global_update() {\n    let p = temp_project().build();\n\n    // Install Node and typescript\n    assert_that!(\n        p.volta(\"install node@14.10.1 typescript@2.8.4\"),\n        execs().with_status(0)\n    );\n    // Confirm correct version of typescript installed\n    assert_that!(\n        p.exec_shim(\"tsc\", \"--version\"),\n        execs().with_status(0).with_stdout_contains(\"Version 2.8.4\")\n    );\n\n    // Update typescript\n    assert_that!(p.npm(\"update --global typescript\"), execs().with_status(0));\n    // Confirm update completed successfully\n    assert_that!(\n        p.exec_shim(\"tsc\", \"--version\"),\n        execs().with_status(0).with_stdout_contains(\"Version 2.9.2\")\n    );\n\n    // Revert typescript update\n    assert_that!(p.npm(\"i -g typescript@2.8.4\"), execs().with_status(0));\n    // Update all packages (should include typescript)\n    assert_that!(p.npm(\"update --global\"), execs().with_status(0));\n    // Confirm update\n    assert_that!(\n        p.exec_shim(\"tsc\", \"--version\"),\n        execs().with_status(0).with_stdout_contains(\"Version 2.9.2\")\n    );\n\n    // Confirm that attempting to upgrade using `yarn` fails\n    assert_that!(\n        p.yarn(\"global upgrade typescript\"),\n        execs()\n            .with_status(ExitCode::ExecutionFailure as i32)\n            .with_stderr_contains(\"[..]The package 'typescript' was installed using npm.\")\n    );\n}\n\n#[test]\nfn yarn_global_update() {\n    let p = temp_project().build();\n\n    // Install Node and Yarn\n    assert_that!(\n        p.volta(\"install node@14.10.1 yarn@1.22.5\"),\n        execs().with_status(0)\n    );\n    // Install typescript\n    assert_that!(\n        p.yarn(\"global add typescript@2.8.4\"),\n        execs().with_status(0)\n    );\n    // Confirm correct version of typescript installed\n    assert_that!(\n        p.exec_shim(\"tsc\", \"--version\"),\n        execs().with_status(0).with_stdout_contains(\"Version 2.8.4\")\n    );\n\n    // Upgrade typescript\n    assert_that!(\n        p.yarn(\"global upgrade typescript@2.9\"),\n        execs().with_status(0)\n    );\n    // Confirm upgrade completed successfully\n    assert_that!(\n        p.exec_shim(\"tsc\", \"--version\"),\n        execs().with_status(0).with_stdout_contains(\"Version 2.9.2\")\n    );\n\n    // Note: Since Yarn always installs the latest version that matches your requirements and\n    // 'upgrade' also gets the latest version that matches (which can change over time), an\n    // immediate call to 'yarn upgrade' without packages won't result in any change.\n\n    // This is in contrast to npm, which treats your installed version as a caret specifier when\n    // runnin `npm update`\n\n    // Confirm that attempting to upgrade using `npm` fails\n    assert_that!(\n        p.npm(\"update -g typescript\"),\n        execs()\n            .with_status(ExitCode::ExecutionFailure as i32)\n            .with_stderr_contains(\"[..]The package 'typescript' was installed using Yarn.\")\n    );\n}\n"
  },
  {
    "path": "tests/smoke/main.rs",
    "content": "// Smoke tests for Volta, that will be run in CI.\n//\n// To run these locally:\n// (CAUTION: this will destroy the Volta installation on the system where this is run)\n//\n// ```\n// VOLTA_LOGLEVEL=debug cargo test --test smoke --features smoke-tests -- --test-threads 1\n// ```\n//\n// Also note that each test uses a different version of node and yarn. This is to prevent\n// false positives if the tests are not cleaned up correctly. Any new tests should use\n// different versions of node and yarn.\n\ncfg_if::cfg_if! {\n    if #[cfg(all(unix, feature = \"smoke-tests\"))] {\n        mod autodownload;\n        mod direct_install;\n        mod direct_upgrade;\n        mod npm_link;\n        mod package_migration;\n        pub mod support;\n        mod volta_fetch;\n        mod volta_install;\n        mod volta_run;\n    }\n}\n"
  },
  {
    "path": "tests/smoke/npm_link.rs",
    "content": "use crate::support::temp_project::temp_project;\nuse hamcrest2::assert_that;\nuse hamcrest2::prelude::*;\nuse test_support::matchers::execs;\n\nconst PACKAGE_JSON: &str = r#\"\n{\n    \"name\": \"my-library\",\n    \"version\": \"1.0.0\",\n    \"bin\": {\n        \"mylibrary\": \"./index.js\"\n    }\n}\"#;\n\nconst INDEX_JS: &str = r#\"#!/usr/bin/env node\n\nconsole.log('VOLTA TEST');\n\"#;\n\n#[test]\nfn link_unlink_local_project() {\n    let p = temp_project()\n        .package_json(PACKAGE_JSON)\n        .project_file(\"index.js\", INDEX_JS)\n        .build();\n\n    // Install node to ensure npm is available\n    assert_that!(p.volta(\"install node@14.15.1\"), execs().with_status(0));\n\n    // Link the current project as a global\n    assert_that!(p.npm(\"link\"), execs().with_status(0));\n    // Executable should be available\n    assert!(p.shim_exists(\"mylibrary\"));\n    assert!(p.package_is_installed(\"my-library\"));\n    assert_that!(\n        p.exec_shim(\"mylibrary\", \"\"),\n        execs().with_status(0).with_stdout_contains(\"VOLTA TEST\")\n    );\n\n    // Unlink the current project\n    assert_that!(p.npm(\"unlink\"), execs().with_status(0));\n    // Executable should no longer be available\n    assert!(!p.shim_exists(\"mylibrary\"));\n    assert!(!p.package_is_installed(\"my-library\"));\n}\n\n#[test]\nfn link_global_into_current_project() {\n    let p = temp_project().package_json(PACKAGE_JSON).build();\n\n    assert_that!(\n        p.volta(\"install node@14.19.0 typescript@4.1.2\"),\n        execs().with_status(0)\n    );\n\n    // Link typescript into the current project\n    assert_that!(p.npm(\"link typescript\"), execs().with_status(0));\n    // Typescript should now be available inside the node_modules directory\n    assert!(p.project_path_exists(\"node_modules/typescript\"));\n    assert!(p.project_path_exists(\"node_modules/typescript/package.json\"));\n}\n"
  },
  {
    "path": "tests/smoke/package_migration.rs",
    "content": "use crate::support::temp_project::temp_project;\n\nuse hamcrest2::assert_that;\nuse hamcrest2::prelude::*;\nuse test_support::matchers::execs;\n\nconst LEGACY_PACKAGE_CONFIG: &str = r#\"{\n  \"name\": \"cowsay\",\n  \"version\": \"1.1.7\",\n  \"platform\": {\n    \"node\": {\n      \"runtime\": \"14.18.2\",\n      \"npm\": null\n    },\n    \"yarn\": null\n  },\n  \"bins\": [\n    \"cowsay\",\n    \"cowthink\"\n  ]\n}\"#;\n\nconst LEGACY_BIN_CONFIG: &str = r#\"{\n  \"name\": \"cowsay\",\n  \"package\": \"cowsay\",\n  \"version\": \"1.1.7\",\n  \"path\": \"./cli.js\",\n  \"platform\": {\n    \"node\": {\n      \"runtime\": \"14.18.2\",\n      \"npm\": null\n    },\n    \"yarn\": null\n  },\n  \"loader\": {\n    \"command\": \"node\",\n    \"args\": []\n  }\n}\"#;\n\nconst COWSAY_HELLO: &str = r#\" _______\n< hello >\n -------\n        \\   ^__^\n         \\  (oo)\\_______\n            (__)\\       )\\/\\\n                ||----w |\n                ||     ||\"#;\n\n#[test]\nfn legacy_package_upgrade() {\n    let p = temp_project()\n        .volta_home_file(\"tools/user/packages/cowsay.json\", LEGACY_PACKAGE_CONFIG)\n        .volta_home_file(\"tools/user/bins/cowsay.json\", LEGACY_BIN_CONFIG)\n        .volta_home_file(\n            \"tools/image/packages/cowsay/1.3.1/README.md\",\n            \"Mock of installed package\",\n        )\n        .volta_home_file(\"layout.v2\", \"\")\n        .build();\n\n    assert_that!(p.volta(\"--version\"), execs().with_status(0));\n\n    assert!(p.package_is_installed(\"cowsay\"));\n\n    assert_that!(\n        p.exec_shim(\"cowsay\", \"hello\"),\n        execs().with_status(0).with_stdout_contains(COWSAY_HELLO)\n    );\n}\n"
  },
  {
    "path": "tests/smoke/support/mod.rs",
    "content": "pub mod temp_project;\n"
  },
  {
    "path": "tests/smoke/support/temp_project.rs",
    "content": "use node_semver::Version;\nuse std::env;\nuse std::ffi::{OsStr, OsString};\nuse std::fs::File;\nuse std::io::{Read, Write};\nuse std::path::{Path, PathBuf};\n\nuse volta_core::fs::symlink_file;\nuse volta_core::tool::Node;\n\nuse test_support::{self, ok_or_panic, paths, paths::PathExt, process::ProcessBuilder};\n\n#[derive(PartialEq, Clone)]\npub struct FileBuilder {\n    path: PathBuf,\n    contents: String,\n}\n\nimpl FileBuilder {\n    pub fn new(path: PathBuf, contents: &str) -> FileBuilder {\n        FileBuilder {\n            path,\n            contents: contents.to_string(),\n        }\n    }\n\n    pub fn build(&self) {\n        self.dirname().mkdir_p();\n\n        let mut file = File::create(&self.path)\n            .unwrap_or_else(|e| panic!(\"could not create file {}: {}\", self.path.display(), e));\n\n        ok_or_panic! { file.write_all(self.contents.as_bytes()) };\n    }\n\n    fn dirname(&self) -> &Path {\n        self.path.parent().unwrap()\n    }\n}\n\npub struct EnvVar {\n    name: String,\n    value: String,\n}\n\nimpl EnvVar {\n    pub fn new(name: &str, value: &str) -> Self {\n        EnvVar {\n            name: name.to_string(),\n            value: value.to_string(),\n        }\n    }\n}\n\n#[must_use]\npub struct TempProjectBuilder {\n    root: TempProject,\n    files: Vec<FileBuilder>,\n}\n\nimpl TempProjectBuilder {\n    /// Root of the project, ex: `/path/to/cargo/target/smoke_test/t0/foo`\n    pub fn root(&self) -> PathBuf {\n        self.root.root()\n    }\n\n    pub fn new(root: PathBuf) -> TempProjectBuilder {\n        TempProjectBuilder {\n            root: TempProject {\n                root: root.clone(),\n                path: OsString::new(),\n                env_vars: vec![],\n            },\n            files: vec![],\n        }\n    }\n\n    /// Set the package.json for the temporary project (chainable)\n    pub fn package_json(mut self, contents: &str) -> Self {\n        let package_file = package_json_file(self.root());\n        self.files.push(FileBuilder::new(package_file, contents));\n        self\n    }\n\n    /// Create a file in the project directory (chainable)\n    pub fn project_file(mut self, path: &str, contents: &str) -> Self {\n        let path = self.root().join(path);\n        self.files.push(FileBuilder::new(path, contents));\n        self\n    }\n\n    /// Create a file in the `volta_home` directory (chainable)\n    pub fn volta_home_file(mut self, path: &str, contents: &str) -> Self {\n        let path = volta_home(self.root()).join(path);\n        self.files.push(FileBuilder::new(path, contents));\n        self\n    }\n\n    /// Set an environment variable (chainable)\n    pub fn env(mut self, name: &str, value: &str) -> Self {\n        self.root.env_vars.push(EnvVar::new(name, value));\n        self\n    }\n\n    /// Create the project\n    pub fn build(mut self) -> TempProject {\n        // First, clean the temporary project directory if it already exists\n        self.rm_root();\n\n        // Create the empty directory\n        self.root.root().mkdir_p();\n\n        // make sure these directories exist and are empty\n        node_cache_dir(self.root()).ensure_empty();\n        volta_bin_dir(self.root()).ensure_empty();\n        node_inventory_dir(self.root()).ensure_empty();\n        yarn_inventory_dir(self.root()).ensure_empty();\n        package_inventory_dir(self.root()).ensure_empty();\n        node_image_root_dir(self.root()).ensure_empty();\n        yarn_image_root_dir(self.root()).ensure_empty();\n        package_image_root_dir(self.root()).ensure_empty();\n        default_toolchain_dir(self.root()).ensure_empty();\n        volta_tmp_dir(self.root()).ensure_empty();\n\n        // and these files do not exist\n        volta_file(self.root()).rm();\n        shim_executable(self.root()).rm();\n        default_hooks_file(self.root()).rm();\n        default_platform_file(self.root()).rm();\n\n        // create symlinks to shim executable for node, yarn, npm, and packages\n        ok_or_panic!(symlink_file(shim_exe(), self.root.node_exe()));\n        ok_or_panic!(symlink_file(shim_exe(), self.root.yarn_exe()));\n        ok_or_panic!(symlink_file(shim_exe(), self.root.npm_exe()));\n\n        // write files\n        for file_builder in self.files {\n            file_builder.build();\n        }\n\n        // prepend Volta bin dir to the PATH\n        let current_path = envoy::path().expect(\"Could not get current PATH\");\n        let new_path = current_path.split();\n        self.root.path = new_path\n            .prefix_entry(volta_bin_dir(self.root.root()))\n            .join()\n            .expect(\"Failed to join paths\");\n\n        let TempProjectBuilder { root, .. } = self;\n        root\n    }\n\n    fn rm_root(&self) {\n        self.root.root().rm_rf()\n    }\n}\n\n// files and dirs in the temporary project\n\nfn home_dir(root: PathBuf) -> PathBuf {\n    root.join(\"home\")\n}\nfn volta_home(root: PathBuf) -> PathBuf {\n    home_dir(root).join(\".volta\")\n}\nfn volta_file(root: PathBuf) -> PathBuf {\n    volta_home(root).join(\"volta\")\n}\nfn shim_executable(root: PathBuf) -> PathBuf {\n    volta_bin_dir(root).join(\"volta-shim\")\n}\nfn default_hooks_file(root: PathBuf) -> PathBuf {\n    volta_home(root).join(\"hooks.json\")\n}\nfn volta_tmp_dir(root: PathBuf) -> PathBuf {\n    volta_home(root).join(\"tmp\")\n}\nfn volta_bin_dir(root: PathBuf) -> PathBuf {\n    volta_home(root).join(\"bin\")\n}\nfn volta_tools_dir(root: PathBuf) -> PathBuf {\n    volta_home(root).join(\"tools\")\n}\nfn inventory_dir(root: PathBuf) -> PathBuf {\n    volta_tools_dir(root).join(\"inventory\")\n}\nfn default_toolchain_dir(root: PathBuf) -> PathBuf {\n    volta_tools_dir(root).join(\"user\")\n}\nfn image_dir(root: PathBuf) -> PathBuf {\n    volta_tools_dir(root).join(\"image\")\n}\nfn node_image_root_dir(root: PathBuf) -> PathBuf {\n    image_dir(root).join(\"node\")\n}\nfn node_image_dir(node: &str, root: PathBuf) -> PathBuf {\n    node_image_root_dir(root).join(node)\n}\nfn node_image_bin_dir(node: &str, root: PathBuf) -> PathBuf {\n    node_image_dir(node, root).join(\"bin\")\n}\nfn npm_image_root_dir(root: PathBuf) -> PathBuf {\n    image_dir(root).join(\"npm\")\n}\nfn npm_image_dir(version: &str, root: PathBuf) -> PathBuf {\n    npm_image_root_dir(root).join(version)\n}\nfn npm_image_bin_dir(version: &str, root: PathBuf) -> PathBuf {\n    npm_image_dir(version, root).join(\"bin\")\n}\nfn yarn_image_root_dir(root: PathBuf) -> PathBuf {\n    image_dir(root).join(\"yarn\")\n}\nfn yarn_image_dir(version: &str, root: PathBuf) -> PathBuf {\n    yarn_image_root_dir(root).join(version)\n}\nfn package_image_root_dir(root: PathBuf) -> PathBuf {\n    image_dir(root).join(\"packages\")\n}\nfn node_inventory_dir(root: PathBuf) -> PathBuf {\n    inventory_dir(root).join(\"node\")\n}\nfn npm_inventory_dir(root: PathBuf) -> PathBuf {\n    inventory_dir(root).join(\"npm\")\n}\nfn yarn_inventory_dir(root: PathBuf) -> PathBuf {\n    inventory_dir(root).join(\"yarn\")\n}\nfn package_inventory_dir(root: PathBuf) -> PathBuf {\n    inventory_dir(root).join(\"packages\")\n}\nfn cache_dir(root: PathBuf) -> PathBuf {\n    volta_home(root).join(\"cache\")\n}\nfn node_cache_dir(root: PathBuf) -> PathBuf {\n    cache_dir(root).join(\"node\")\n}\nfn package_json_file(mut root: PathBuf) -> PathBuf {\n    root.push(\"package.json\");\n    root\n}\nfn shim_file(name: &str, root: PathBuf) -> PathBuf {\n    volta_bin_dir(root).join(format!(\"{}{}\", name, env::consts::EXE_SUFFIX))\n}\nfn package_image_dir(name: &str, root: PathBuf) -> PathBuf {\n    image_dir(root).join(\"packages\").join(name)\n}\nfn default_platform_file(root: PathBuf) -> PathBuf {\n    default_toolchain_dir(root).join(\"platform.json\")\n}\npub fn node_distro_file_name(version: &str) -> String {\n    let version = Version::parse(version).unwrap();\n    Node::archive_filename(&version)\n}\nfn npm_distro_file_name(version: &str) -> String {\n    package_distro_file_name(\"npm\", version)\n}\nfn yarn_distro_file_name(version: &str) -> String {\n    format!(\"yarn-v{}.tar.gz\", version)\n}\nfn package_distro_file_name(name: &str, version: &str) -> String {\n    format!(\"{}-{}.tgz\", name, version)\n}\n\npub struct TempProject {\n    root: PathBuf,\n    path: OsString,\n    env_vars: Vec<EnvVar>,\n}\n\nimpl TempProject {\n    /// Root of the project, ex: `/path/to/cargo/target/integration_test/t0/foo`\n    pub fn root(&self) -> PathBuf {\n        self.root.clone()\n    }\n\n    /// Create a `ProcessBuilder` to run a program in the project.\n    /// Example:\n    ///         assert_that(\n    ///             p.process(&p.bin(\"foo\")),\n    ///             execs().with_stdout(\"bar\\n\"),\n    ///         );\n    pub fn process<T: AsRef<OsStr>>(&self, program: T) -> ProcessBuilder {\n        let mut p = test_support::process::process(program);\n        p.cwd(self.root())\n            // setup the Volta environment\n            .env(\"PATH\", &self.path)\n            .env(\"HOME\", home_dir(self.root()))\n            .env(\"VOLTA_HOME\", volta_home(self.root()))\n            .env(\"VOLTA_INSTALL_DIR\", cargo_dir())\n            .env_remove(\"VOLTA_NODE_VERSION\")\n            .env_remove(\"MSYSTEM\"); // assume cmd.exe everywhere on windows\n\n        // overrides for env vars\n        for env_var in &self.env_vars {\n            p.env(&env_var.name, &env_var.value);\n        }\n\n        p\n    }\n\n    /// Create a `ProcessBuilder` to run volta.\n    /// Arguments can be separated by spaces.\n    /// Example:\n    ///     assert_that(p.volta(\"use node 9.5\"), execs());\n    pub fn volta(&self, cmd: &str) -> ProcessBuilder {\n        let mut p = self.process(&volta_exe());\n        split_and_add_args(&mut p, cmd);\n        p\n    }\n\n    /// Create a `ProcessBuilder` to run Node.\n    pub fn node(&self, cmd: &str) -> ProcessBuilder {\n        let mut p = self.process(&self.node_exe());\n        split_and_add_args(&mut p, cmd);\n        p\n    }\n\n    pub fn node_exe(&self) -> PathBuf {\n        volta_bin_dir(self.root()).join(format!(\"node{}\", env::consts::EXE_SUFFIX))\n    }\n\n    /// Create a `ProcessBuilder` to run Yarn.\n    pub fn yarn(&self, cmd: &str) -> ProcessBuilder {\n        let mut p = self.process(&self.yarn_exe());\n        split_and_add_args(&mut p, cmd);\n        p\n    }\n\n    pub fn yarn_exe(&self) -> PathBuf {\n        volta_bin_dir(self.root()).join(format!(\"yarn{}\", env::consts::EXE_SUFFIX))\n    }\n\n    /// Create a `ProcessBuilder` to run Npm.\n    pub fn npm(&self, cmd: &str) -> ProcessBuilder {\n        let mut p = self.process(&self.npm_exe());\n        split_and_add_args(&mut p, cmd);\n        p\n    }\n\n    pub fn npm_exe(&self) -> PathBuf {\n        volta_bin_dir(self.root()).join(format!(\"npm{}\", env::consts::EXE_SUFFIX))\n    }\n\n    /// Create a `ProcessBuilder` to run a package executable.\n    pub fn exec_shim(&self, exe: &str, cmd: &str) -> ProcessBuilder {\n        let shim_file = shim_file(exe, self.root());\n        let mut p = self.process(shim_file);\n        split_and_add_args(&mut p, cmd);\n        p\n    }\n\n    /// Verify that the input Node version has been fetched.\n    pub fn node_version_is_fetched(&self, version: &str) -> bool {\n        let distro_file_name = node_distro_file_name(version);\n        let inventory_dir = node_inventory_dir(self.root());\n        inventory_dir.join(distro_file_name).exists()\n    }\n\n    /// Verify that the input Node version has been unpacked.\n    pub fn node_version_is_unpacked(&self, version: &str) -> bool {\n        let unpack_dir = node_image_bin_dir(version, self.root());\n        unpack_dir.exists()\n    }\n\n    /// Verify that the input Node version has been installed.\n    pub fn assert_node_version_is_installed(&self, version: &str) -> () {\n        let default_platform = default_platform_file(self.root());\n        let platform_contents = read_file_to_string(default_platform);\n        let json_contents: serde_json::Value =\n            serde_json::from_str(&platform_contents).expect(\"could not parse platform.json\");\n        assert_eq!(json_contents[\"node\"][\"runtime\"], version);\n    }\n\n    /// Verify that the input Yarn version has been fetched.\n    pub fn yarn_version_is_fetched(&self, version: &str) -> bool {\n        let distro_file_name = yarn_distro_file_name(version);\n        let inventory_dir = yarn_inventory_dir(self.root());\n        inventory_dir.join(distro_file_name).exists()\n    }\n\n    /// Verify that the input Yarn version has been unpacked.\n    pub fn yarn_version_is_unpacked(&self, version: &str) -> bool {\n        let unpack_dir = yarn_image_dir(version, self.root());\n        unpack_dir.exists()\n    }\n\n    /// Verify that the input Yarn version has been installed.\n    pub fn assert_yarn_version_is_installed(&self, version: &str) -> () {\n        let default_platform = default_platform_file(self.root());\n        let platform_contents = read_file_to_string(default_platform);\n        let json_contents: serde_json::Value =\n            serde_json::from_str(&platform_contents).expect(\"could not parse platform.json\");\n        assert_eq!(json_contents[\"yarn\"], version);\n    }\n\n    /// Verify that the input Npm version has been fetched.\n    pub fn npm_version_is_fetched(&self, version: &str) -> bool {\n        let distro_file_name = npm_distro_file_name(version);\n        let inventory_dir = npm_inventory_dir(self.root());\n        inventory_dir.join(distro_file_name).exists()\n    }\n\n    /// Verify that the input Npm version has been unpacked.\n    pub fn npm_version_is_unpacked(&self, version: &str) -> bool {\n        npm_image_bin_dir(version, self.root()).exists()\n    }\n\n    /// Verify that the input Npm version has been installed.\n    pub fn assert_npm_version_is_installed(&self, version: &str) -> () {\n        let default_platform = default_platform_file(self.root());\n        let platform_contents = read_file_to_string(default_platform);\n        let json_contents: serde_json::Value =\n            serde_json::from_str(&platform_contents).expect(\"could not parse platform.json\");\n        assert_eq!(json_contents[\"node\"][\"npm\"], version);\n    }\n\n    /// Verify that the input package has been installed\n    pub fn package_is_installed(&self, name: &str) -> bool {\n        let install_dir = package_image_dir(name, self.root());\n        install_dir.exists()\n    }\n\n    /// Verify that the input package version has been fetched.\n    pub fn shim_exists(&self, name: &str) -> bool {\n        shim_file(name, self.root()).exists()\n    }\n\n    /// Verify that a given path in the project directory exists\n    pub fn project_path_exists(&self, path: &str) -> bool {\n        self.root().join(path).exists()\n    }\n}\n\nimpl Drop for TempProject {\n    fn drop(&mut self) {\n        self.root().rm_rf();\n    }\n}\n\n// Generates a temporary project environment\npub fn temp_project() -> TempProjectBuilder {\n    TempProjectBuilder::new(paths::root().join(\"temp-project\"))\n}\n\n// Path to compiled executables\npub fn cargo_dir() -> PathBuf {\n    env::var_os(\"CARGO_BIN_PATH\")\n        .map(PathBuf::from)\n        .or_else(|| {\n            env::current_exe().ok().map(|mut path| {\n                path.pop();\n                if path.ends_with(\"deps\") {\n                    path.pop();\n                }\n                path\n            })\n        })\n        .unwrap_or_else(|| panic!(\"CARGO_BIN_PATH wasn't set. Cannot continue running test\"))\n}\n\nfn volta_exe() -> PathBuf {\n    cargo_dir().join(format!(\"volta{}\", env::consts::EXE_SUFFIX))\n}\n\nfn shim_exe() -> PathBuf {\n    cargo_dir().join(format!(\"volta-shim{}\", env::consts::EXE_SUFFIX))\n}\n\nfn split_and_add_args(p: &mut ProcessBuilder, s: &str) {\n    for arg in s.split_whitespace() {\n        if arg.contains('\"') || arg.contains('\\'') {\n            panic!(\"shell-style argument parsing is not supported\")\n        }\n        p.arg(arg);\n    }\n}\n\nfn read_file_to_string(file_path: PathBuf) -> String {\n    let mut contents = String::new();\n    let mut file = ok_or_panic! { File::open(file_path) };\n    ok_or_panic! { file.read_to_string(&mut contents) };\n    contents\n}\n"
  },
  {
    "path": "tests/smoke/volta_fetch.rs",
    "content": "use crate::support::temp_project::temp_project;\n\nuse hamcrest2::assert_that;\nuse hamcrest2::prelude::*;\nuse test_support::matchers::execs;\n\n#[test]\nfn fetch_node() {\n    let p = temp_project().build();\n\n    assert_that!(p.volta(\"fetch node@14.17.6\"), execs().with_status(0));\n    assert!(p.node_version_is_fetched(\"14.17.6\"));\n    assert!(p.node_version_is_unpacked(\"14.17.6\"));\n}\n\n#[test]\nfn fetch_yarn_1() {\n    let p = temp_project().build();\n\n    assert_that!(p.volta(\"fetch yarn@1.22.1\"), execs().with_status(0));\n    assert!(p.yarn_version_is_fetched(\"1.22.1\"));\n    assert!(p.yarn_version_is_unpacked(\"1.22.1\"));\n}\n\n#[test]\nfn fetch_yarn_3() {\n    let p = temp_project().build();\n\n    assert_that!(p.volta(\"fetch yarn@3.2.0\"), execs().with_status(0));\n    assert!(p.yarn_version_is_fetched(\"3.2.0\"));\n    assert!(p.yarn_version_is_unpacked(\"3.2.0\"));\n}\n\n#[test]\nfn fetch_npm() {\n    let p = temp_project().build();\n\n    assert_that!(p.volta(\"fetch npm@8.3.1\"), execs().with_status(0));\n    assert!(p.npm_version_is_fetched(\"8.3.1\"));\n    assert!(p.npm_version_is_unpacked(\"8.3.1\"));\n}\n"
  },
  {
    "path": "tests/smoke/volta_install.rs",
    "content": "use std::thread;\n\nuse crate::support::temp_project::temp_project;\nuse hamcrest2::assert_that;\nuse hamcrest2::prelude::*;\nuse test_support::matchers::execs;\n\n#[test]\nfn install_node() {\n    let p = temp_project().build();\n\n    assert_that!(p.volta(\"install node@14.15.4\"), execs().with_status(0));\n\n    assert_that!(\n        p.node(\"--version\"),\n        execs().with_status(0).with_stdout_contains(\"v14.15.4\")\n    );\n\n    assert!(p.node_version_is_fetched(\"14.15.4\"));\n    assert!(p.node_version_is_unpacked(\"14.15.4\"));\n    p.assert_node_version_is_installed(\"14.15.4\");\n}\n\n#[test]\nfn install_node_lts() {\n    let p = temp_project().build();\n\n    assert_that!(p.volta(\"install node@lts\"), execs().with_status(0));\n\n    assert_that!(p.node(\"--version\"), execs().with_status(0));\n}\n\n#[test]\nfn install_node_concurrent() {\n    let p = temp_project().build();\n\n    let install = p.volta(\"install node@14.17.2\");\n    let run = p.node(\"--version\");\n\n    let concurrent_thread = thread::spawn(move || {\n        assert_that!(install, execs().with_status(0));\n        assert_that!(run, execs().with_status(0));\n    });\n\n    assert_that!(p.volta(\"install node@14.17.2\"), execs().with_status(0));\n    assert_that!(p.node(\"--version\"), execs().with_status(0));\n\n    assert!(concurrent_thread.join().is_ok());\n}\n\n#[test]\nfn install_yarn() {\n    let p = temp_project().build();\n\n    assert_that!(p.volta(\"install node@14.15.2\"), execs().with_status(0));\n    assert_that!(p.volta(\"install yarn@1.22.1\"), execs().with_status(0));\n\n    assert_that!(\n        p.yarn(\"--version\"),\n        execs().with_status(0).with_stdout_contains(\"1.22.1\")\n    );\n\n    assert!(p.yarn_version_is_fetched(\"1.22.1\"));\n    assert!(p.yarn_version_is_unpacked(\"1.22.1\"));\n    p.assert_yarn_version_is_installed(\"1.22.1\");\n}\n\n#[test]\nfn install_old_yarn() {\n    let p = temp_project().build();\n\n    assert_that!(p.volta(\"install node@14.11.0\"), execs().with_status(0));\n    // Yarn 1.9.2 is old enough that it is no longer on the first page of results from the GitHub API\n    assert_that!(p.volta(\"install yarn@1.9.2\"), execs().with_status(0));\n\n    assert_that!(\n        p.yarn(\"--version\"),\n        execs().with_status(0).with_stdout_contains(\"1.9.2\")\n    );\n\n    assert!(p.yarn_version_is_fetched(\"1.9.2\"));\n    assert!(p.yarn_version_is_unpacked(\"1.9.2\"));\n    p.assert_yarn_version_is_installed(\"1.9.2\");\n}\n\n#[test]\nfn install_yarn_concurrent() {\n    let p = temp_project().build();\n\n    assert_that!(p.volta(\"install node@14.19.0\"), execs().with_status(0));\n\n    let install = p.volta(\"install yarn@1.17.0\");\n    let run = p.yarn(\"--version\");\n\n    let concurrent_thread = thread::spawn(move || {\n        assert_that!(install, execs().with_status(0));\n        assert_that!(run, execs().with_status(0));\n    });\n\n    assert_that!(p.volta(\"install yarn@1.17.0\"), execs().with_status(0));\n    assert_that!(p.yarn(\"--version\"), execs().with_status(0));\n\n    assert!(concurrent_thread.join().is_ok());\n}\n\n#[test]\nfn install_npm() {\n    let p = temp_project().build();\n\n    // node 17.6.0 is bundled with npm 8.5.1\n    assert_that!(p.volta(\"install node@17.6.0\"), execs().with_status(0));\n    assert_that!(\n        p.npm(\"--version\"),\n        execs().with_status(0).with_stdout_contains(\"8.5.1\")\n    );\n\n    // install npm 6.8.0 and verify that is installed correctly\n    assert_that!(p.volta(\"install npm@8.5.5\"), execs().with_status(0));\n    assert!(p.npm_version_is_fetched(\"8.5.5\"));\n    assert!(p.npm_version_is_unpacked(\"8.5.5\"));\n    p.assert_npm_version_is_installed(\"8.5.5\");\n\n    assert_that!(\n        p.npm(\"--version\"),\n        execs().with_status(0).with_stdout_contains(\"8.5.5\")\n    );\n}\n\n#[test]\nfn install_npm_concurrent() {\n    let p = temp_project().build();\n\n    assert_that!(p.volta(\"install node@14.5.0\"), execs().with_status(0));\n\n    let install = p.volta(\"install npm@6.14.2\");\n    let run = p.npm(\"--version\");\n\n    let concurrent_thread = thread::spawn(move || {\n        assert_that!(install, execs().with_status(0));\n        assert_that!(run, execs().with_status(0));\n    });\n\n    assert_that!(p.volta(\"install npm@6.14.2\"), execs().with_status(0));\n    assert_that!(p.npm(\"--version\"), execs().with_status(0));\n\n    assert!(concurrent_thread.join().is_ok());\n}\n\nconst COWSAY_HELLO: &'static str = r#\" _______\n< hello >\n -------\n        \\   ^__^\n         \\  (oo)\\_______\n            (__)\\       )\\/\\\n                ||----w |\n                ||     ||\"#;\n\n#[test]\nfn install_package() {\n    let p = temp_project().build();\n\n    // have to install node first, because we need npm\n    assert_that!(p.volta(\"install node@14.11.0\"), execs().with_status(0));\n\n    assert_that!(p.volta(\"install cowsay@1.4.0\"), execs().with_status(0));\n    assert!(p.shim_exists(\"cowsay\"));\n    assert!(p.package_is_installed(\"cowsay\"));\n\n    assert_that!(\n        p.exec_shim(\"cowsay\", \"hello\"),\n        execs().with_status(0).with_stdout_contains(COWSAY_HELLO)\n    );\n}\n\n#[test]\nfn install_package_concurrent() {\n    let p = temp_project().build();\n\n    assert_that!(p.volta(\"install node@14.14.0\"), execs().with_status(0));\n\n    let install = p.volta(\"install cowsay@1.3.0\");\n    let run = p.exec_shim(\"cowsay\", \"hello\");\n\n    let concurrent_thread = thread::spawn(move || {\n        assert_that!(install, execs().with_status(0));\n        assert_that!(run, execs().with_status(0));\n    });\n\n    assert_that!(p.volta(\"install cowsay@1.3.0\"), execs().with_status(0));\n    assert_that!(p.exec_shim(\"cowsay\", \"hello\"), execs().with_status(0));\n\n    assert!(concurrent_thread.join().is_ok());\n}\n\n#[test]\nfn install_scoped_package() {\n    let p = temp_project().build();\n\n    // have to install node first, because we need npm\n    assert_that!(p.volta(\"install node@14.15.0\"), execs().with_status(0));\n\n    assert_that!(p.volta(\"install @wdio/cli@5.12.4\"), execs().with_status(0));\n    assert!(p.shim_exists(\"wdio\"));\n    assert!(p.package_is_installed(\"@wdio/cli\"));\n\n    assert_that!(\n        p.exec_shim(\"wdio\", \"--version\"),\n        execs().with_status(0).with_stdout_contains(\"5.12.4\")\n    );\n}\n\n#[test]\nfn install_package_tag_version() {\n    let p = temp_project().build();\n\n    // have to install node first, because we need npm\n    assert_that!(p.volta(\"install node@14.8.0\"), execs().with_status(0));\n\n    assert_that!(p.volta(\"install elm@elm0.19.0\"), execs().with_status(0));\n    assert!(p.shim_exists(\"elm\"));\n\n    assert_that!(\n        p.exec_shim(\"elm\", \"--version\"),\n        execs().with_status(0).with_stdout_contains(\"0.19.0\")\n    );\n}\n"
  },
  {
    "path": "tests/smoke/volta_run.rs",
    "content": "use crate::support::temp_project::temp_project;\n\nuse hamcrest2::assert_that;\nuse hamcrest2::prelude::*;\nuse test_support::matchers::execs;\n\n// Note: Node 14.11.0 is bundled with npm 6.14.8\nconst PACKAGE_JSON: &str = r#\"{\n    \"name\": \"test-package\",\n    \"volta\": {\n        \"node\": \"14.11.0\",\n        \"npm\": \"6.14.15\",\n        \"yarn\": \"1.22.10\"\n    }\n}\"#;\n\n#[test]\nfn run_node() {\n    let p = temp_project().build();\n\n    assert_that!(\n        p.volta(\"run --node 14.16.0 node --version\"),\n        execs().with_status(0).with_stdout_contains(\"v14.16.0\")\n    );\n}\n\n#[test]\nfn run_npm() {\n    let p = temp_project().build();\n\n    assert_that!(\n        p.volta(\"run --node 14.14.0 --npm 6.14.16 npm --version\"),\n        execs().with_status(0).with_stdout_contains(\"6.14.16\")\n    )\n}\n\n#[test]\nfn run_yarn_1() {\n    let p = temp_project().build();\n\n    assert_that!(\n        p.volta(\"run --node 14.16.1 --yarn 1.22.0 yarn --version\"),\n        execs().with_status(0).with_stdout_contains(\"1.22.0\")\n    );\n}\n\n#[test]\nfn run_yarn_3() {\n    let p = temp_project().build();\n\n    assert_that!(\n        p.volta(\"run --node 16.14.1 --yarn 3.1.1 yarn --version\"),\n        execs().with_status(0).with_stdout_contains(\"3.1.1\")\n    );\n}\n\n#[test]\nfn inherits_project_platform() {\n    let p = temp_project().package_json(PACKAGE_JSON).build();\n\n    assert_that!(\n        p.volta(\"run --yarn 1.21.0 yarn --version\"),\n        execs().with_status(0).with_stdout_contains(\"1.21.0\")\n    );\n}\n\n#[test]\nfn run_environment() {\n    let p = temp_project().build();\n\n    assert_that!(\n        p.volta(\"run --node 14.15.3 --env VOLTA_SMOKE_1234=hello node -e console.log(process.env.VOLTA_SMOKE_1234)\"),\n        execs().with_status(0).with_stdout_contains(\"hello\")\n    );\n}\n"
  },
  {
    "path": "volta.iml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<module type=\"RUST_MODULE\" version=\"4\">\n  <component name=\"NewModuleRootManager\" inherit-compiler-output=\"true\">\n    <exclude-output />\n    <content url=\"file://$MODULE_DIR$\">\n      <sourceFolder url=\"file://$MODULE_DIR$/src\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/examples\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/tests\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/benches\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/crates/volta-core/src\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/crates/volta-core/examples\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/crates/volta-core/tests\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/crates/volta-core/benches\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/crates/archive/src\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/crates/archive/examples\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/crates/archive/tests\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/crates/archive/benches\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/crates/volta-fail-derive/src\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/crates/volta-fail-derive/examples\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/crates/volta-fail-derive/tests\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/crates/volta-fail-derive/benches\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/crates/test-support/src\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/crates/test-support/examples\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/crates/test-support/tests\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/crates/test-support/benches\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/crates/volta-fail/src\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/crates/volta-fail/examples\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/crates/volta-fail/tests\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/crates/volta-fail/benches\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/crates/progress-read/src\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/crates/progress-read/examples\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/crates/progress-read/tests\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/crates/progress-read/benches\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/crates/headers-011/src\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/crates/headers-011/examples\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/crates/headers-011/tests\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/crates/headers-011/benches\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/crates/validate-npm-package-name/src\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/crates/validate-npm-package-name/examples\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/crates/validate-npm-package-name/tests\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/crates/validate-npm-package-name/benches\" isTestSource=\"true\" />\n      <excludeFolder url=\"file://$MODULE_DIR$/crates/archive/target\" />\n      <excludeFolder url=\"file://$MODULE_DIR$/crates/headers-011/target\" />\n      <excludeFolder url=\"file://$MODULE_DIR$/crates/progress-read/target\" />\n      <excludeFolder url=\"file://$MODULE_DIR$/crates/test-support/target\" />\n      <excludeFolder url=\"file://$MODULE_DIR$/crates/validate-npm-package-name/target\" />\n      <excludeFolder url=\"file://$MODULE_DIR$/crates/volta-core/target\" />\n      <excludeFolder url=\"file://$MODULE_DIR$/crates/volta-fail-derive/target\" />\n      <excludeFolder url=\"file://$MODULE_DIR$/crates/volta-fail/target\" />\n      <excludeFolder url=\"file://$MODULE_DIR$/target\" />\n    </content>\n    <orderEntry type=\"inheritedJdk\" />\n    <orderEntry type=\"sourceFolder\" forTests=\"false\" />\n  </component>\n</module>"
  },
  {
    "path": "wix/main.wxs",
    "content": "<?xml version='1.0' encoding='windows-1252'?>\n<!--\n  Copyright (C) 2017 Christopher R. Field.\n\n  Licensed under the Apache License, Version 2.0 (the \"License\");\n  you may not use this file except in compliance with the License.\n  You may obtain a copy of the License at\n\n  http://www.apache.org/licenses/LICENSE-2.0\n\n  Unless required by applicable law or agreed to in writing, software\n  distributed under the License is distributed on an \"AS IS\" BASIS,\n  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  See the License for the specific language governing permissions and\n  limitations under the License.\n-->\n\n<!--\n  Please do not remove these pre-processor If-Else blocks. These are used with\n  the `cargo wix` subcommand to automatically determine the installation\n  destination for 32-bit versus 64-bit installers. Removal of these lines will\n  cause installation errors.\n-->\n<?if $(sys.BUILDARCH) = x64 or $(sys.BUILDARCH) = arm64 ?>\n    <?define PlatformProgramFilesFolder = \"ProgramFiles64Folder\" ?>\n<?else ?>\n  <?define PlatformProgramFilesFolder = \"ProgramFilesFolder\" ?>\n<?endif ?>\n\n<Wix xmlns='http://schemas.microsoft.com/wix/2006/wi'>\n\n    <Product\n        Id='*'\n        Name='Volta'\n        UpgradeCode='42A39E14-335A-4464-AA37-17FDA38FA377'\n        Manufacturer='The Volta Maintainers'\n        Language='1033'\n        Codepage='1252'\n        Version='$(var.Version)'>\n\n        <Package Id='*'\n            Keywords='Installer'\n            Description='The JavaScript Launcher'\n            Manufacturer='The Volta Maintainers'\n            InstallerVersion='500'\n            Languages='1033'\n            Compressed='yes'\n            InstallScope='perMachine'\n            SummaryCodepage='1252'/>\n\n        <MajorUpgrade\n            Schedule='afterInstallInitialize'\n            DowngradeErrorMessage='A newer version of [ProductName] is already installed. Setup will now exit.'/>\n\n        <Media Id='1' Cabinet='media1.cab' EmbedCab='yes' CompressionLevel='high'/>\n\n        <Directory Id='TARGETDIR' Name='SourceDir'>\n            <Directory Id='$(var.PlatformProgramFilesFolder)' Name='PFiles'>\n                <Directory Id='INSTALLDIR' Name='Volta'>\n                </Directory>\n            </Directory>\n        </Directory>\n\n        <ComponentGroup Id='Binaries' Directory='INSTALLDIR'>\n            <Component Id='voltaBinary' Guid='*'>\n                <File\n                    Id='voltaEXE'\n                    Name='volta.exe'\n                    DiskId='1'\n                    Source='$(var.CargoTargetBinDir)\\volta.exe'\n                    KeyPath='yes'/>\n                <Environment\n                    Id='INSTALLPATH'\n                    Name='PATH'\n                    Value='[INSTALLDIR]'\n                    Permanent='no'\n                    Part='first'\n                    Action='set'\n                    System='yes' />\n            </Component>\n            <Component Id='shimBinary' Guid='*'>\n                <File\n                    Id='voltashimEXE'\n                    Name='volta-shim.exe'\n                    DiskId='1'\n                    Source='$(var.CargoTargetBinDir)\\volta-shim.exe'\n                    KeyPath='yes'/>\n            </Component>\n            <Component Id='migrateBinary' Guid='*'>\n                <File\n                    Id='voltamigrateEXE'\n                    Name='volta-migrate.exe'\n                    DiskId='1'\n                    Source='$(var.CargoTargetBinDir)\\volta-migrate.exe'\n                    KeyPath='yes'/>\n            </Component>\n            <Component Id='nodeBinary' Guid='*'>\n                <File\n                    Id='nodeEXE'\n                    Name='node.exe'\n                    DiskId='1'\n                    Source='$(var.CargoTargetBinDir)\\volta-shim.exe'\n                    KeyPath='yes'/>\n            </Component>\n            <Component Id='npmBinary' Guid='*'>\n                <File\n                    Id='npmEXE'\n                    Name='npm.exe'\n                    DiskId='1'\n                    Source='$(var.CargoTargetBinDir)\\volta-shim.exe'\n                    KeyPath='yes'/>\n            </Component>\n            <Component Id='npmScript' Guid='*'>\n                <File\n                    Id='npmCMD'\n                    Name='npm.cmd'\n                    DiskId='1'\n                    Source='wix\\shim.cmd'\n                    KeyPath='yes'/>\n            </Component>\n            <Component Id='npxBinary' Guid='*'>\n                <File\n                    Id='npxEXE'\n                    Name='npx.exe'\n                    DiskId='1'\n                    Source='$(var.CargoTargetBinDir)\\volta-shim.exe'\n                    KeyPath='yes'/>\n            </Component>\n            <Component Id='npxScript' Guid='*'>\n                <File\n                    Id='npxCMD'\n                    Name='npx.cmd'\n                    DiskId='1'\n                    Source='wix\\shim.cmd'\n                    KeyPath='yes'/>\n            </Component>\n            <Component Id='pnpmBinary' Guid='*'>\n                <File\n                    Id='pnpmEXE'\n                    Name='pnpm.exe'\n                    DiskId='1'\n                    Source='$(var.CargoTargetBinDir)\\volta-shim.exe'\n                    KeyPath='yes'/>\n            </Component>\n            <Component Id='pnpmScript' Guid='*'>\n                <File\n                    Id='pnpmCMD'\n                    Name='pnpm.cmd'\n                    DiskId='1'\n                    Source='wix\\shim.cmd'\n                    KeyPath='yes'/>\n            </Component>\n            <Component Id='yarnBinary' Guid='*'>\n                <File\n                    Id='yarnEXE'\n                    Name='yarn.exe'\n                    DiskId='1'\n                    Source='$(var.CargoTargetBinDir)\\volta-shim.exe'\n                    KeyPath='yes'/>\n            </Component>\n            <Component Id='yarnScript' Guid='*'>\n                <File\n                    Id='yarnCMD'\n                    Name='yarn.cmd'\n                    DiskId='1'\n                    Source='wix\\shim.cmd'\n                    KeyPath='yes'/>\n            </Component>\n            <Component Id='yarnPkgBinary' Guid='*'>\n                <File\n                    Id='yarnpgkEXE'\n                    Name='yarnpkg.exe'\n                    DiskId='1'\n                    Source='$(var.CargoTargetBinDir)\\volta-shim.exe'\n                    KeyPath='yes'/>\n            </Component>\n            <Component Id='yarnPkgScript' Guid='*'>\n                <File\n                    Id='yarnpkgCMD'\n                    Name='yarnpkg.cmd'\n                    DiskId='1'\n                    Source='wix\\shim.cmd'\n                    KeyPath='yes'/>\n            </Component>\n        </ComponentGroup>\n\n        <Feature Id='MainProgram'>\n            <ComponentGroupRef Id='Binaries'/>\n        </Feature>\n\n        <SetProperty Id='ARPINSTALLLOCATION' Value='[APPLICATIONFOLDER]' After='CostFinalize'/>\n\n        <CustomAction\n            Id=\"VoltaSetup\"\n            FileKey=\"voltaEXE\"\n            ExeCommand=\"setup\"\n            Execute=\"deferred\"\n            Return=\"ignore\" />\n\n        <InstallExecuteSequence>\n            <Custom Action=\"VoltaSetup\" Before=\"InstallFinalize\" />\n        </InstallExecuteSequence>\n\n        <!--\n          Uncomment the following `Icon` and `Property` tags to change the product icon.\n\n          The product icon is the graphic that appears in the Add/Remove\n          Programs control panel for the application.\n        -->\n        <Icon Id='ProductICO' SourceFile='wix\\volta.ico'/>\n        <Property Id='ARPPRODUCTICON' Value='ProductICO' />\n\n        <Property Id='ARPHELPLINK' Value='https://github.com/volta-cli/volta'/>\n        <Property Id='LicenseAccepted' Value='1'/>\n\n        <UI>\n            <UIRef Id='WixUI_FeatureTree' />\n\n            <Publish Dialog='LicenseAgreementDlg' Control='Next' Event='NewDialog' Value='VerifyReadyDlg' Order='2'>1</Publish>\n            <Publish Dialog='VerifyReadyDlg' Control='Back' Event='NewDialog' Value='LicenseAgreementDlg' Order='2'>1</Publish>\n        </UI>\n\n        <!--\n          Enabling the EULA dialog in the installer requires uncommenting\n          the following `WixUILicenseRTF` tag and changing the `Value`\n          attribute.\n        -->\n        <WixVariable Id='WixUILicenseRtf' Value='wix\\License.rtf'/>\n\n        <!--\n          Uncomment the next `WixVaraible` tag to customize the installer's\n          Graphical User Interface (GUI) and add a custom banner image across\n          the top of each screen. See the WiX Toolset documentation for details\n          about customization.\n\n          The banner BMP dimensions are 493 x 58 pixels.\n        -->\n        <!--<WixVariable Id='WixUIBannerBmp' Value='wix\\Banner.bmp'/>-->\n\n        \n        <!--\n          Uncomment the next `WixVariable` tag to customize the installer's\n          Graphical User Interface (GUI) and add a custom image to the first\n          dialog, or screen. See the WiX Toolset documentation for details about\n          customization.\n\n          The dialog BMP dimensions are 493 x 312 pixels.\n        -->\n        <!--<WixVariable Id='WixUIDialogBmp' Value='wix\\Dialog.bmp'/>-->\n\n    </Product>\n</Wix>\n"
  },
  {
    "path": "wix/shim.cmd",
    "content": "@echo off\r\n\"%~dpn0.exe\" %*\r\n"
  }
]