[
  {
    "path": ".gitattributes",
    "content": "/.gitattributes export-ignore\n/.github export-ignore\n/.gitignore export-ignore\n/.php_cs export-ignore\n/.travis.yml export-ignore\n/CODE_OF_CONDUCT.md export-ignore\n/CONTRIBUTING.md export-ignore\n/Makefile export-ignore\n/box.json.dist export-ignore\n/composer.json export-ignore\n/depfile.yml export-ignore\n/infection.json.dist export-ignore\n/phpunit.xml.dist export-ignore\n/tests export-ignore\n/tools export-ignore\n/scoper.inc.php export-ignore\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: [jakzal]\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "<!--\nPlease read the CONTRIBUTING.md to learn about contributing to this project.\n\nPlease also note that this project is released with a Contributor Code of Conduct.\nBy participating in this project you agree to abide by its terms.\nThe Code of Conduct can be found in CODE_OF_CONDUCT.md.\n-->\n\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Build\n\non:\n    push:\n        branches: [master]\n    pull_request:\n    release:\n        types: [created]\n    schedule:\n        -   cron: '0 4 * * *'\n\nenv:\n    GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}\n    # for phive\n    GITHUB_AUTH_TOKEN: ${{ secrets.ACCESS_TOKEN }}\n\njobs:\n    tests:\n        runs-on: ubuntu-latest\n        name: Build and test\n        strategy:\n            matrix:\n                php: [8.2, 8.3, 8.4, 8.5]\n                deps: [high]\n                include:\n                    -   php: 8.2\n                        deps: low\n\n        steps:\n            -   uses: actions/checkout@v4\n\n            -   name: Setup PHP\n                uses: shivammathur/setup-php@v2\n                with:\n                    php-version: \"${{ matrix.php }}\"\n                    tools: composer\n                    ini-values: \"phar.readonly=0\"\n                    coverage: pcov\n\n            -   if: matrix.deps == 'high'\n                run: make update test package package-devkit\n\n            -   if: matrix.deps == 'low'\n                run: make update-min test-min\n\n            -   uses: actions/upload-artifact@v4\n                if: matrix.php == '8.2' && matrix.deps == 'high'\n                with:\n                    name: toolbox.phar\n                    path: build/toolbox.phar\n\n            -   uses: actions/upload-artifact@v4\n                if: matrix.php == '8.2' && matrix.deps == 'high'\n                with:\n                    name: devkit.phar\n                    path: build/devkit.phar\n\n    integration-tests:\n        runs-on: ubuntu-latest\n        name: Run integration tests\n        needs: tests\n        strategy:\n            matrix:\n                php: [8.2, 8.3, 8.4, 8.5]\n\n        steps:\n            -   uses: actions/checkout@v4\n\n            -   name: Setup PHP\n                uses: shivammathur/setup-php@v2\n                with:\n                    php-version: \"${{ matrix.php }}\"\n                    tools: composer\n                    ini-values: \"phar.readonly=0\"\n                    coverage: none\n                    extensions: bz2, zip\n\n            -   uses: actions/download-artifact@v4\n                with:\n                    name: toolbox.phar\n                    path: build/\n            -   run: make test-integration\n                env:\n                    GITHUB_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n    publish-phars:\n        runs-on: ubuntu-latest\n        name: Publish PHARs\n        needs: tests\n        if: github.event_name == 'release'\n        steps:\n            -   uses: actions/download-artifact@v4\n                with:\n                    name: toolbox.phar\n                    path: .\n            -   uses: actions/download-artifact@v4\n                with:\n                    name: devkit.phar\n                    path: .\n            -   name: Upload toolbox.phar\n                run: gh release upload ${{ github.event.release.tag_name }} toolbox.phar --clobber --repo github.com/jakzal/toolbox\n                env:\n                    GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}\n            -   name: Upload devkit.phar\n                run: gh release upload ${{ github.event.release.tag_name }} devkit.phar --clobber --repo github.com/jakzal/toolbox\n                env:\n                    GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}\n\n"
  },
  {
    "path": ".github/workflows/publish-website.yml",
    "content": "name: Publish the website\n\non:\n    push:\n        branches: [master]\n    release:\n        types: [created]\n\njobs:\n    publish-website:\n        runs-on: ubuntu-latest\n        name: Build and publish\n        steps:\n            -   uses: actions/checkout@v4\n                with:\n                    persist-credentials: false\n\n            -   name: Setup PHP\n                uses: shivammathur/setup-php@v2\n                with:\n                    php-version: \"8.2\"\n                    ini-values: \"phar.readonly=0\"\n\n            -   name: Build the website\n                run: make package-devkit website\n\n            -   name: Publish the website\n                uses: JamesIves/github-pages-deploy-action@v4\n                with:\n                    ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}\n                    BRANCH: gh-pages\n                    FOLDER: build/website\n"
  },
  {
    "path": ".github/workflows/update-phars.yml",
    "content": "name: Update PHARs\n\non:\n    schedule:\n        -   cron: '30 3 * * *'\njobs:\n    update-phars:\n        runs-on: ubuntu-latest\n        name: Create a PR\n        steps:\n            -   uses: actions/checkout@v4\n\n            -   name: Setup PHP\n                uses: shivammathur/setup-php@v2\n                with:\n                    php-version: \"8.2\"\n                    ini-values: \"phar.readonly=0\"\n\n            -   name: Configure git\n                run: git config user.email 'jakub@zalas.pl' && git config user.name 'Jakub Zalas'\n\n            -   name: Install dependencies\n                run: sudo apt-get update && sudo apt-get install -y hub\n\n            -   name: Update PHARs\n                run: make package-devkit update-phars\n\n            -   name: Send a Pull Request\n                run: \"git diff --exit-code master -- resources/ || hub pull-request -h tools-update -a jakzal -m 'Update tools' -m '' -m ':robot: This pull request was automagically sent from Github'\"\n                env:\n                    GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "/composer.lock\n/.deptrac.cache\n/.phpunit.result.cache\n/.php-cs-fixer.php\n/.php-cs-fixer.cache\n/build/\n/vendor/\n"
  },
  {
    "path": ".php-cs-fixer.dist.php",
    "content": "<?php\n\n$finder = PhpCsFixer\\Finder::create()\n    ->in(['src', 'tests'])\n;\n\nreturn (new PhpCsFixer\\Config())\n    ->setRules([\n        '@PSR2' => true,\n        'array_syntax' => ['syntax' => 'short'],\n        'blank_line_before_statement' => true,\n        'concat_space' => ['spacing' => 'none'],\n        'declare_strict_types' => true,\n        'native_function_invocation' => ['include' => ['@internal']],\n        'no_empty_comment' => true,\n        'no_empty_phpdoc' => true,\n        'no_empty_statement' => true,\n        'no_extra_blank_lines' => true,\n        'no_leading_import_slash' => true,\n        'no_leading_namespace_whitespace' => true,\n        'no_unused_imports' => true,\n        'no_useless_else' => true,\n        'ordered_class_elements' => true,\n        'ordered_imports' => true,\n        'phpdoc_add_missing_param_annotation' => ['only_untyped' => true],\n        'protected_to_private' => true,\n        'strict_comparison' => true,\n        'ternary_operator_spaces' => true,\n        'ternary_to_null_coalescing' => true,\n        'yoda_style' => true,\n    ])\n    ->setFinder($finder)\n;\n"
  },
  {
    "path": ".scrutinizer.yml",
    "content": "inherit: true\n\nbuild:\n  environment:\n    php:\n      version: 8.2\n    variables:\n      XDEBUG_MODE: coverage\n  tests:\n    override:\n    - make phpunit\n  project_setup:\n  nodes:\n    coverage:\n      tests:\n        override:\n        - command: make phpunit\n          coverage:\n            file: build/coverage.xml\n            format: clover\nfilter:\n  paths: [src/*]\n\nbuild_failure_conditions:\n- 'elements.rating(<= B).new.exists'\n- 'issues.label(\"coding-style\").new.exists'\n- 'issues.severity(>= MAJOR).new.exists'\n\nchecks:\n  php: true\n\ntools:\n  php_code_sniffer: false\n  php_cs_fixer: { config: { level: psr2 } }\n  external_code_coverage: false\n  php_code_coverage: true\n  php_changetracking: true\n  php_sim: true\n  php_mess_detector: true\n  php_pdepend: true\n  php_analyzer: true\n  sensiolabs_security_checker: true\n\ncoding_style:\n  php:\n    spaces:\n      within:\n        brackets: false\n      before_parentheses:\n        closure_definition: true\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\nadvances\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 oss@zalas.pl. 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 [http://contributor-covenant.org/version/1/4][version]\n\n[homepage]: http://contributor-covenant.org\n[version]: http://contributor-covenant.org/version/1/4/\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\nWhen contributing to this repository send a new pull request.\nIf your change is big or complex, or you simply want to suggest an improvement,\nplease discuss the change you wish to make via an issue.\n\nPlease note we have a [code of conduct](CODE_OF_CONDUCT.md). Please follow it in all your interactions with the project.\n\n## Pull Request Process\n\n* Provide good commit messages describing what you've done.\n* Provide tests for any code you write.\n* Make sure all tests are passing.\n* Prefer `phive`, `phar` or `composer-bin-plugin` installation over `composer global` installations to avoid dependency conflicts.\n* Update the `resources/*.json` files with any new tools'd like to add.\n* Update `README.md` with any new tools you added (`php bin/devkit.php update:readme`).\n\n## Adding a new tool\n\nTo add support for a new tool, add it to the list in one of the `json` files in the `resources/` folder:\n\n```json\n{\n  \"name\": \"behat\",\n  \"summary\": \"Helps to test business expectations\",\n  \"website\": \"http://behat.org/\",\n  \"command\": {\n    \"composer-bin-plugin\": {\n      \"package\": \"behat/behat\",\n      \"namespace\": \"behat\"\n    }\n  },\n  \"test\": \"behat --version\",\n  \"tags\": [\"testing\", \"test\", \"bdd\"]\n}\n```\n\nEach tool should have the following properties specified:\n\n* `name` - name of the tool, most of the time the name of executable;\n* `summary` - shortly stated purpose of the tool;\n* `website` - link to the tool's website;\n* `command` - the command to install the tool. See supported commands below;\n* `test` - the command to verify if the tool is installed. Most of the time it will be the command to show the version or help;\n\nOnce you added a new tool to the list, update the list in `README.md` by running the following command:\n\n```bash\nphp bin/devkit.php update:readme\n```\n\n### Commands\n\nThere are several supported ways to install tools.\nAll of them are listed below in order of preference.\n\n#### phive\n\nDownloads a phar for the given alias using phive and puts it into the specified location.\n\n```json\n{\n  \"command\": {\n    \"phive-install\": {\n      \"alias\": \"dephpend\",\n      \"bin\": \"%target-dir%/dephpend\",\n      \"sig\": \"76835C9464877BDD\"\n    }\n  }\n}\n```\n\n`sig` is optional, but it is recommended if the phar is signed.\n\n#### phar-download\n\nDownloads a phar from the given URL and puts it into the specified location.\n\n```json\n{\n  \"command\": {\n    \"phar-download\": {\n      \"phar\": \"https://github.com/phpspec/phpspec/releases/download/4.3.0/phpspec.phar\",\n      \"bin\": \"/usr/local/bin/phpspec\"\n    }\n  }\n}\n```\n\n#### file-download\n\nDownloads a file from the given URL and puts it into the specified location.\n\n```json\n{\n  \"command\": {\n    \"file-download\": {\n      \"url\": \"https://github.com/vimeo/psalm/releases/download/2.0.0/psalm.phar.asc\",\n      \"file\": \"/usr/local/bin/psalm.phar.asc\"\n    }\n  }\n}\n```\n\n\n#### composer-bin-plugin\n\nThe `composer-bin-plugin` method uses the [bamarni/composer-bin-plugin](https://github.com/bamarni/composer-bin-plugin)\nto install the package in isolated directory.\nThanks to the isolation we're less likely to run into problem with conflicting dependencies between tools.\n\n```json\n{\n  \"command\": {\n    \"composer-bin-plugin\": {\n      \"package\": \"behat/behat\",\n      \"namespace\": \"behat\",\n      \"links\": {\"/tools/behat\": \"behat\"}\n    }\n  }\n}\n```\n\nThe `links` attribute is optional, but it's recommended for packages that provide commands.\n\n#### box-build\n\nUses [box](https://box-project.github.io/box2/) to build a phar and puts it into the specified location.\nIt will clone the given repository and checkout the latest tag if available.\n\n```json\n{\n  \"command\": {\n    \"box-build\": {\n      \"repository\": \"https://github.com/behat/behat.git\",\n      \"phar\": \"behat.phar\",\n      \"bin\": \"/usr/local/bin/behat\"\n    }\n  }\n}\n```\n\n#### composer-global-install\n\nUses the `composer global require` command to install a composer package globally.\n\n```json\n{\n  \"command\": {\n    \"composer-global-install\": {\n      \"package\": \"bmitch/churn-php\"\n    }\n  }\n}\n```\n\n#### composer-install\n\nClones the specified repository, checkouts the latest tag (if available), and runs `composer install` inside.\nMostly useful with applications.\n\n```json\n{\n  \"command\": {\n    \"composer-install\": {\n      \"repository\": \"https://github.com/Qafoo/QualityAnalyzer.git\"\n    }\n  }\n}\n```\n\n#### Executing multiple commands\n\nIt's sometimes useful to run multiple installation commands i.e. when downloading a phar and its signature.\n\n```json\n{\n  \"command\": {\n    \"file-download\": {\n      \"url\": \"https://github.com/vimeo/psalm/releases/download/2.0.0/psalm.phar.asc\",\n      \"file\": \"/usr/local/bin/psalm.phar.asc\"\n    },\n    \"phar-download\": {\n      \"phar\": \"https://github.com/vimeo/psalm/releases/download/2.0.0/psalm.phar\",\n      \"bin\": \"/usr/local/bin/psalm\"\n    }\n  }\n}\n```\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2017 Jakub Zalas <jakub@zalas.pl>\n\nPermission is hereby granted, free of charge, to any person\nobtaining a copy of this software and associated documentation\nfiles (the \"Software\"), to deal in the Software without\nrestriction, including without limitation the rights to use,\ncopy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the\nSoftware is furnished to do so, subject to the following\nconditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\nOF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\nHOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nWHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\nOTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "default: build\n\nPHP_VERSION:=$(shell php -r 'echo PHP_MAJOR_VERSION.\".\".PHP_MINOR_VERSION;')\nTOOLBOX_VERSION?=dev\n\nbuild: install test\n.PHONY: build\n\ninstall:\n\tcomposer install\n.PHONY: install\n\nupdate:\n\tcomposer update\n.PHONY: update\n\nupdate-min:\n\tcomposer update --prefer-stable --prefer-lowest\n.PHONY: update-min\n\nupdate-no-dev:\n\tcomposer update --prefer-stable --no-dev\n.PHONY: update-no-dev\n\ntest: vendor cs deptrac phpunit infection\n.PHONY: test\n\ntest-min: update-min cs deptrac phpunit infection\n.PHONY: test-min\n\ntest-integration: build/toolbox.phar\n\trm -rf ./build/tools && \\\n\t  export PATH=\"$(shell pwd)/build/tools:$(shell pwd)/build/tools/.composer/vendor/bin:$(shell pwd)/build/tools/QualityAnalyzer/bin:$$PATH\" && \\\n\t  export COMPOSER_HOME=$(shell pwd)/build/tools/.composer && \\\n\t  chmod +x build/toolbox.phar && \\\n\t  mkdir -p ./build/tools && \\\n\t  build/toolbox.phar install --target-dir ./build/tools --exclude-tag exclude-php:$(PHP_VERSION) && \\\n\t  build/toolbox.phar test --target-dir ./build/tools --exclude-tag exclude-php:$(PHP_VERSION)\n.PHONY: test-integration\n\ncs: tools/php-cs-fixer\n\tPHP_CS_FIXER_IGNORE_ENV=true tools/php-cs-fixer --dry-run --allow-risky=yes --no-interaction --ansi fix\n\ncs-fix: tools/php-cs-fixer\n\tPHP_CS_FIXER_IGNORE_ENV=true tools/php-cs-fixer --allow-risky=yes --no-interaction --ansi fix\n\ndeptrac: tools/deptrac\n\ttools/deptrac --no-interaction --ansi\n.PHONY: deptrac\n\ninfection:\n\t./vendor/bin/infection --no-interaction --formatter=progress --min-msi=100 --min-covered-msi=100 --ansi\n.PHONY: infection\n\nphpunit: tools/phpunit\n\ttools/phpunit\n.PHONY: phpunit\n\npackage: tools/box\n\t@rm -rf build/phar && mkdir -p build/phar build/phar/bin\n\n\tcp -r resources src LICENSE composer.json scoper.inc.php build/phar\n\tsed -e 's/Application('\"'\"'dev/Application('\"'\"'$(TOOLBOX_VERSION)/g' bin/toolbox.php > build/phar/bin/toolbox.php\n\n\tcd build/phar && \\\n\t  composer config platform.php 8.2.0 && \\\n\t  composer update --no-dev -o -a\n\n\ttools/box compile\n\n\t@rm -rf build/phar\n.PHONY: package\n\npackage-devkit: tools/box\n\t@rm -rf build/devkit-phar && mkdir -p build/devkit-phar build/devkit-phar/bin build/devkit-phar/src\n\n\tcp -r resources LICENSE composer.json scoper.inc.php build/devkit-phar\n\tcp -r src/Json src/Runner src/Tool build/devkit-phar/src\n\tsed -e 's/\\(Application(.*\\)'\"'\"'dev/\\1'\"'\"'$(TOOLBOX_VERSION)/g' bin/devkit.php > build/devkit-phar/bin/devkit.php\n\n\tcd build/devkit-phar && \\\n\t  composer config platform.php 8.2.0 && \\\n\t  composer update --no-dev -o -a\n\n\ttools/box compile -c box-devkit.json.dist\n\n\t@rm -rf build/devkit-phar\n.PHONY: package-devkit\n\nwebsite: build/devkit.phar\n\trm -rf build/website\n\tmkdir -p build/website\n\tphp build/devkit.phar generate:html > build/website/index.html\n\ttouch build/website/.nojekyll\n.PHONY: website\n\npublish-website: website\n\tcd build/website && \\\n\t  git init . && \\\n\t  git add . && \\\n\t  git commit -m \"Build the website\" && \\\n\t  git push --force --quiet \"https://github.com/jakzal/toolbox.git\" master:gh-pages\n.PHONY: publish-website\n\nupdate-phars: vendor\n\tphp bin/devkit.php update:phars\n\tgit diff --exit-code resources/ || \\\n\t \t  ( \\\n\t \t    git checkout -b tools-update && \\\n\t \t    git add resources/*.json && \\\n\t \t    git commit -m \"Update tools\" && \\\n\t \t    git push origin tools-update \\\n\t \t  )\n.PHONY: update-phars\n\ntools: tools/php-cs-fixer tools/deptrac tools/box\n.PHONY: tools\n\nclean:\n\trm -rf build\n\trm -rf vendor\n\tfind tools -not -path '*/\\.*' -type f -delete\n.PHONY: clean\n\nvendor: install\n\nvendor/bin/phpunit: install\n\ntools/phpunit: vendor/bin/phpunit\n\tln -sf ../vendor/bin/phpunit tools/phpunit\n\ntools/php-cs-fixer:\n\tcurl -Ls https://cs.symfony.com/download/php-cs-fixer-v3.phar -o tools/php-cs-fixer && chmod +x tools/php-cs-fixer\n\ntools/deptrac:\n\tln -sf ../vendor/bin/deptrac tools/deptrac\n\ntools/box:\n\tcurl -Ls https://github.com/humbug/box/releases/download/4.2.0/box.phar -o tools/box && chmod +x tools/box\n"
  },
  {
    "path": "README.md",
    "content": "# Toolbox\n\n[![Build Status](https://github.com/jakzal/toolbox/workflows/Build/badge.svg)](https://github.com/jakzal/toolbox/actions)\n[![Build Status](https://scrutinizer-ci.com/g/jakzal/toolbox/badges/build.png?b=master)](https://scrutinizer-ci.com/g/jakzal/toolbox/build-status/master)\n\nHelps to discover and install tools.\n\n## Use cases\n\nToolbox [started its life](https://github.com/jakzal/phpqa/blob/49482ae447d4b6341cf77aac9d51390fe1176e8c/tools.php)\nas a simple script in the [phpqa docker image](https://github.com/jakzal/phpqa).\nIts purpose was to install set of tools while building the docker image and it's still its main goal.\nIt has been extracted as a separate project to make maintenance easier and enable new use cases.\n\n## Available tools\n\n| Name | Description | PHP 8.2 | PHP 8.3 | PHP 8.4 | PHP 8.5 |\n| :--- | :---------- | :------ | :------ | :------ | :------ |\n| behat | [Helps to test business expectations](http://behat.org/) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| box | [Fast, zero config application bundler with PHARs](https://github.com/humbug/box) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| churn | [Discovers good candidates for refactoring](https://github.com/bmitch/churn-php) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| codeception | [Codeception is a BDD-styled PHP testing framework](https://codeception.com/) | &#x2705; | &#x2705; | &#x2705; | &#x274C; |\n| composer | [Dependency Manager for PHP](https://getcomposer.org/) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| composer-bin-plugin | [Composer plugin to install bin vendors in isolated locations](https://github.com/bamarni/composer-bin-plugin) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| composer-lock-diff | [Composer plugin to check what has changed after a composer update](https://github.com/davidrjonas/composer-lock-diff) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| composer-normalize | [Composer plugin to normalize composer.json files](https://github.com/ergebnis/composer-normalize) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| composer-require-checker | [Verify that no unknown symbols are used in the sources of a package.](https://github.com/maglnet/ComposerRequireChecker) | &#x274C; | &#x274C; | &#x2705; | &#x2705; |\n| composer-require-checker-3 | [Verify that no unknown symbols are used in the sources of a package.](https://github.com/maglnet/ComposerRequireChecker) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| composer-unused | [Show unused packages by scanning your code](https://github.com/icanhazstring/composer-unused) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| cyclonedx-php-composer | [Composer plugin to create Software-Bill-of-Materials (SBOM) in CycloneDX format](https://github.com/CycloneDX/cyclonedx-php-composer) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| dephpend | [Detect flaws in your architecture](https://dephpend.com/) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| deprecation-detector | [Finds usages of deprecated code](https://github.com/sensiolabs-de/deprecation-detector) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| deptrac | [Enforces dependency rules between software layers](https://github.com/deptrac/deptrac) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| diffFilter | [Applies QA tools to run on a single pull request](https://github.com/exussum12/coverageChecker) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| ecs | [Sets up and runs coding standard checks](https://github.com/Symplify/EasyCodingStandard) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| gherkin-lint-php | [Gherkin linter for PHP](https://github.com/dantleech/gherkin-lint-php) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| infection | [AST based PHP Mutation Testing Framework](https://infection.github.io/) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| jack | [Helps to upgrade outdated Composer dependencies incrementally](https://github.com/rectorphp/jack) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| kahlan | [Kahlan is a full-featured Unit & BDD test framework a la RSpec/JSpec](https://kahlan.github.io/docs/) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| larastan | [PHPStan extension for Laravel](https://github.com/nunomaduro/larastan) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| lines | [CLI tool for quick metrics of PHP projects](https://github.com/tomasVotruba/lines) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| local-php-security-checker | [Checks composer dependencies for known security vulnerabilities](https://github.com/fabpot/local-php-security-checker) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| parallel-lint | [Checks PHP file syntax](https://github.com/php-parallel-lint/PHP-Parallel-Lint) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| paratest | [Parallel testing for PHPUnit](https://github.com/paratestphp/paratest) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| pdepend | [Static Analysis Tool](https://pdepend.org/) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| phan | [Static Analysis Tool](https://github.com/phan/phan) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| phive | [PHAR Installation and Verification Environment](https://phar.io/) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| php-cs-fixer | [PHP Coding Standards Fixer](http://cs.symfony.com/) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| php-fuzzer | [A fuzzer for PHP, which can be used to find bugs in libraries by feeding them 'random' inputs](https://github.com/nikic/PHP-Fuzzer) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| php-semver-checker | [Suggests a next version according to semantic versioning](https://github.com/tomzx/php-semver-checker) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| phpa | [Checks for weak assumptions](https://github.com/rskuipers/php-assumptions) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| phparkitect | [Helps to put architectural constraints in a PHP code base](https://github.com/phparkitect/arkitect) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| phpat | [Easy to use architecture testing tool](https://github.com/carlosas/phpat) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| phpbench | [PHP Benchmarking framework](https://github.com/phpbench/phpbench) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| phpca | [Finds usage of non-built-in extensions](https://github.com/wapmorgan/PhpCodeAnalyzer) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| phpcb | [PHP Code Browser](https://github.com/mayflower/PHP_CodeBrowser) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| phpcbf | [Automatically corrects coding standard violations](https://github.com/PHPCSStandards/PHP_CodeSniffer) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| phpcodesniffer-composer-install | [Easy installation of PHP_CodeSniffer coding standards (rulesets).](https://github.com/PHPCSStandards/composer-installer) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| phpcov | [a command-line frontend for the PHP_CodeCoverage library](https://github.com/sebastianbergmann/phpcov) | &#x274C; | &#x274C; | &#x2705; | &#x2705; |\n| phpcpd | [Copy/Paste Detector](https://github.com/sebastianbergmann/phpcpd) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| phpcs | [Detects coding standard violations](https://github.com/PHPCSStandards/PHP_CodeSniffer) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| phpcs-security-audit | [Finds vulnerabilities and weaknesses related to security in PHP code](https://github.com/FloeDesignTechnologies/phpcs-security-audit) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| phpdd | [Finds usage of deprecated features](http://wapmorgan.github.io/PhpDeprecationDetector) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| phpDocumentor | [Documentation generator](https://www.phpdoc.org/) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| phpinsights | [Analyses code quality, style, architecture and complexity](https://phpinsights.com/) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| phplint | [Lints php files in parallel](https://github.com/overtrue/phplint) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| phploc | [A tool for quickly measuring the size of a PHP project](https://github.com/sebastianbergmann/phploc) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| phpmd | [A tool for finding problems in PHP code](https://phpmd.org/) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| phpmetrics | [Static Analysis Tool](http://www.phpmetrics.org/) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| phpmnd | [Helps to detect magic numbers](https://github.com/povils/phpmnd) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| phpspec | [SpecBDD Framework](http://www.phpspec.net/) | &#x2705; | &#x2705; | &#x2705; | &#x274C; |\n| phpstan | [Static Analysis Tool](https://github.com/phpstan/phpstan) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| phpstan-banned-code | [PHPStan rules for detecting calls to specific functions you don't want in your project](https://github.com/ekino/phpstan-banned-code) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| phpstan-beberlei-assert | [PHPStan extension for beberlei/assert](https://github.com/phpstan/phpstan-beberlei-assert) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| phpstan-deprecation-rules | [PHPStan rules for detecting deprecated code](https://github.com/phpstan/phpstan-deprecation-rules) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| phpstan-doctrine | [Doctrine extensions for PHPStan](https://github.com/phpstan/phpstan-doctrine) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| phpstan-ergebnis-rules | [Additional rules for PHPstan](https://github.com/ergebnis/phpstan-rules) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| phpstan-larastan | [Separate installation of phpstan for larastan](https://github.com/phpstan/phpstan) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| phpstan-phpunit | [PHPUnit extensions and rules for PHPStan](https://github.com/phpstan/phpstan-phpunit) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| phpstan-strict-rules | [Extra strict and opinionated rules for PHPStan](https://github.com/phpstan/phpstan-strict-rules) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| phpstan-symfony | [Symfony extension for PHPStan](https://github.com/phpstan/phpstan-symfony) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| phpstan-webmozart-assert | [PHPStan extension for webmozart/assert](https://github.com/phpstan/phpstan-webmozart-assert) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| phpunit | [The PHP testing framework](https://phpunit.de/) | &#x274C; | &#x274C; | &#x2705; | &#x2705; |\n| phpunit-10 | [The PHP testing framework (10.x version)](https://phpunit.de/) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| phpunit-11 | [The PHP testing framework (11.x version)](https://phpunit.de/) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| phpunit-12 | [The PHP testing framework (12.x version)](https://phpunit.de/) | &#x274C; | &#x2705; | &#x2705; | &#x2705; |\n| phpunit-8 | [The PHP testing framework (8.x version)](https://phpunit.de/) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| phpunit-9 | [The PHP testing framework (9.x version)](https://phpunit.de/) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| pint | [Opinionated PHP code style fixer for Laravel](https://github.com/laravel/pint) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| psalm | [Finds errors in PHP applications](https://psalm.dev/) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| psalm-plugin-doctrine | [Stubs to let Psalm understand Doctrine better](https://github.com/weirdan/doctrine-psalm-plugin) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| psalm-plugin-phpunit | [Psalm plugin for PHPUnit](https://github.com/psalm/psalm-plugin-phpunit) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| psalm-plugin-symfony | [Psalm Plugin for Symfony](https://github.com/psalm/psalm-plugin-symfony) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| psecio-parse | [Scans code for potential security-related issues](https://github.com/psecio/parse) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| rector | [Tool for instant code upgrades and refactoring](https://github.com/rectorphp/rector) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| roave-backward-compatibility-check | [Tool to compare two revisions of a class API to check for BC breaks](https://github.com/Roave/BackwardCompatibilityCheck) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| simple-phpunit | [Provides utilities to report legacy tests and usage of deprecated code](https://symfony.com/doc/current/components/phpunit_bridge.html) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| twig-cs-fixer | [Automatically corrects twig files following the official coding standard rules](https://github.com/VincentLanglet/Twig-CS-Fixer) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| twig-lint | [Standalone cli twig 1.X linter](https://github.com/asm89/twig-lint) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| twig-linter | [Standalone cli twig 3.X linter](https://github.com/sserbin/twig-linter) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n| twigcs | [The missing checkstyle for twig!](https://github.com/friendsoftwig/twigcs) | &#x2705; | &#x2705; | &#x2705; | &#x274C; |\n| yaml-lint | [Compact command line utility for checking YAML file syntax](https://github.com/j13k/yaml-lint) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |\n\n### Removed tools\n\n| Name | Summary |  \n| :--- | :------ |\n| analyze | [Visualizes metrics and source code](https://github.com/Qafoo/QualityAnalyzer) |\n| box-legacy | [Legacy version of box](https://box-project.github.io/box2/) |\n| design-pattern | [Detects design patterns](https://github.com/Halleck45/DesignPatternDetector) |\n| parallel-lint | [Checks PHP file syntax](https://github.com/JakubOnderka/PHP-Parallel-Lint) |\n| pest | [The elegant PHP Testing Framework](https://github.com/pestphp/pest) |\n| php-coupling-detector | [Detects code coupling issues](https://akeneo.github.io/php-coupling-detector/) |\n| php-formatter | [Custom coding standards fixer](https://github.com/mmoreram/php-formatter) |\n| phpcf | [Finds usage of deprecated features](http://wapmorgan.github.io/PhpCodeFixer/) |\n| phpda | [Generates dependency graphs](https://mamuz.github.io/PhpDependencyAnalysis/) |\n| phpdoc-to-typehint | [Automatically adds type hints and return types based on PHPDocs](https://github.com/dunglas/phpdoc-to-typehint) |\n| phpstan-exception-rules | [PHPStan rules for checked and unchecked exceptions](https://github.com/pepakriz/phpstan-exception-rules) |\n| phpstan-localheinz-rules | [Additional rules for PHPstan](https://github.com/localheinz/phpstan-rules) |\n| phpunit-5 | [The PHP testing framework (5.x version)](https://phpunit.de/) |\n| phpunit-7 | [The PHP testing framework (7.x version)](https://phpunit.de/) |\n| security-checker | [Checks composer dependencies for known security vulnerabilities](https://github.com/sensiolabs/security-checker) |\n| testability | [Analyses and reports testability issues of a php codebase](https://github.com/edsonmedina/php_testability) |\n\n## Installation\n\nGet the `toolbox.phar` from the [latest release](https://github.com/jakzal/toolbox/releases/latest).\nThe command below should do the job:\n\n```bash\ncurl -Ls https://github.com/jakzal/toolbox/releases/latest/download/toolbox.phar -o toolbox && chmod +x toolbox\n```\n\n## Usage\n\n### List available tools\n\n```\n./toolbox list-tools\n```\n\n#### Filter tools by tags\n\nTo exclude some tools from the listing multiple `--exclude-tag` options can be added.\nThe `--tag` option can be used to filter tools by tags.\n\n```\n./toolbox list-tools --exclude-tag exclude-php:8.2 --exclude-tag foo --tag bar\n```\n\n### Install tools\n\n```\n./toolbox install\n```\n\n#### Install tools in a custom directory\n\nBy default tools are installed in the `/usr/local/bin` directory. To perform an installation in another location,\npass the `--target-dir` option to the `install` command. Also, to change the location composer packages are installed in,\nexport the `COMPOSER_HOME` environment variable.\n\n```\nmkdir /tools\nexport COMPOSER_HOME=/tools/.composer\nexport PATH=\"/tools:$COMPOSER_HOME/vendor/bin:$PATH\"\n./toolbox install --target-dir /tools\n```\n\nThe target dir can also be configured with the `TOOLBOX_TARGET_DIR` environment variable.\n\n#### Dry run\n\nTo only see what commands would be executed, use the dry run mode:\n\n```\n./toolbox install --dry-run\n```\n\n#### Filter tools by tags\n\nTo exclude some tools from the installation multiple `--exclude-tag` options can be added.\nThe `--tag` option can be used to filter tools by tags.\n\n```\n./toolbox install --exclude-tag exclude-php:8.2 --exclude-tag foo --tag bar\n```\n\n### Test if installed tools are usable\n\n```\n./toolbox test\n```\n\n#### Dry run\n\nTo only see what commands would be executed, use the dry run mode:\n\n```\n./toolbox test --dry-run\n```\n\n#### Filter tools by tags\n\nTo exclude some tools from the generated test command multiple `--exclude-tag` options can be added.\nThe `--tag` option can be used to filter tools by tags.\n\n```\n./toolbox test --exclude-tag exclude-php:8.2 --exclude-tag foo --tag bar\n```\n\n### Tools definitions\n\nBy default the following files are used to load tool definitions:\n\n* `resources/pre-installation.json`\n* `resources/architecture.json`\n* `resources/checkstyle.json`\n* `resources/compatibility.json`\n* `resources/composer.json`\n* `resources/deprecation.json`\n* `resources/documentation.json`\n* `resources/linting.json`\n* `resources/metrics.json`\n* `resources/phpstan.json`\n* `resources/psalm.json`\n* `resources/refactoring.json`\n* `resources/security.json`\n* `resources/test.json`\n* `resources/tools.json`\n\nDefinitions can be loaded from customised files by passing the `--tools` option(s):\n\n```\n./toolbox list-tools --tools path/to/file1.json --tools path/to/file2.json\n```\n\nTool definition location(s) can be also specified with the `TOOLBOX_JSON` environment variable:\n\n```\nTOOLBOX_JSON='path/to/file1.json,path/to/file2.json' ./toolbox list-tools\n```\n\n### Tool tags\n\nTools can be tagged in order to enable grouping and filtering them.\n\nThe tags below have a special meaning:\n\n* `pre-installation` - these tools will be installed before any other tools.\n* `exclude-php:8.2`, `exclude-php:8.3` etc - used to exclude installation on the specified php version.\n\n## Contributing\n\nPlease read the [Contributing guide](CONTRIBUTING.md) to learn about contributing to this project.\nPlease note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md).\nBy participating in this project you agree to abide by its terms.\n"
  },
  {
    "path": "bin/devkit.php",
    "content": "#!/usr/bin/env php\n<?php declare(strict_types=1);\n\nrequire __DIR__ . '/../vendor/autoload.php';\n\nuse Symfony\\Component\\Console\\Application;\nuse Symfony\\Component\\Console\\Command\\Command as CliCommand;\nuse Symfony\\Component\\Console\\Input\\InputInterface;\nuse Symfony\\Component\\Console\\Input\\InputOption;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\nuse Zalas\\Toolbox\\Runner\\PassthruRunner;\nuse Zalas\\Toolbox\\Json\\JsonTools;\nuse Zalas\\Toolbox\\Tool\\Collection;\nuse Zalas\\Toolbox\\Tool\\Command;\nuse Zalas\\Toolbox\\Tool\\Command\\ShCommand;\nuse Zalas\\Toolbox\\Tool\\Filter;\nuse Zalas\\Toolbox\\Tool\\Tool;\n\ntrait Tools\n{\n    private function toolsJsonDefault(): array\n    {\n        return \\getenv('TOOLBOX_JSON')\n            ? \\array_map('trim', \\explode(',', \\getenv('TOOLBOX_JSON')))\n            : [\n                __DIR__ . '/../resources/pre-installation.json',\n                __DIR__ . '/../resources/architecture.json',\n                __DIR__ . '/../resources/checkstyle.json',\n                __DIR__ . '/../resources/compatibility.json',\n                __DIR__ . '/../resources/composer.json',\n                __DIR__ . '/../resources/deprecation.json',\n                __DIR__ . '/../resources/documentation.json',\n                __DIR__ . '/../resources/linting.json',\n                __DIR__ . '/../resources/metrics.json',\n                __DIR__ . '/../resources/phpcs.json',\n                __DIR__ . '/../resources/phpstan.json',\n                __DIR__ . '/../resources/psalm.json',\n                __DIR__ . '/../resources/refactoring.json',\n                __DIR__ . '/../resources/security.json',\n                __DIR__ . '/../resources/test.json',\n                __DIR__ . '/../resources/tools.json'\n            ];\n    }\n\n    private function loadTools($jsonPath, ?Filter $filter = null): Collection\n    {\n        return (new JsonTools(function () use ($jsonPath) {\n            return $jsonPath;\n        }))->all($filter ?? new Filter([], []));\n    }\n}\n\n$application = new Application('Toolbox DevKit', 'dev');\n$application->add(\n    new class extends CliCommand\n    {\n        use Tools;\n\n        protected function configure(): void\n        {\n            $this->setName('update:readme');\n            $this->setDescription('Updates README.md with latest list of available tools');\n            $this->addOption('tools', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Path(s) to the list of tools. Can also be set with TOOLBOX_JSON environment variable.', $this->toolsJsonDefault());\n            $this->addOption('readme', null, InputOption::VALUE_REQUIRED, 'Path to the readme file', __DIR__ . '/../README.md');\n        }\n\n        protected function execute(InputInterface $input, OutputInterface $output): int\n        {\n            $jsonPath = $input->getOption('tools');\n            $readmePath = $input->getOption('readme');\n            $tools = $this->loadTools($jsonPath);\n\n            $versions = ['8.2', '8.3', '8.4', '8.5'];\n\n            $toolsList = '| Name | Description | '. implode(' ', array_map(fn($v) => sprintf('PHP %s |', $v), $versions))  . PHP_EOL;\n            $toolsList .= '| :--- | :---------- | '. implode(' ', array_fill(0, count($versions), ':------ |')) . PHP_EOL;\n            $toolsList .= $tools->sort(function (Tool $left, Tool $right) {\n                return strcasecmp($left->name(), $right->name());\n            })->reduce('', function ($acc, Tool $tool) use ($versions) {\n\n                $args = [\n                    $tool->name(),\n                    $tool->summary(),\n                    $tool->website(),\n                ];\n                foreach ($versions as $version) {\n                    $args[] = in_array(sprintf('exclude-php:%s', $version), $tool->tags(), true) ? '&#x274C;' : '&#x2705;';\n                }\n\n                return $acc . vsprintf('| %s | [%s](%s) | '. implode(' ', array_fill(0, count($versions), '%s |')), $args) . PHP_EOL;\n            });\n\n            $readme = file_get_contents($readmePath);\n            $readme = preg_replace('/(## Available tools\\n\\n).*?(\\n#+ )/smi', '$1' . $toolsList . '$2', $readme);\n\n            file_put_contents($readmePath, $readme);\n\n            $output->writeln(sprintf('The <info>%s</info> was updated with latest tools found in <info>%s</info>.', $readmePath, implode(', ', $jsonPath)));\n\n            return 0;\n        }\n    }\n);\n$application->add(\n    new class extends CliCommand\n    {\n        use Tools;\n\n        protected function configure(): void\n        {\n            $this->setName('update:phars');\n            $this->setDescription('Attempts to update phar links to latest versions');\n            $this->addOption('tools', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Path(s) to the list of tools. Can also be set with TOOLBOX_JSON environment variable.', $this->toolsJsonDefault());\n        }\n\n        protected function execute(InputInterface $input, OutputInterface $output): int\n        {\n            foreach ($input->getOption('tools') as $jsonPath) {\n                $result = $this->updatePhars($jsonPath, $output);\n\n                if ($result !== 0) {\n                    return $result;\n                }\n            }\n\n            return 0;\n        }\n\n        private function updatePhars(string $jsonPath, OutputInterface $output): int\n        {\n            $phars = $this->findLatestPhars($jsonPath);\n\n            if (empty($phars)) {\n                return 0;\n            }\n\n            $output->writeln('Found phars:');\n\n            foreach ($phars as $phar) {\n                $output->writeln(sprintf('* %s', $phar));\n            }\n\n            $output->writeln(sprintf('Updated <info>%s</info>.', $jsonPath));\n\n            return (new PassthruRunner())->run($this->updatePharsCommand($jsonPath, $phars));\n        }\n\n        private function findLatestPharsCommand(string $jsonPath): Command\n        {\n            $command = <<<'CMD'\n            grep -e 'github\\.com.*releases.*\\.phar\"' %TOOLBOX_JSON% |\n            grep -v -e '/latest/' |\n            sed -e 's@.*github.com/\\(.*\\)/releases.*@\\1@' |\n            xargs -I\"{}\" sh -c \"curl -s -XGET 'https://api.github.com/repos/{}/releases/latest' -H 'Accept:application/json' | grep browser_download_url | grep .phar | head -n 1\" |\n            sed -e 's/^[^:]*: \"\\([^\"]*\\)\"/\\1/'\nCMD;\n            $command = strtr($command, ['%TOOLBOX_JSON%' => $jsonPath]);\n\n            return new ShCommand($command);\n        }\n\n        private function findLatestPhars(string $jsonPath): array\n        {\n            $phars = [];\n\n            exec((string)$this->findLatestPharsCommand($jsonPath), $phars);\n\n            return $phars;\n        }\n\n        private function updatePharsCommand(string $jsonPath, array $phars): Command\n        {\n            $replacements = implode(' ', array_map(\n                function (string $phar) {\n                    $project = preg_replace('@https://[^/]*/([^/]*/[^/]*).*@', '$1', $phar);\n\n                    return strtr(\n                        '-e \"s@\\\"phar\\\": \\\"([^\\\"]*%PROJECT%[^\\\"]*)\\\"@\\\"phar\\\": \\\"%PHAR%\\\"@g\"' .\n                        ' ' .\n                        '-e \"s@\\\"url\\\": \\\"([^\\\"]*%PROJECT%[^\\\"]*\\.phar(\\.asc|\\.pubkey))\\\"@\\\"url\\\": \\\"%PHAR%\\\\2\\\"@g\"',\n                        ['%PROJECT%' => $project, '%PHAR%' => $phar]\n                    );\n                },\n                $phars\n            ));\n\n            return new ShCommand(sprintf('sed -i.bak -E %s %s', $replacements, $jsonPath));\n        }\n    }\n);\n$application->add(\n    new class extends CliCommand\n    {\n        use Tools;\n\n        protected function configure(): void\n        {\n            $this->setName('generate:html');\n            $this->setDescription('Generates an html list of available tools');\n            $this->addOption('tools', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Path(s) to the list of tools. Can also be set with TOOLBOX_JSON environment variable.', $this->toolsJsonDefault());\n        }\n\n        protected function execute(InputInterface $input, OutputInterface $output): int\n        {\n            $tools = $this->loadTools($input->getOption('tools'), new Filter(['pre-installation'], []));\n\n            $output->writeln($this->renderPage($tools->map($this->toolToHtml())));\n\n            return 0;\n        }\n\n        private function toolToHtml(): \\Closure\n        {\n            $tagTemplate = '<li class=\"list-inline-item\"><span class=\"badge badge-primary\">%TAG%</span></li>';\n            $toolTemplate = <<<'TEMPLATE'\n<div class=\"card m-3\">\n    <div class=\"card-body\">\n        <h5 class=\"card-title\">%NAME%</h5>\n        <p class=\"card-text tool-summary\">%SUMMARY%</p>\n        <a href=\"%WEBSITE%\" class=\"card-link\" title=\"%NAME%\">%WEBSITE_NAME%</a>\n    </div>\n    <div class=\"card-footer\">\n        <ul class=\"list-inline mb-1\">\n            %TAGS%\n        </ul>\n    </div>\n</div>\nTEMPLATE;\n\n            return function (Tool $tool) use ($toolTemplate, $tagTemplate) {\n                return strtr(\n                    $toolTemplate,\n                    [\n                        '%NAME%' => $tool->name(),\n                        '%SUMMARY%' => $tool->summary(),\n                        '%WEBSITE%' => $tool->website(),\n                        '%WEBSITE_NAME%' => preg_replace('#^(https?://(github.com/)?)(.*?)/?$#', '$3', $tool->website()),\n                        '%TAGS%' => \\implode(\\array_map(function (string $tag) use ($tagTemplate) {\n                            return strtr($tagTemplate, ['%TAG%' => $tag]);\n                        }, $tool->tags()))\n                    ]);\n            };\n        }\n\n        private function renderPage(Collection $toolsHtml): string\n        {\n            $template = <<<'TEMPLATE'\n<!doctype html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n    <link rel=\"stylesheet\" href=\"https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css\"\n          integrity=\"sha384-GJzZqFGwb1QTTN6wy59ffF1BuGJpLSa9DkKMp0DgiMDm4iYMj70gZWKYbI706tWS\" crossorigin=\"anonymous\">\n    <title>Quality Assurance Tools for PHP | Toolbox | PHPQA</title>\n    <style>\n        .tool-summary {\n            font-size: 0.8em;\n            height: 2em;\n        }\n        .card-link {\n            font-size: 0.7em;\n        }\n    </style>\n</style>\n</head>\n<body>\n<div class=\"container-fluid p-5\">\n\n    <div class=\"jumbotron\">\n        <h1 class=\"display-4\">Quality Assurance Tools for PHP</h1>\n        <p class=\"lead\">\n          The below list of tools is provided by the <a href=\"https://hub.docker.com/r/jakzal/phpqa/\">phpqa</a> docker image.\n          <a href=\"https://github.com/jakzal/toolbox\">Toolbox</a> is used to install them in the image.\n        </p>\n        <hr class=\"my-4\">\n        <a class=\"btn btn-primary btn-lg\" href=\"https://github.com/jakzal/toolbox\" role=\"button\">toolbox repository</a>\n        <a class=\"btn btn-primary btn-lg\" href=\"https://hub.docker.com/r/jakzal/phpqa/\" role=\"button\">phpqa docker image</a>\n        <a class=\"btn btn-primary btn-lg\" href=\"https://github.com/jakzal/phpqa\" role=\"button\">phpqa repository</a>\n    </div>\n\n    %TOOLS%\n\n    <hr class=\"my-4\"/>\n\n    <div class=\"text-center\">Generated on %GENERATED_ON%.</div>\n</body>\n</html>\nTEMPLATE;\n\n            return strtr($template, [\n                '%TOOLS%' => \\implode(PHP_EOL, \\array_map(\n                    function ($htmls) {\n                        return PHP_EOL . '<div class=\"card-deck\">' . implode($htmls) . '</div>' . PHP_EOL;\n                    },\n                    \\array_chunk($toolsHtml->toArray(), 4)\n                )),\n                '%GENERATED_ON%' => (new \\DateTime('now', new \\DateTimeZone('UTC')))->format('r'),\n            ]);\n        }\n    }\n);\n$application->run();\n"
  },
  {
    "path": "bin/toolbox.php",
    "content": "#!/usr/bin/env php\n<?php\n\nrequire __DIR__ . '/../vendor/autoload.php';\n\n(new Zalas\\Toolbox\\Cli\\Application('dev', new Zalas\\Toolbox\\Cli\\ServiceContainer()))->run();\n"
  },
  {
    "path": "box-devkit.json.dist",
    "content": "{\n    \"base-path\": \"build/devkit-phar\",\n    \"output\": \"../devkit.phar\",\n    \"compression\": \"GZ\",\n    \"directories\": [\".\"],\n    \"check-requirements\": false,\n    \"main\": \"bin/devkit.php\",\n    \"compactors\": [\n        \"KevinGH\\\\Box\\\\Compactor\\\\PhpScoper\"\n    ],\n    \"banner\": [\n        \"This file is part of the zalas/toolbox project.\",\n        \"\",\n        \"(c) Jakub Zalas <jakub@zalas.pl>\"\n    ]\n}\n"
  },
  {
    "path": "box.json.dist",
    "content": "{\n    \"base-path\": \"build/phar\",\n    \"output\": \"../toolbox.phar\",\n    \"compression\": \"GZ\",\n    \"directories\": [\".\"],\n    \"check-requirements\": false,\n    \"main\": \"bin/toolbox.php\",\n    \"compactors\": [\n        \"KevinGH\\\\Box\\\\Compactor\\\\PhpScoper\"\n    ],\n    \"banner\": [\n        \"This file is part of the zalas/toolbox project.\",\n        \"\",\n        \"(c) Jakub Zalas <jakub@zalas.pl>\"\n    ]\n}\n"
  },
  {
    "path": "composer.json",
    "content": "{\n    \"name\": \"zalas/toolbox\",\n    \"description\": \"Helps to discover and install tools\",\n    \"type\": \"project\",\n    \"require\": {\n        \"php\": \"~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0\",\n        \"symfony/console\": \"^7.4 || ^8.0\",\n        \"psr/container\": \"^2.0\"\n    },\n    \"require-dev\": {\n        \"phpunit/phpunit\": \"^11.5.9 || ^12.0 || ^13.0\",\n        \"zalas/phpunit-globals\": \"^4.0\",\n        \"infection/infection\": \"^0.31\",\n        \"deptrac/deptrac\": \"^4.0\"\n    },\n    \"autoload\": {\n        \"psr-4\": {\n            \"Zalas\\\\Toolbox\\\\\": \"src\"\n        }\n    },\n    \"autoload-dev\": {\n        \"psr-4\": {\n            \"Zalas\\\\Toolbox\\\\Tests\\\\\": \"tests\"\n        }\n    },\n    \"license\": \"MIT\",\n    \"authors\": [\n        {\n            \"name\": \"Jakub Zalas\",\n            \"email\": \"jakub@zalas.pl\"\n        }\n    ],\n    \"extra\": {\n        \"branch-alias\": {\n            \"dev-master\": \"1.x-dev\"\n        }\n    },\n    \"config\": {\n        \"allow-plugins\": {\n            \"infection/extension-installer\": true\n        }\n    }\n}\n"
  },
  {
    "path": "deptrac.yaml",
    "content": "parameters:\n  paths:\n    - ./src\n  exclude_files: []\n  layers:\n    - name: Cli\n      collectors:\n        - type: classLike\n          value: ^Zalas\\\\Toolbox\\\\Cli\\\\.*\n    - name: Json\n      collectors:\n        - type: classLike\n          value: ^Zalas\\\\Toolbox\\\\Json\\\\.*\n    - name: Runner\n      collectors:\n        - type: classLike\n          value: ^Zalas\\\\Toolbox\\\\Runner\\\\.*\n    - name: Tool\n      collectors:\n        - type: classLike\n          value: ^Zalas\\\\Toolbox\\\\Tool\\\\.*\n    - name: UseCase\n      collectors:\n        - type: classLike\n          value: ^Zalas\\\\Toolbox\\\\UseCase\\\\.*\n    - name: Psr Container\n      collectors:\n        - type: classLike\n          value: ^Psr\\\\Container\\\\.*\n    - name: Symfony Console\n      collectors:\n        - type: classLike\n          value: ^Symfony\\\\Component\\\\Console\\\\.*\n    - name: Other Vendors\n      collectors:\n        - type: bool\n          must:\n            # must be outside of global namespace\n            - type: classLike\n              value: '[\\\\]+'\n          must_not:\n            # must not be one of the known vendors\n            - type: classLike\n              value: ^Zalas\\\\Toolbox\\\\(Cli|Json|Runner|Tool|UseCase)\\\\.*\n            - type: classLike\n              value: ^Psr\\\\Container\\\\.*\n            - type: classLike\n              value: ^Symfony\\\\Component\\\\Console\\\\.*\n  ruleset:\n    Cli:\n      - Tool\n      - Json\n      - Runner\n      - UseCase\n      - Symfony Console\n      - Psr Container\n    Json:\n      - Tool\n    Runner:\n      - Tool\n    Tool:\n    UseCase:\n      - Tool\n"
  },
  {
    "path": "infection.json.dist",
    "content": "{\n    \"timeout\": 2,\n    \"source\": {\n        \"directories\": [\n            \"src\"\n        ]\n    },\n    \"logs\": {\n        \"text\": \"build/infection-log.txt\"\n    },\n    \"mutators\": {\n        \"@default\": true,\n        \"EqualIdentical\": false,\n        \"NotIdenticalNotEqual\": false,\n        \"Concat\": false,\n        \"ConcatOperandRemoval\": false,\n        \"ArrayItemRemoval\": {\n            \"ignore\": [\n                \"Zalas\\\\Toolbox\\\\Cli\\\\Command\\\\ListCommand::execute\"\n            ]\n        }\n    }\n}\n"
  },
  {
    "path": "phpunit.xml.dist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:noNamespaceSchemaLocation=\"vendor/phpunit/phpunit/phpunit.xsd\" bootstrap=\"vendor/autoload.php\" beStrictAboutOutputDuringTests=\"true\" colors=\"true\">\n  <coverage includeUncoveredFiles=\"true\">\n    <report>\n      <clover outputFile=\"build/coverage.xml\"/>\n      <html outputDirectory=\"build/coverage\" lowUpperBound=\"50\" highLowerBound=\"95\"/>\n    </report>\n  </coverage>\n  <testsuites>\n    <testsuite name=\"default\">\n      <directory suffix=\"Test.php\">tests</directory>\n    </testsuite>\n  </testsuites>\n  <php>\n    <env name=\"TOOLBOX_TARGET_DIR\" value=\"\" force=\"true\"/>\n    <env name=\"TOOLBOX_TAGS\" value=\"\" force=\"true\"/>\n    <env name=\"TOOLBOX_EXCLUDED_TAGS\" value=\"\" force=\"true\"/>\n  </php>\n  <extensions>\n    <bootstrap class=\"Zalas\\PHPUnit\\Globals\\AttributeExtension\"/>\n  </extensions>\n  <logging/>\n  <source>\n    <include>\n      <directory suffix=\".php\">src</directory>\n    </include>\n  </source>\n</phpunit>\n"
  },
  {
    "path": "resources/architecture.json",
    "content": "{\n    \"tools\": [\n        {\n            \"name\": \"dephpend\",\n            \"summary\": \"Detect flaws in your architecture\",\n            \"website\": \"https://dephpend.com/\",\n            \"command\": {\n                \"phive-install\": {\n                    \"alias\": \"dephpend\",\n                    \"bin\": \"%target-dir%/dephpend\",\n                    \"sig\": \"76835C9464877BDD\"\n                }\n            },\n            \"test\": \"dephpend list\",\n            \"tags\": [\"architecture\"]\n        },\n        {\n            \"name\": \"deptrac\",\n            \"summary\": \"Enforces dependency rules between software layers\",\n            \"website\": \"https://github.com/deptrac/deptrac\",\n            \"command\": {\n                \"composer-global-install\": {\n                    \"package\": \"deptrac/deptrac\"\n                }\n            },\n            \"test\": \"deptrac list\",\n            \"tags\": [\"featured\", \"architecture\"]\n        },\n        {\n            \"name\": \"pdepend\",\n            \"summary\": \"Static Analysis Tool\",\n            \"website\": \"https://pdepend.org/\",\n            \"command\": {\n                \"phive-install\": {\n                    \"alias\": \"pdepend/pdepend\",\n                    \"bin\": \"%target-dir%/pdepend\",\n                    \"sig\": \"508003DAED98C308\"\n                }\n            },\n            \"test\": \"pdepend --version\",\n            \"tags\": [\"featured\", \"architecture\"]\n        },\n        {\n            \"name\": \"phparkitect\",\n            \"summary\": \"Helps to put architectural constraints in a PHP code base\",\n            \"website\": \"https://github.com/phparkitect/arkitect\",\n            \"command\": {\n                \"phar-download\": {\n                    \"phar\": \"https://github.com/phparkitect/arkitect/releases/latest/download/phparkitect.phar\",\n                    \"bin\": \"%target-dir%/phparkitect\"\n                }\n            },\n            \"test\": \"phparkitect --version\",\n            \"tags\": [\"architecture\"]\n        }\n    ]\n}\n"
  },
  {
    "path": "resources/checkstyle.json",
    "content": "{\n    \"tools\": [\n        {\n            \"name\": \"ecs\",\n            \"summary\": \"Sets up and runs coding standard checks\",\n            \"website\": \"https://github.com/Symplify/EasyCodingStandard\",\n            \"command\": {\n                \"composer-bin-plugin\": {\n                    \"package\": \"symplify/easy-coding-standard\",\n                    \"namespace\": \"ecs\",\n                    \"links\": {\"%target-dir%/ecs\": \"ecs\"}\n                }\n            },\n            \"test\": \"ecs -h\",\n            \"tags\": [\"checkstyle\"]\n        },\n        {\n            \"name\": \"pint\",\n            \"summary\": \"Opinionated PHP code style fixer for Laravel\",\n            \"website\": \"https://github.com/laravel/pint\",\n            \"command\": {\n                \"composer-bin-plugin\": {\n                    \"package\": \"laravel/pint\",\n                    \"namespace\": \"pint\",\n                    \"links\": {\"%target-dir%/pint\": \"pint\"}\n                }\n            },\n            \"test\": \"pint --version\",\n            \"tags\": [\"checkstyle\"]\n        },\n        {\n            \"name\": \"php-cs-fixer\",\n            \"summary\": \"PHP Coding Standards Fixer\",\n            \"website\": \"http://cs.symfony.com/\",\n            \"command\": {\n                \"phive-install\": {\n                    \"alias\": \"php-cs-fixer\",\n                    \"bin\": \"%target-dir%/php-cs-fixer\",\n                    \"sig\": \"E82B2FB314E9906E\"\n                }\n            },\n            \"test\": \"php-cs-fixer list\",\n            \"tags\": [\"featured\", \"checkstyle\"]\n        },\n        {\n            \"name\": \"phpcbf\",\n            \"summary\": \"Automatically corrects coding standard violations\",\n            \"website\": \"https://github.com/PHPCSStandards/PHP_CodeSniffer\",\n            \"command\": {\n                \"phive-install\": {\n                    \"alias\": \"phpcbf\",\n                    \"bin\": \"%target-dir%/phpcbf\",\n                    \"sig\": \"97B02DD8E5071466\"\n                }\n            },\n            \"test\": \"phpcbf --help\",\n            \"tags\": [\"checkstyle\"]\n        },\n        {\n            \"name\": \"twigcs\",\n            \"summary\": \"The missing checkstyle for twig!\",\n            \"website\": \"https://github.com/friendsoftwig/twigcs\",\n            \"command\": {\n                \"phive-install\": {\n                    \"alias\": \"friendsoftwig/twigcs\",\n                    \"bin\": \"%target-dir%/twigcs\",\n                    \"sig\": \"C00543248C87FB13\"\n                }\n            },\n            \"test\": \"twigcs --help\",\n            \"tags\": [\"checkstyle\", \"exclude-php:8.5\"]\n        },\n        {\n            \"name\": \"twig-cs-fixer\",\n            \"summary\": \"Automatically corrects twig files following the official coding standard rules\",\n            \"website\": \"https://github.com/VincentLanglet/Twig-CS-Fixer\",\n            \"command\": {\n                \"composer-bin-plugin\": {\n                    \"package\": \"vincentlanglet/twig-cs-fixer\",\n                    \"namespace\": \"twig-cs-fixer\",\n                    \"links\": {\"%target-dir%/twig-cs-fixer\": \"twig-cs-fixer\"}\n                }\n            },\n            \"test\": \"twig-cs-fixer --help\",\n            \"tags\": [\"checkstyle\"]\n        }\n    ]\n}\n"
  },
  {
    "path": "resources/compatibility.json",
    "content": "{\n    \"tools\": [\n        {\n            \"name\": \"php-semver-checker\",\n            \"summary\": \"Suggests a next version according to semantic versioning\",\n            \"website\": \"https://github.com/tomzx/php-semver-checker\",\n            \"command\": {\n                \"phar-download\": {\n                    \"phar\": \"https://github.com/tomzx/php-semver-checker/releases/download/v0.17.0/php-semver-checker.phar\",\n                    \"bin\": \"%target-dir%/php-semver-checker\"\n                }\n            },\n            \"test\": \"php-semver-checker list\",\n            \"tags\": [\"compatibility\"]\n        },\n        {\n            \"name\": \"roave-backward-compatibility-check\",\n            \"summary\": \"Tool to compare two revisions of a class API to check for BC breaks\",\n            \"website\": \"https://github.com/Roave/BackwardCompatibilityCheck\",\n            \"command\": {\n                \"sh\": {\n                    \"command\": \"composer global bin roavebackwardcompatibilitycheck config allow-plugins.ocramius/package-versions true\"\n                },\n                \"composer-bin-plugin\": {\n                    \"package\": \"roave/backward-compatibility-check\",\n                    \"namespace\": \"roavebackwardcompatibilitycheck\",\n                    \"links\": {\"%target-dir%/roave-backward-compatibility-check\": \"roave-backward-compatibility-check\"}\n                }\n            },\n            \"test\": \"roave-backward-compatibility-check --version\",\n            \"tags\": [\"compatibility\"]\n        }\n    ]\n}\n"
  },
  {
    "path": "resources/composer.json",
    "content": "{\n    \"tools\": [\n        {\n            \"name\": \"composer-normalize\",\n            \"summary\": \"Composer plugin to normalize composer.json files\",\n            \"website\": \"https://github.com/ergebnis/composer-normalize\",\n            \"command\": {\n                \"sh\": {\n                    \"command\": \"composer config --global --json allow-plugins.ergebnis/composer-normalize true\"\n                },\n                \"composer-global-install\": {\n                    \"package\": \"ergebnis/composer-normalize\"\n                }\n            },\n            \"test\": \"composer global show ergebnis/composer-normalize\",\n            \"tags\": [\"composer\"]\n        },\n        {\n            \"name\": \"composer-unused\",\n            \"summary\": \"Show unused packages by scanning your code\",\n            \"website\": \"https://github.com/icanhazstring/composer-unused\",\n            \"command\": {\n                \"phive-install\": {\n                    \"alias\": \"composer-unused\",\n                    \"bin\": \"%target-dir%/composer-unused\",\n                    \"sig\": \"3135AA4CB4F1AB0B\"\n                }\n            },\n            \"test\": \"composer-unused -V\",\n            \"tags\": [\"composer\"]\n        },\n        {\n            \"name\": \"composer-require-checker\",\n            \"summary\": \"Verify that no unknown symbols are used in the sources of a package.\",\n            \"website\": \"https://github.com/maglnet/ComposerRequireChecker\",\n            \"command\": {\n                \"phive-install\": {\n                    \"alias\": \"composer-require-checker\",\n                    \"bin\": \"%target-dir%/composer-require-checker\",\n                    \"sig\": \"033E5F8D801A2F8D\"\n                }\n            },\n            \"test\": \"composer-require-checker -V\",\n            \"tags\": [\"composer\", \"exclude-php:8.2\", \"exclude-php:8.3\"]\n        },\n        {\n            \"name\": \"composer-require-checker-3\",\n            \"summary\": \"Verify that no unknown symbols are used in the sources of a package.\",\n            \"website\": \"https://github.com/maglnet/ComposerRequireChecker\",\n            \"command\": {\n                \"phive-install\": {\n                    \"alias\": \"composer-require-checker@^3.8\",\n                    \"bin\": \"%target-dir%/composer-require-checker-3\",\n                    \"sig\": \"033E5F8D801A2F8D\"\n                }\n            },\n            \"test\": \"composer-require-checker-3 -V\",\n            \"tags\": [\"composer\"]\n        },\n        {\n            \"name\": \"cyclonedx-php-composer\",\n            \"summary\": \"Composer plugin to create Software-Bill-of-Materials (SBOM) in CycloneDX format\",\n            \"website\": \"https://github.com/CycloneDX/cyclonedx-php-composer\",\n            \"command\": {\n                \"sh\": {\n                    \"command\": \"composer global config --no-plugins allow-plugins.cyclonedx/cyclonedx-php-composer true\"\n                },\n                \"composer-global-install\": {\n                    \"package\": \"cyclonedx/cyclonedx-php-composer\"\n                }\n            },\n            \"test\": \"composer global show cyclonedx/cyclonedx-php-composer\",\n            \"tags\": [\"composer\"]\n        },\n        {\n            \"name\": \"composer-lock-diff\",\n            \"summary\": \"Composer plugin to check what has changed after a composer update\",\n            \"website\": \"https://github.com/davidrjonas/composer-lock-diff\",\n            \"command\": {\n                \"composer-bin-plugin\": {\n                    \"package\": \"davidrjonas/composer-lock-diff\",\n                    \"namespace\": \"composer-lock-diff\",\n                    \"links\": {\"%target-dir%/composer-lock-diff\": \"composer-lock-diff\"}\n                }\n            },\n            \"test\": \"composer-lock-diff --help\",\n            \"tags\": [\"composer\"]\n        },\n        {\n            \"name\": \"jack\",\n            \"summary\": \"Helps to upgrade outdated Composer dependencies incrementally\",\n            \"website\": \"https://github.com/rectorphp/jack\",\n            \"command\": {\n                \"composer-bin-plugin\": {\n                    \"package\": \"rector/jack:0.4.0\",\n                    \"namespace\": \"jack\",\n                    \"links\": {\"%target-dir%/jack\": \"jack\"}\n                }\n            },\n            \"test\": \"jack help\",\n            \"tags\": [\"composer\"]\n        }\n    ]\n}\n"
  },
  {
    "path": "resources/deprecation.json",
    "content": "{\n    \"tools\": [\n        {\n            \"name\": \"deprecation-detector\",\n            \"summary\": \"Finds usages of deprecated code\",\n            \"website\": \"https://github.com/sensiolabs-de/deprecation-detector\",\n            \"command\": {\n                \"phive-install\": {\n                    \"alias\": \"sensiolabs-de/deprecation-detector\",\n                    \"bin\": \"%target-dir%/deprecation-detector\"\n                }\n            },\n            \"test\": \"deprecation-detector list\",\n            \"tags\": [\"deprecation\"]\n        },\n        {\n            \"name\": \"phpdd\",\n            \"summary\": \"Finds usage of deprecated features\",\n            \"website\": \"http://wapmorgan.github.io/PhpDeprecationDetector\",\n            \"command\": {\n                \"phive-install\": {\n                    \"alias\": \"wapmorgan/phpdeprecationdetector\",\n                    \"bin\": \"%target-dir%/phpdd\"\n                }\n            },\n            \"test\": \"phpdd -h\",\n            \"tags\": [\"deprecation\"]\n        }\n    ]\n}\n"
  },
  {
    "path": "resources/documentation.json",
    "content": "{\n    \"tools\": [\n        {\n            \"name\": \"phpDocumentor\",\n            \"summary\": \"Documentation generator\",\n            \"website\": \"https://www.phpdoc.org/\",\n            \"command\": {\n                \"phive-install\": {\n                    \"alias\": \"phpDocumentor\",\n                    \"bin\": \"%target-dir%/phpDocumentor\",\n                    \"sig\": \"6DA3ACC4991FFAE5\"\n                }\n            },\n            \"test\": \"phpDocumentor --help\",\n            \"tags\": [\"featured\", \"documentation\"]\n        },\n        {\n            \"name\": \"phpcb\",\n            \"summary\": \"PHP Code Browser\",\n            \"website\": \"https://github.com/mayflower/PHP_CodeBrowser\",\n            \"command\": {\n                \"phar-download\": {\n                    \"phar\": \"https://github.com/bytepark/php-phar-qatools/raw/master/phpcb.phar\",\n                    \"bin\": \"%target-dir%/phpcb\"\n                }\n            },\n            \"test\": \"phpcb -V\",\n            \"tags\": [\"documentation\"]\n        }\n    ]\n}\n"
  },
  {
    "path": "resources/linting.json",
    "content": "{\n    \"tools\": [\n        {\n            \"name\": \"parallel-lint\",\n            \"summary\": \"Checks PHP file syntax\",\n            \"website\": \"https://github.com/php-parallel-lint/PHP-Parallel-Lint\",\n            \"command\": {\n                \"phive-install\": {\n                    \"alias\": \"php-parallel-lint/PHP-Parallel-Lint\",\n                    \"bin\": \"%target-dir%/parallel-lint\"\n                }\n            },\n            \"test\": \"parallel-lint -h\",\n            \"tags\": [\"linting\"]\n        },\n        {\n            \"name\": \"phplint\",\n            \"summary\": \"Lints php files in parallel\",\n            \"website\": \"https://github.com/overtrue/phplint\",\n            \"command\": {\n                \"composer-bin-plugin\": {\n                    \"package\": \"overtrue/phplint\",\n                    \"namespace\": \"phplint\",\n                    \"links\": {\"%target-dir%/phplint\": \"phplint\"}\n                }\n            },\n            \"test\": \"phplint -V\",\n            \"tags\": [\"linting\"]\n        },\n        {\n            \"name\": \"twig-lint\",\n            \"summary\": \"Standalone cli twig 1.X linter\",\n            \"website\": \"https://github.com/asm89/twig-lint\",\n            \"command\": {\n                \"phar-download\": {\n                    \"phar\": \"https://asm89.github.io/d/twig-lint.phar\",\n                    \"bin\": \"%target-dir%/twig-lint\"\n                }\n            },\n            \"test\": \"twig-lint --version\",\n            \"tags\": [\"linting\"]\n        },\n        {\n            \"name\": \"yaml-lint\",\n            \"summary\": \"Compact command line utility for checking YAML file syntax\",\n            \"website\": \"https://github.com/j13k/yaml-lint\",\n            \"command\": {\n                \"phive-install\": {\n                    \"alias\": \"j13k/yaml-lint\",\n                    \"bin\": \"%target-dir%/yaml-lint\",\n                    \"sig\": \"38A182AB413064D7\"\n                }\n            },\n            \"test\": \"yaml-lint --version\",\n            \"tags\": [\"linting\"]\n        },\n        {\n            \"name\": \"twig-linter\",\n            \"summary\": \"Standalone cli twig 3.X linter\",\n            \"website\": \"https://github.com/sserbin/twig-linter\",\n            \"command\": {\n                \"composer-bin-plugin\": {\n                    \"package\": \"sserbin/twig-linter:@dev\",\n                    \"namespace\": \"twig-linter\",\n                    \"links\": {\"%target-dir%/twig-linter\": \"twig-linter\"}\n                }\n            },\n            \"test\": \"twig-linter --help\",\n            \"tags\": [\"linting\"]\n        },\n        {\n            \"name\": \"gherkin-lint-php\",\n            \"summary\": \"Gherkin linter for PHP\",\n            \"website\": \"https://github.com/dantleech/gherkin-lint-php\",\n            \"command\": {\n                \"composer-bin-plugin\": {\n                    \"package\": \"dantleech/gherkin-lint\",\n                    \"namespace\": \"gherkin-lint-php\",\n                    \"links\": {\"%target-dir%/gherkinlint\": \"gherkinlint\"}\n                }\n            },\n            \"test\": \"gherkinlint --help\",\n            \"tags\": [\"linting\"]\n        }\n    ]\n}\n"
  },
  {
    "path": "resources/metrics.json",
    "content": "{\n    \"tools\": [\n        {\n            \"name\": \"phpinsights\",\n            \"summary\": \"Analyses code quality, style, architecture and complexity\",\n            \"website\": \"https://phpinsights.com/\",\n            \"command\": {\n                \"sh\": {\n                    \"command\": \"composer global bin phpinsights config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true\"\n                },\n                \"composer-bin-plugin\": {\n                    \"package\": \"nunomaduro/phpinsights\",\n                    \"namespace\": \"phpinsights\",\n                    \"links\": {\"%target-dir%/phpinsights\": \"phpinsights\"}\n                }\n            },\n            \"test\": \"phpinsights --version\",\n            \"tags\": [\"metrics\"]\n        },\n        {\n            \"name\": \"phploc\",\n            \"summary\": \"A tool for quickly measuring the size of a PHP project\",\n            \"website\": \"https://github.com/sebastianbergmann/phploc\",\n            \"command\": {\n                \"phive-install\": {\n                    \"alias\": \"phploc\",\n                    \"bin\": \"%target-dir%/phploc\",\n                    \"sig\": \"4AA394086372C20A\"\n                }\n            },\n            \"test\": \"phploc -v\",\n            \"tags\": [\"metrics\"]\n        },\n        {\n            \"name\": \"phpmetrics\",\n            \"summary\": \"Static Analysis Tool\",\n            \"website\": \"http://www.phpmetrics.org/\",\n            \"command\": {\n                \"phive-install\": {\n                    \"alias\": \"phpmetrics/PhpMetrics\",\n                    \"bin\": \"%target-dir%/phpmetrics\"\n                }\n            },\n            \"test\": \"phpmetrics --version\",\n            \"tags\": [\"featured\", \"metrics\"]\n        },\n        {\n            \"name\": \"lines\",\n            \"summary\": \"CLI tool for quick metrics of PHP projects\",\n            \"website\": \"https://github.com/tomasVotruba/lines\",\n            \"command\": {\n                \"composer-bin-plugin\": {\n                    \"package\": \"tomasvotruba/lines\",\n                    \"namespace\": \"lines\",\n                    \"links\": {\"%target-dir%/lines\": \"lines\"}\n                }\n            },\n            \"test\": \"lines --version\",\n            \"tags\": [\"metrics\"]\n        }\n    ]\n}\n"
  },
  {
    "path": "resources/phpcs.json",
    "content": "{\n  \"tools\": [\n    {\n      \"name\": \"phpcs\",\n      \"summary\": \"Detects coding standard violations\",\n      \"website\": \"https://github.com/PHPCSStandards/PHP_CodeSniffer\",\n      \"command\": {\n        \"composer-bin-plugin\": {\n          \"package\": \"squizlabs/php_codesniffer\",\n          \"namespace\": \"phpcs\",\n          \"links\": {\"%target-dir%/phpcs\": \"phpcs\"}\n        }\n      },\n      \"test\": \"phpcs --help\",\n      \"tags\": [\"checkstyle\"]\n    },\n    {\n      \"name\": \"phpcodesniffer-composer-install\",\n      \"summary\": \"Easy installation of PHP_CodeSniffer coding standards (rulesets).\",\n      \"website\": \"https://github.com/PHPCSStandards/composer-installer\",\n      \"command\": {\n        \"sh\": {\n          \"command\": \"composer global bin phpcs config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true\"\n        },\n        \"composer-bin-plugin\": {\n          \"package\": \"dealerdirect/phpcodesniffer-composer-installer\",\n          \"namespace\": \"phpcs\"\n        }\n      },\n      \"test\": \"composer global bin phpcs show dealerdirect/phpcodesniffer-composer-installer\",\n      \"tags\": [\"pre-installation\"]\n    },\n    {\n      \"name\": \"phpcs-security-audit\",\n      \"summary\": \"Finds vulnerabilities and weaknesses related to security in PHP code\",\n      \"website\": \"https://github.com/FloeDesignTechnologies/phpcs-security-audit\",\n      \"command\": {\n        \"composer-bin-plugin\": {\n          \"package\": \"pheromone/phpcs-security-audit\",\n          \"namespace\": \"phpcs\"\n        }\n      },\n      \"test\": \"phpcs -i | grep Security\",\n      \"tags\": [\"security\"]\n    }\n  ]\n}\n"
  },
  {
    "path": "resources/phpstan.json",
    "content": "{\n    \"tools\": [\n        {\n            \"name\": \"phpstan\",\n            \"summary\": \"Static Analysis Tool\",\n            \"website\": \"https://github.com/phpstan/phpstan\",\n            \"command\": {\n                \"composer-bin-plugin\": {\n                    \"package\": \"phpstan/phpstan\",\n                    \"namespace\": \"phpstan\",\n                    \"links\": {\"%target-dir%/phpstan\": \"phpstan\"}\n                }\n            },\n            \"test\": \"phpstan --version\",\n            \"tags\": [\"featured\", \"phpstan\"]\n        },\n        {\n            \"name\": \"phpstan-deprecation-rules\",\n            \"summary\": \"PHPStan rules for detecting deprecated code\",\n            \"website\": \"https://github.com/phpstan/phpstan-deprecation-rules\",\n            \"command\": {\n                \"composer-bin-plugin\": {\n                    \"package\": \"phpstan/phpstan-deprecation-rules\",\n                    \"namespace\": \"phpstan\"\n                }\n            },\n            \"test\": \"composer global bin phpstan show phpstan/phpstan-deprecation-rules\",\n            \"tags\": [\"phpstan\"]\n        },\n        {\n            \"name\": \"phpstan-ergebnis-rules\",\n            \"summary\": \"Additional rules for PHPstan\",\n            \"website\": \"https://github.com/ergebnis/phpstan-rules\",\n            \"command\": {\n                \"composer-bin-plugin\": {\n                    \"package\": \"ergebnis/phpstan-rules\",\n                    \"namespace\": \"phpstan\"\n                }\n            },\n            \"test\": \"composer global bin phpstan show ergebnis/phpstan-rules\",\n            \"tags\": [\"phpstan\"]\n        },\n        {\n            \"name\": \"phpstan-strict-rules\",\n            \"summary\": \"Extra strict and opinionated rules for PHPStan\",\n            \"website\": \"https://github.com/phpstan/phpstan-strict-rules\",\n            \"command\": {\n                \"composer-bin-plugin\": {\n                    \"package\": \"phpstan/phpstan-strict-rules\",\n                    \"namespace\": \"phpstan\"\n                }\n            },\n            \"test\": \"composer global bin phpstan show phpstan/phpstan-strict-rules\",\n            \"tags\": [\"phpstan\"]\n        },\n        {\n            \"name\": \"phpstan-doctrine\",\n            \"summary\": \"Doctrine extensions for PHPStan\",\n            \"website\": \"https://github.com/phpstan/phpstan-doctrine\",\n            \"command\": {\n                \"composer-bin-plugin\": {\n                    \"package\": \"phpstan/phpstan-doctrine\",\n                    \"namespace\": \"phpstan\"\n                }\n            },\n            \"test\": \"composer global bin phpstan show phpstan/phpstan-doctrine\",\n            \"tags\": [\"phpstan\"]\n        },\n        {\n            \"name\": \"phpstan-phpunit\",\n            \"summary\": \"PHPUnit extensions and rules for PHPStan\",\n            \"website\": \"https://github.com/phpstan/phpstan-phpunit\",\n            \"command\": {\n                \"composer-bin-plugin\": {\n                    \"package\": \"phpstan/phpstan-phpunit\",\n                    \"namespace\": \"phpstan\"\n                }\n            },\n            \"test\": \"composer global bin phpstan show phpstan/phpstan-phpunit\",\n            \"tags\": [\"phpstan\"]\n        },\n        {\n            \"name\": \"phpstan-symfony\",\n            \"summary\": \"Symfony extension for PHPStan\",\n            \"website\": \"https://github.com/phpstan/phpstan-symfony\",\n            \"command\": {\n                \"composer-bin-plugin\": {\n                    \"package\": \"phpstan/phpstan-symfony\",\n                    \"namespace\": \"phpstan\"\n                }\n            },\n            \"test\": \"composer global bin phpstan show phpstan/phpstan-symfony\",\n            \"tags\": [\"phpstan\"]\n        },\n        {\n            \"name\": \"phpstan-beberlei-assert\",\n            \"summary\": \"PHPStan extension for beberlei/assert\",\n            \"website\": \"https://github.com/phpstan/phpstan-beberlei-assert\",\n            \"command\": {\n                \"composer-bin-plugin\": {\n                    \"package\": \"phpstan/phpstan-beberlei-assert\",\n                    \"namespace\": \"phpstan\"\n                }\n            },\n            \"test\": \"composer global bin phpstan show phpstan/phpstan-beberlei-assert\",\n            \"tags\": [\"phpstan\"]\n        },\n        {\n            \"name\": \"phpstan-webmozart-assert\",\n            \"summary\": \"PHPStan extension for webmozart/assert\",\n            \"website\": \"https://github.com/phpstan/phpstan-webmozart-assert\",\n            \"command\": {\n                \"composer-bin-plugin\": {\n                    \"package\": \"phpstan/phpstan-webmozart-assert\",\n                    \"namespace\": \"phpstan\"\n                }\n            },\n            \"test\": \"composer global bin phpstan show phpstan/phpstan-webmozart-assert\",\n            \"tags\": [\"phpstan\"]\n        },\n        {\n            \"name\": \"phpat\",\n            \"summary\": \"Easy to use architecture testing tool\",\n            \"website\": \"https://github.com/carlosas/phpat\",\n            \"command\": {\n                \"composer-bin-plugin\": {\n                    \"package\": \"phpat/phpat\",\n                    \"namespace\": \"phpstan\"\n                }\n            },\n            \"test\": \"composer global bin phpstan show phpat/phpat\",\n            \"tags\": [\"phpstan\", \"architecture\"]\n        },\n        {\n            \"name\": \"phpstan-larastan\",\n            \"summary\": \"Separate installation of phpstan for larastan\",\n            \"website\": \"https://github.com/phpstan/phpstan\",\n            \"command\": {\n                \"composer-bin-plugin\": {\n                    \"package\": \"phpstan/phpstan\",\n                    \"namespace\": \"larastan\",\n                    \"links\": {\"%target-dir%/phpstan-larastan\": \"phpstan\"}\n                }\n            },\n            \"test\": \"phpstan-larastan --version\",\n            \"tags\": [\"phpstan\"]\n        },\n        {\n            \"name\": \"larastan\",\n            \"summary\": \"PHPStan extension for Laravel\",\n            \"website\": \"https://github.com/nunomaduro/larastan\",\n            \"command\": {\n                \"composer-bin-plugin\": {\n                    \"package\": \"nunomaduro/larastan\",\n                    \"namespace\": \"larastan\"\n                }\n            },\n            \"test\": \"composer global bin phpstan show ergebnis/phpstan-rules\",\n            \"tags\": [\"phpstan\"]\n        },\n        {\n            \"name\": \"phpstan-banned-code\",\n            \"summary\": \"PHPStan rules for detecting calls to specific functions you don't want in your project\",\n            \"website\": \"https://github.com/ekino/phpstan-banned-code\",\n            \"command\": {\n                \"composer-bin-plugin\": {\n                    \"package\": \"ekino/phpstan-banned-code\",\n                    \"namespace\": \"phpstan\"\n                }\n            },\n            \"test\": \"composer global bin phpstan show ekino/phpstan-banned-code\",\n            \"tags\": [\n                \"phpstan\"\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "resources/pre-installation.json",
    "content": "{\n  \"tools\": [\n    {\n      \"name\": \"composer\",\n      \"summary\": \"Dependency Manager for PHP\",\n      \"website\": \"https://getcomposer.org/\",\n      \"command\": {\n        \"sh\": {\n          \"command\": \"composer self-update\"\n        }\n      },\n      \"test\": \"composer list\",\n      \"tags\": [\"pre-installation\"]\n    },\n    {\n      \"name\": \"phive\",\n      \"summary\": \"PHAR Installation and Verification Environment\",\n      \"website\": \"https://phar.io/\",\n      \"command\": {\n        \"file-download\": {\n          \"url\": \"https://github.com/phar-io/phive/releases/download/0.16.0/phive-0.16.0.phar.asc\",\n          \"file\": \"%target-dir%/phive.asc\"\n        },\n        \"phar-download\": {\n          \"phar\": \"https://github.com/phar-io/phive/releases/download/0.16.0/phive-0.16.0.phar\",\n          \"bin\": \"%target-dir%/phive\"\n        },\n        \"sh\": {\n          \"command\": \"gpg --keyserver hkps://keys.openpgp.org --recv-keys 0x9D8A98B29B2D5D79 && gpg --verify %target-dir%/phive.asc %target-dir%/phive\"\n        }\n      },\n      \"test\": \"phive --version\",\n      \"tags\": [\"pre-installation\"]\n    },\n    {\n      \"name\": \"composer-bin-plugin\",\n      \"summary\": \"Composer plugin to install bin vendors in isolated locations\",\n      \"website\": \"https://github.com/bamarni/composer-bin-plugin\",\n      \"command\": {\n        \"sh\": {\n          \"command\": \"composer global config --json extra.bamarni-bin.bin-links false && composer config --global --json allow-plugins.bamarni/composer-bin-plugin true\"\n        },\n        \"composer-global-install\": {\n          \"package\": \"bamarni/composer-bin-plugin\"\n        }\n      },\n      \"test\": \"composer global show bamarni/composer-bin-plugin\",\n      \"tags\": [\"pre-installation\"]\n    },\n    {\n      \"name\": \"box\",\n      \"summary\": \"Fast, zero config application bundler with PHARs\",\n      \"website\": \"https://github.com/humbug/box\",\n      \"command\": {\n        \"phive-install\": {\n          \"alias\": \"humbug/box\",\n          \"bin\": \"%target-dir%/box\",\n          \"sig\": \"2DF45277AEF09A2F\"\n        }\n      },\n      \"test\": \"box list\",\n      \"tags\": [\"pre-installation\"]\n    }\n  ]\n}\n"
  },
  {
    "path": "resources/psalm.json",
    "content": "{\n    \"tools\": [\n        {\n            \"name\": \"psalm\",\n            \"summary\": \"Finds errors in PHP applications\",\n            \"website\": \"https://psalm.dev/\",\n            \"command\": {\n                \"composer-bin-plugin\": {\n                    \"package\": \"vimeo/psalm\",\n                    \"namespace\": \"psalm\",\n                    \"links\": {\n                        \"%target-dir%/psalm\": \"psalm\",\n                        \"%target-dir%/psalm-language-server\": \"psalm-language-server\",\n                        \"%target-dir%/psalm-plugin\": \"psalm-plugin\",\n                        \"%target-dir%/psalm-refactor\": \"psalm-refactor\",\n                        \"%target-dir%/psalter\": \"psalter\"\n                    }\n                }\n            },\n            \"test\": \"psalm -h\",\n            \"tags\": [\"featured\", \"psalm\"]\n        },\n        {\n            \"name\": \"psalm-plugin-doctrine\",\n            \"summary\": \"Stubs to let Psalm understand Doctrine better\",\n            \"website\": \"https://github.com/weirdan/doctrine-psalm-plugin\",\n            \"command\": {\n                \"composer-bin-plugin\": {\n                    \"package\": \"weirdan/doctrine-psalm-plugin\",\n                    \"namespace\": \"psalm\"\n                }\n            },\n            \"test\": \"cd / && psalm-plugin show | grep weirdan/doctrine-psalm-plugin\",\n            \"tags\": [\"psalm\"]\n        },\n        {\n            \"name\": \"psalm-plugin-phpunit\",\n            \"summary\": \"Psalm plugin for PHPUnit\",\n            \"website\": \"https://github.com/psalm/psalm-plugin-phpunit\",\n            \"command\": {\n                \"composer-bin-plugin\": {\n                    \"package\": \"psalm/plugin-phpunit\",\n                    \"namespace\": \"psalm\"\n                }\n            },\n            \"test\": \"cd / && psalm-plugin show | grep psalm/plugin-phpunit\",\n            \"tags\": [\"psalm\"]\n        },\n        {\n            \"name\": \"psalm-plugin-symfony\",\n            \"summary\": \"Psalm Plugin for Symfony\",\n            \"website\": \"https://github.com/psalm/psalm-plugin-symfony\",\n            \"command\": {\n                \"composer-bin-plugin\": {\n                    \"package\": \"psalm/plugin-symfony\",\n                    \"namespace\": \"psalm\"\n                }\n            },\n            \"test\": \"cd / && psalm-plugin show | grep psalm/plugin-symfony\",\n            \"tags\": [\"psalm\"]\n        }\n    ]\n}\n"
  },
  {
    "path": "resources/refactoring.json",
    "content": "{\n    \"tools\": [\n        {\n            \"name\": \"churn\",\n            \"summary\": \"Discovers good candidates for refactoring\",\n            \"website\": \"https://github.com/bmitch/churn-php\",\n            \"command\": {\n                \"phive-install\": {\n                    \"alias\": \"churn\",\n                    \"bin\": \"%target-dir%/churn\",\n                    \"sig\": \"96141E4421A9B0D5\"\n                }\n            },\n            \"test\": \"churn --version\",\n            \"tags\": [\"featured\", \"refactoring\"]\n        },\n        {\n            \"name\": \"rector\",\n            \"summary\": \"Tool for instant code upgrades and refactoring\",\n            \"website\": \"https://github.com/rectorphp/rector\",\n            \"command\": {\n                \"composer-bin-plugin\": {\n                    \"package\": \"rector/rector\",\n                    \"namespace\": \"rector\",\n                    \"links\": {\"%target-dir%/rector\": \"rector\"}\n                }\n            },\n            \"test\": \"rector --version\",\n            \"tags\": [\"refactoring\"]\n        }\n    ]\n}\n"
  },
  {
    "path": "resources/security.json",
    "content": "{\n    \"tools\": [\n        {\n            \"name\": \"psecio-parse\",\n            \"summary\": \"Scans code for potential security-related issues\",\n            \"website\": \"https://github.com/psecio/parse\",\n            \"command\": {\n                \"composer-bin-plugin\": {\n                    \"package\": \"psecio/parse:dev-master\",\n                    \"namespace\": \"legacy-php-parser\",\n                    \"links\": {\"%target-dir%/psecio-parse\": \"psecio-parse\"}\n                }\n            },\n            \"test\": \"psecio-parse --version\",\n            \"tags\": [\"security\"]\n        },\n        {\n            \"name\": \"local-php-security-checker\",\n            \"summary\": \"Checks composer dependencies for known security vulnerabilities\",\n            \"website\": \"https://github.com/fabpot/local-php-security-checker\",\n            \"command\": {\n                \"file-download\": {\n                    \"url\": \"https://github.com/fabpot/local-php-security-checker/releases/download/v2.0.6/local-php-security-checker_2.0.6_linux_amd64\",\n                    \"file\": \"%target-dir%/local-php-security-checker\"\n                },\n                \"sh\": {\n                    \"command\": \"chmod +x %target-dir%/local-php-security-checker\"\n                }\n            },\n            \"test\": \"local-php-security-checker --help\",\n            \"tags\": [\"featured\", \"security\"]\n        }\n    ]\n}\n"
  },
  {
    "path": "resources/test.json",
    "content": "{\n    \"tools\": [\n        {\n            \"name\": \"behat\",\n            \"summary\": \"Helps to test business expectations\",\n            \"website\": \"http://behat.org/\",\n            \"command\": {\n                \"composer-bin-plugin\": {\n                    \"package\": \"behat/behat\",\n                    \"namespace\": \"behat\",\n                    \"links\": {\"%target-dir%/behat\": \"behat\"}\n                }\n            },\n            \"test\": \"behat --version\",\n            \"tags\": [\"featured\", \"test\"]\n        },\n        {\n            \"name\": \"codeception\",\n            \"summary\": \"Codeception is a BDD-styled PHP testing framework\",\n            \"website\": \"https://codeception.com/\",\n            \"command\": {\n                \"phar-download\": {\n                    \"phar\": \"https://codeception.com/codecept.phar\",\n                    \"bin\": \"%target-dir%/codeception\"\n                }\n            },\n            \"test\": \"codeception --version\",\n            \"tags\": [\"test\", \"exclude-php:8.5\"]\n        },\n        {\n            \"name\": \"infection\",\n            \"summary\": \"AST based PHP Mutation Testing Framework\",\n            \"website\": \"https://infection.github.io/\",\n            \"command\": {\n                \"phive-install\": {\n                    \"alias\": \"infection\",\n                    \"bin\": \"%target-dir%/infection\",\n                    \"sig\": \"C5095986493B4AA0\"\n                }\n            },\n            \"test\": \"infection --version\",\n            \"tags\": [\"featured\", \"test\"]\n        },\n        {\n            \"name\": \"paratest\",\n            \"summary\": \"Parallel testing for PHPUnit\",\n            \"website\": \"https://github.com/paratestphp/paratest\",\n            \"command\": {\n                \"composer-bin-plugin\": {\n                    \"package\": \"brianium/paratest\",\n                    \"namespace\": \"paratest\",\n                    \"links\": {\"%target-dir%/paratest\": \"paratest\"}\n                }\n            },\n            \"test\": \"paratest --version\",\n            \"tags\": [\"test\"]\n        },\n        {\n            \"name\": \"phpcov\",\n            \"summary\": \"a command-line frontend for the PHP_CodeCoverage library\",\n            \"website\": \"https://github.com/sebastianbergmann/phpcov\",\n            \"command\": {\n                \"phive-install\": {\n                    \"alias\": \"phpcov\",\n                    \"bin\": \"%target-dir%/phpcov\",\n                    \"sig\": \"4AA394086372C20A\"\n                }\n            },\n            \"test\": \"phpcov -v\",\n            \"tags\": [\"test\", \"exclude-php:8.2\", \"exclude-php:8.3\"]\n        },\n        {\n            \"name\": \"php-fuzzer\",\n            \"summary\": \"A fuzzer for PHP, which can be used to find bugs in libraries by feeding them 'random' inputs\",\n            \"website\": \"https://github.com/nikic/PHP-Fuzzer\",\n            \"command\": {\n                \"phive-install\": {\n                    \"alias\": \"nikic/php-fuzzer\",\n                    \"bin\": \"%target-dir%/php-fuzzer\"\n                }\n            },\n            \"test\": \"php-fuzzer --help | grep 'Usage:'\",\n            \"tags\": [\"test\"]\n        },\n        {\n            \"name\": \"phpspec\",\n            \"summary\": \"SpecBDD Framework\",\n            \"website\": \"http://www.phpspec.net/\",\n            \"command\": {\n                \"phive-install\": {\n                    \"alias\": \"phpspec/phpspec\",\n                    \"bin\": \"%target-dir%/phpspec\"\n                }\n            },\n            \"test\": \"phpspec --version\",\n            \"tags\": [\"featured\", \"test\", \"exclude-php:8.5\"]\n        },\n        {\n            \"name\": \"phpunit\",\n            \"summary\": \"The PHP testing framework\",\n            \"website\": \"https://phpunit.de/\",\n            \"command\": {\n                \"phive-install\": {\n                    \"alias\": \"phpunit\",\n                    \"bin\": \"%target-dir%/phpunit\",\n                    \"sig\": \"4AA394086372C20A\"\n                }\n            },\n            \"test\": \"phpunit --version\",\n            \"tags\": [\"featured\", \"test\", \"exclude-php:8.2\", \"exclude-php:8.3\"]\n        },\n        {\n            \"name\": \"phpunit-12\",\n            \"summary\": \"The PHP testing framework (12.x version)\",\n            \"website\": \"https://phpunit.de/\",\n            \"command\": {\n                \"phive-install\": {\n                    \"alias\": \"phpunit@^12.0\",\n                    \"bin\": \"%target-dir%/phpunit-12\",\n                    \"sig\": \"4AA394086372C20A\"\n                }\n            },\n            \"test\": \"phpunit-12 --version\",\n            \"tags\": [\"test\", \"exclude-php:8.2\"]\n        },\n        {\n            \"name\": \"phpunit-11\",\n            \"summary\": \"The PHP testing framework (11.x version)\",\n            \"website\": \"https://phpunit.de/\",\n            \"command\": {\n                \"phive-install\": {\n                    \"alias\": \"phpunit@^11.0\",\n                    \"bin\": \"%target-dir%/phpunit-11\",\n                    \"sig\": \"4AA394086372C20A\"\n                }\n            },\n            \"test\": \"phpunit-11 --version\",\n            \"tags\": [\"test\"]\n        },\n        {\n            \"name\": \"phpunit-10\",\n            \"summary\": \"The PHP testing framework (10.x version)\",\n            \"website\": \"https://phpunit.de/\",\n            \"command\": {\n                \"phive-install\": {\n                    \"alias\": \"phpunit@^10.0\",\n                    \"bin\": \"%target-dir%/phpunit-10\",\n                    \"sig\": \"4AA394086372C20A\"\n                }\n            },\n            \"test\": \"phpunit-10 --version\",\n            \"tags\": [\"test\"]\n        },\n        {\n            \"name\": \"phpunit-9\",\n            \"summary\": \"The PHP testing framework (9.x version)\",\n            \"website\": \"https://phpunit.de/\",\n            \"command\": {\n                \"phive-install\": {\n                    \"alias\": \"phpunit@^9.0\",\n                    \"bin\": \"%target-dir%/phpunit-9\",\n                    \"sig\": \"4AA394086372C20A\"\n                }\n            },\n            \"test\": \"phpunit-9 --version\",\n            \"tags\": [\"test\"]\n        },\n        {\n            \"name\": \"phpunit-8\",\n            \"summary\": \"The PHP testing framework (8.x version)\",\n            \"website\": \"https://phpunit.de/\",\n            \"command\": {\n                \"phive-install\": {\n                    \"alias\": \"phpunit@^8.0\",\n                    \"bin\": \"%target-dir%/phpunit-8\",\n                    \"sig\": \"4AA394086372C20A\"\n                }\n            },\n            \"test\": \"phpunit-8 --version\",\n            \"tags\": [\"test\"]\n        },\n        {\n            \"name\": \"simple-phpunit\",\n            \"summary\": \"Provides utilities to report legacy tests and usage of deprecated code\",\n            \"website\": \"https://symfony.com/doc/current/components/phpunit_bridge.html\",\n            \"command\": {\n                \"composer-bin-plugin\": {\n                    \"package\": \"symfony/phpunit-bridge\",\n                    \"namespace\": \"symfony\",\n                    \"links\": {\"%target-dir%/simple-phpunit\": \"simple-phpunit\"}\n                },\n                \"sh\": {\n                    \"command\": \"simple-phpunit install && SYMFONY_PHPUNIT_VERSION=9 simple-phpunit install\"\n                }\n            },\n            \"test\": \"simple-phpunit --version\",\n            \"tags\": [\"test\"]\n        },\n        {\n            \"name\": \"kahlan\",\n            \"summary\": \"Kahlan is a full-featured Unit & BDD test framework a la RSpec/JSpec\",\n            \"website\": \"https://kahlan.github.io/docs/\",\n            \"command\": {\n                \"composer-bin-plugin\": {\n                    \"package\": \"kahlan/kahlan\",\n                    \"namespace\": \"kahlan\",\n                    \"links\": {\"%target-dir%/kahlan\": \"kahlan\"}\n                }\n            },\n            \"test\": \"kahlan --version\",\n            \"tags\": [\"test\"]\n        }\n    ]\n}\n"
  },
  {
    "path": "resources/tools.json",
    "content": "{\n  \"tools\": [\n    {\n      \"name\": \"diffFilter\",\n      \"summary\": \"Applies QA tools to run on a single pull request\",\n      \"website\": \"https://github.com/exussum12/coverageChecker\",\n      \"command\": {\n        \"composer-bin-plugin\": {\n          \"package\": \"exussum12/coverage-checker\",\n          \"namespace\": \"tools\",\n          \"links\": {\"%target-dir%/diffFilter\": \"diffFilter\"}\n        }\n      },\n      \"test\": \"diffFilter -v\",\n      \"tags\": []\n    },\n    {\n      \"name\": \"phan\",\n      \"summary\": \"Static Analysis Tool\",\n      \"website\": \"https://github.com/phan/phan\",\n      \"command\": {\n        \"phar-download\": {\n          \"phar\": \"https://github.com/phan/phan/releases/latest/download/phan.phar\",\n          \"bin\": \"%target-dir%/phan\"\n        }\n      },\n      \"test\": \"phan -v\",\n      \"tags\": [\"featured\"]\n    },\n    {\n      \"name\": \"phpbench\",\n      \"summary\": \"PHP Benchmarking framework\",\n      \"website\": \"https://github.com/phpbench/phpbench\",\n      \"command\": {\n        \"phive-install\": {\n          \"alias\": \"phpbench\",\n          \"bin\": \"%target-dir%/phpbench\",\n          \"sig\": \"6FC579F5F0FCC966\"\n        }\n      },\n      \"test\": \"phpbench -V\",\n      \"tags\": []\n    },\n    {\n      \"name\": \"phpa\",\n      \"summary\": \"Checks for weak assumptions\",\n      \"website\": \"https://github.com/rskuipers/php-assumptions\",\n      \"command\": {\n        \"composer-bin-plugin\": {\n          \"package\": \"rskuipers/php-assumptions\",\n          \"namespace\": \"tools\",\n          \"links\": {\"%target-dir%/phpa\": \"phpa\"}\n        }\n      },\n      \"test\": \"phpa --version\",\n      \"tags\": [\"not-maintained\"]\n    },\n    {\n      \"name\": \"phpca\",\n      \"summary\": \"Finds usage of non-built-in extensions\",\n      \"website\": \"https://github.com/wapmorgan/PhpCodeAnalyzer\",\n      \"command\": {\n        \"composer-bin-plugin\": {\n          \"package\": \"wapmorgan/php-code-analyzer\",\n          \"namespace\": \"tools\",\n          \"links\": {\"%target-dir%/phpca\": \"phpca\"}\n        }\n      },\n      \"test\": \"phpca -h\"\n    },\n    {\n      \"name\": \"phpcpd\",\n      \"summary\": \"Copy/Paste Detector\",\n      \"website\": \"https://github.com/sebastianbergmann/phpcpd\",\n      \"command\": {\n        \"phive-install\": {\n          \"alias\": \"phpcpd\",\n          \"bin\": \"%target-dir%/phpcpd\",\n          \"sig\": \"4AA394086372C20A\"\n        }\n      },\n      \"test\": \"phpcpd -v\",\n      \"tags\": [\"featured\"]\n    },\n    {\n      \"name\": \"phpmd\",\n      \"summary\": \"A tool for finding problems in PHP code\",\n      \"website\": \"https://phpmd.org/\",\n      \"command\": {\n        \"phive-install\": {\n          \"alias\": \"phpmd\",\n          \"bin\": \"%target-dir%/phpmd\",\n          \"sig\": \"9093F8B32E4815AA\"\n        }\n      },\n      \"test\": \"phpmd --version\"\n    },\n    {\n      \"name\": \"phpmnd\",\n      \"summary\": \"Helps to detect magic numbers\",\n      \"website\": \"https://github.com/povils/phpmnd\",\n      \"command\": {\n        \"composer-bin-plugin\": {\n          \"package\": \"povils/phpmnd\",\n          \"namespace\": \"phpmnd\",\n          \"links\": {\"%target-dir%/phpmnd\": \"phpmnd\"}\n        }\n      },\n      \"test\": \"phpmnd -V\"\n    }\n  ]\n}\n\n"
  },
  {
    "path": "scoper.inc.php",
    "content": "<?php declare(strict_types=1);\n\nreturn [\n    // Whitelist globals so that Symfony polyfills are not scoped\n    'expose-global-constants' => true,\n    'expose-global-classes' => true,\n    'expose-global-functions' => true,\n    'exclude-files' => [\n        'vendor/symfony/polyfill-php80/Resources/stubs/Stringable.php',\n        'vendor/symfony/polyfill-php80/bootstrap.php',\n        'vendor/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php',\n        'vendor/symfony/polyfill-intl-normalizer/bootstrap.php',\n    ],\n    'expose-namespaces' => [\n        'Symfony\\Polyfill\\Php80',\n        'Symfony\\Polyfill\\Intl',\n    ],\n];\n"
  },
  {
    "path": "src/Cli/Application.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Cli;\n\nuse Psr\\Container\\ContainerInterface;\nuse Symfony\\Component\\Console\\Application as CliApplication;\nuse Symfony\\Component\\Console\\CommandLoader\\CommandLoaderInterface;\nuse Symfony\\Component\\Console\\CommandLoader\\ContainerCommandLoader;\nuse Symfony\\Component\\Console\\Input\\InputDefinition;\nuse Symfony\\Component\\Console\\Input\\InputInterface;\nuse Symfony\\Component\\Console\\Input\\InputOption;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\nuse Zalas\\Toolbox\\Cli\\Command\\InstallCommand;\nuse Zalas\\Toolbox\\Cli\\Command\\ListCommand;\nuse Zalas\\Toolbox\\Cli\\Command\\TestCommand;\n\nfinal class Application extends CliApplication\n{\n    private ServiceContainer $serviceContainer;\n\n    public function __construct(string $version, ServiceContainer $serviceContainer)\n    {\n        parent::__construct('toolbox', $version);\n\n        $this->serviceContainer = $serviceContainer;\n\n        $this->setCommandLoader($this->createCommandLoader($serviceContainer));\n    }\n\n    /**\n     * @throws \\Throwable\n     */\n    public function doRun(InputInterface $input, OutputInterface $output): int\n    {\n        $this->serviceContainer->set(InputInterface::class, $input);\n        $this->serviceContainer->set(OutputInterface::class, $output);\n\n        return parent::doRun($input, $output);\n    }\n\n    protected function getDefaultInputDefinition(): InputDefinition\n    {\n        $definition = parent::getDefaultInputDefinition();\n        $definition->addOption(new InputOption('tools', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Path(s) to the list of tools. Can also be set with TOOLBOX_JSON environment variable.', $this->toolsJsonDefault()));\n\n        return $definition;\n    }\n\n    private function toolsJsonDefault(): array\n    {\n        return \\getenv('TOOLBOX_JSON')\n            ? \\array_map('trim', \\explode(',', \\getenv('TOOLBOX_JSON')))\n            : [\n                __DIR__.'/../../resources/pre-installation.json',\n                __DIR__.'/../../resources/architecture.json',\n                __DIR__.'/../../resources/checkstyle.json',\n                __DIR__.'/../../resources/compatibility.json',\n                __DIR__.'/../../resources/composer.json',\n                __DIR__.'/../../resources/deprecation.json',\n                __DIR__.'/../../resources/documentation.json',\n                __DIR__.'/../../resources/linting.json',\n                __DIR__.'/../../resources/metrics.json',\n                __DIR__.'/../../resources/phpcs.json',\n                __DIR__.'/../../resources/phpstan.json',\n                __DIR__.'/../../resources/psalm.json',\n                __DIR__.'/../../resources/refactoring.json',\n                __DIR__.'/../../resources/security.json',\n                __DIR__.'/../../resources/test.json',\n                __DIR__.'/../../resources/tools.json',\n            ];\n    }\n\n    private function createCommandLoader(ContainerInterface $container): CommandLoaderInterface\n    {\n        return new ContainerCommandLoader(\n            $container,\n            [\n                InstallCommand::NAME => InstallCommand::class,\n                ListCommand::NAME => ListCommand::class,\n                TestCommand::NAME => TestCommand::class,\n            ]\n        );\n    }\n}\n"
  },
  {
    "path": "src/Cli/Command/DefaultTag.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Cli\\Command;\n\ntrait DefaultTag\n{\n    private function defaultExcludeTag(): array\n    {\n        return \\getenv('TOOLBOX_EXCLUDED_TAGS') ? \\explode(',', \\getenv('TOOLBOX_EXCLUDED_TAGS')) : [];\n    }\n\n    private function defaultTag(): array\n    {\n        return \\getenv('TOOLBOX_TAGS') ? \\explode(',', \\getenv('TOOLBOX_TAGS')) : [];\n    }\n}\n"
  },
  {
    "path": "src/Cli/Command/DefaultTargetDir.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Cli\\Command;\n\ntrait DefaultTargetDir\n{\n    private function defaultTargetDir(): string\n    {\n        return \\getenv('TOOLBOX_TARGET_DIR') ? \\getenv('TOOLBOX_TARGET_DIR') : '/usr/local/bin';\n    }\n}\n"
  },
  {
    "path": "src/Cli/Command/InstallCommand.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Cli\\Command;\n\nuse Symfony\\Component\\Console\\Command\\Command;\nuse Symfony\\Component\\Console\\Input\\InputInterface;\nuse Symfony\\Component\\Console\\Input\\InputOption;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\nuse Zalas\\Toolbox\\Runner\\Runner;\nuse Zalas\\Toolbox\\Tool\\Filter;\nuse Zalas\\Toolbox\\UseCase\\InstallTools;\n\nfinal class InstallCommand extends Command\n{\n    use DefaultTag;\n    use DefaultTargetDir;\n\n    public const NAME = 'install';\n\n    private InstallTools $useCase;\n    private Runner $runner;\n\n    public function __construct(InstallTools $useCase, Runner $runner)\n    {\n        parent::__construct(self::NAME);\n\n        $this->useCase = $useCase;\n        $this->runner = $runner;\n    }\n\n    protected function configure(): void\n    {\n        $this->setDescription('Installs tools');\n        $this->addOption('dry-run', null, InputOption::VALUE_NONE, 'Output the command without executing it');\n        $this->addOption('target-dir', null, InputOption::VALUE_REQUIRED, 'The target installation directory', $this->defaultTargetDir());\n        $this->addOption('exclude-tag', 'e', InputOption::VALUE_REQUIRED|InputOption::VALUE_IS_ARRAY, 'Tool tags to exclude', $this->defaultExcludeTag());\n        $this->addOption('tag', 't', InputOption::VALUE_REQUIRED|InputOption::VALUE_IS_ARRAY, 'Tool tags to filter by', $this->defaultTag());\n    }\n\n    protected function execute(InputInterface $input, OutputInterface $output): int\n    {\n        return $this->runner->run(\\call_user_func($this->useCase, new Filter($input->getOption('exclude-tag'), $input->getOption('tag'))));\n    }\n}\n"
  },
  {
    "path": "src/Cli/Command/ListCommand.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Cli\\Command;\n\nuse Symfony\\Component\\Console\\Command\\Command;\nuse Symfony\\Component\\Console\\Input\\InputInterface;\nuse Symfony\\Component\\Console\\Input\\InputOption;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\nuse Symfony\\Component\\Console\\Style\\StyleInterface;\nuse Symfony\\Component\\Console\\Style\\SymfonyStyle;\nuse Zalas\\Toolbox\\Tool\\Filter;\nuse Zalas\\Toolbox\\Tool\\Tool;\nuse Zalas\\Toolbox\\UseCase\\ListTools;\n\nfinal class ListCommand extends Command\n{\n    use DefaultTag;\n\n    public const NAME = 'list-tools';\n\n    private ListTools $listTools;\n\n    public function __construct(ListTools $listTools)\n    {\n        parent::__construct(self::NAME);\n\n        $this->listTools = $listTools;\n    }\n\n    protected function configure(): void\n    {\n        $this->setDescription('Lists available tools');\n        $this->addOption('exclude-tag', 'e', InputOption::VALUE_REQUIRED|InputOption::VALUE_IS_ARRAY, 'Tool tags to exclude', $this->defaultExcludeTag());\n        $this->addOption('tag', 't', InputOption::VALUE_REQUIRED|InputOption::VALUE_IS_ARRAY, 'Tool tags to filter by', $this->defaultTag());\n    }\n\n    protected function execute(InputInterface $input, OutputInterface $output): int\n    {\n        $tools = \\call_user_func($this->listTools, new Filter($input->getOption('exclude-tag'), $input->getOption('tag')));\n\n        $style = $this->createStyle($input, $output);\n        $style->title('Available tools');\n        $style->table(\n            ['Name', 'Summary'],\n            $tools->map(function (Tool $tool) {\n                return [\\sprintf('<info>%s</info>', $tool->name()), $tool->summary().PHP_EOL.$tool->website().PHP_EOL];\n            })->toArray()\n        );\n\n        return 0;\n    }\n\n    private function createStyle(InputInterface $input, OutputInterface $output): StyleInterface\n    {\n        return new SymfonyStyle($input, $output);\n    }\n}\n"
  },
  {
    "path": "src/Cli/Command/TestCommand.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Cli\\Command;\n\nuse Symfony\\Component\\Console\\Command\\Command;\nuse Symfony\\Component\\Console\\Input\\InputInterface;\nuse Symfony\\Component\\Console\\Input\\InputOption;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\nuse Zalas\\Toolbox\\Runner\\Runner;\nuse Zalas\\Toolbox\\Tool\\Filter;\nuse Zalas\\Toolbox\\UseCase\\TestTools;\n\nfinal class TestCommand extends Command\n{\n    use DefaultTag;\n    use DefaultTargetDir;\n\n    public const NAME = 'test';\n\n    private TestTools $useCase;\n    private Runner $runner;\n\n    public function __construct(TestTools $useCase, Runner $runner)\n    {\n        parent::__construct(self::NAME);\n\n        $this->useCase = $useCase;\n        $this->runner = $runner;\n    }\n\n    protected function configure(): void\n    {\n        $this->setDescription('Runs basic tests to verify tools are installed');\n        $this->addOption('dry-run', null, InputOption::VALUE_NONE, 'Output the command without executing it');\n        $this->addOption('target-dir', null, InputOption::VALUE_REQUIRED, 'The target installation directory', $this->defaultTargetDir());\n        $this->addOption('exclude-tag', 'e', InputOption::VALUE_REQUIRED|InputOption::VALUE_IS_ARRAY, 'Tool tags to exclude', $this->defaultExcludeTag());\n        $this->addOption('tag', 't', InputOption::VALUE_REQUIRED|InputOption::VALUE_IS_ARRAY, 'Tool tags to filter by', $this->defaultTag());\n    }\n\n    protected function execute(InputInterface $input, OutputInterface $output): int\n    {\n        return $this->runner->run(\\call_user_func($this->useCase, new Filter($input->getOption('exclude-tag'), $input->getOption('tag'))));\n    }\n}\n"
  },
  {
    "path": "src/Cli/Runner/DryRunner.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Cli\\Runner;\n\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\nuse Zalas\\Toolbox\\Runner\\Runner;\nuse Zalas\\Toolbox\\Tool\\Command;\n\nfinal class DryRunner implements Runner\n{\n    private OutputInterface $output;\n\n    public function __construct(OutputInterface $output)\n    {\n        $this->output = $output;\n    }\n\n    public function run(Command $command): int\n    {\n        $this->output->writeln((string) $command);\n\n        return 0;\n    }\n}\n"
  },
  {
    "path": "src/Cli/ServiceContainer/LazyRunner.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Cli\\ServiceContainer;\n\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Zalas\\Toolbox\\Runner\\Runner;\nuse Zalas\\Toolbox\\Tool\\Command;\n\nfinal class LazyRunner implements Runner\n{\n    private ?Runner $runner = null;\n\n    private RunnerFactory $factory;\n\n    public function __construct(RunnerFactory $factory)\n    {\n        $this->factory = $factory;\n    }\n\n    /**\n     * @throws ContainerExceptionInterface\n     * @throws NotFoundExceptionInterface\n     */\n    public function run(Command $command): int\n    {\n        return $this->runner()->run($command);\n    }\n\n    /**\n     * @throws ContainerExceptionInterface\n     * @throws NotFoundExceptionInterface\n     */\n    private function runner(): Runner\n    {\n        if (null === $this->runner) {\n            $this->runner = $this->factory->createRunner();\n        }\n\n        return $this->runner;\n    }\n}\n"
  },
  {
    "path": "src/Cli/ServiceContainer/RunnerFactory.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Cli\\ServiceContainer;\n\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\ContainerInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Symfony\\Component\\Console\\Input\\InputInterface;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\nuse Zalas\\Toolbox\\Cli\\Runner\\DryRunner;\nuse Zalas\\Toolbox\\Runner\\ParametrisedRunner;\nuse Zalas\\Toolbox\\Runner\\PassthruRunner;\nuse Zalas\\Toolbox\\Runner\\Runner;\n\nclass RunnerFactory\n{\n    private ContainerInterface $container;\n\n    public function __construct(ContainerInterface $container)\n    {\n        $this->container = $container;\n    }\n\n    /**\n     * @throws ContainerExceptionInterface\n     * @throws NotFoundExceptionInterface\n     */\n    public function createRunner(): Runner\n    {\n        $runner = $this->createRealRunner();\n\n        if ($parameters = $this->parameters()) {\n            return new ParametrisedRunner($runner, $parameters);\n        }\n\n        return $runner;\n    }\n\n    /**\n     * @throws ContainerExceptionInterface\n     * @throws NotFoundExceptionInterface\n     */\n    private function createRealRunner(): DryRunner|PassthruRunner\n    {\n        if ($this->container->get(InputInterface::class)->getOption('dry-run')) {\n            return new DryRunner($this->container->get(OutputInterface::class));\n        }\n\n        return new PassthruRunner();\n    }\n\n    /**\n     * @throws ContainerExceptionInterface\n     * @throws NotFoundExceptionInterface\n     */\n    private function parameters(): array\n    {\n        if ($targetDir = $this->targetDir()) {\n            return ['%target-dir%' => $targetDir];\n        }\n\n        return [];\n    }\n\n    /**\n     * @throws ContainerExceptionInterface\n     * @throws NotFoundExceptionInterface\n     */\n    private function targetDir(): ?string\n    {\n        if (!$this->container->get(InputInterface::class)->hasOption('target-dir')) {\n            return null;\n        }\n\n        $targetDir = $this->container->get(InputInterface::class)->getOption('target-dir');\n\n        if (!\\is_dir($targetDir)) {\n            throw new class(\\sprintf('The target dir does not exist: \"%s\".', $targetDir)) extends \\RuntimeException implements ContainerExceptionInterface {\n            };\n        }\n\n        return \\realpath($targetDir);\n    }\n}\n"
  },
  {
    "path": "src/Cli/ServiceContainer.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Cli;\n\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\ContainerInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse RuntimeException;\nuse Symfony\\Component\\Console\\Input\\InputInterface;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\nuse Zalas\\Toolbox\\Cli\\Command\\InstallCommand;\nuse Zalas\\Toolbox\\Cli\\Command\\ListCommand;\nuse Zalas\\Toolbox\\Cli\\Command\\TestCommand;\nuse Zalas\\Toolbox\\Cli\\ServiceContainer\\LazyRunner;\nuse Zalas\\Toolbox\\Cli\\ServiceContainer\\RunnerFactory;\nuse Zalas\\Toolbox\\Json\\JsonTools;\nuse Zalas\\Toolbox\\Runner\\Runner;\nuse Zalas\\Toolbox\\Tool\\Tools;\nuse Zalas\\Toolbox\\UseCase\\InstallTools;\nuse Zalas\\Toolbox\\UseCase\\ListTools;\nuse Zalas\\Toolbox\\UseCase\\TestTools;\n\nclass ServiceContainer implements ContainerInterface\n{\n    private array $services = [\n        InstallCommand::class => 'createInstallCommand',\n        ListCommand::class => 'createListCommand',\n        TestCommand::class => 'createTestCommand',\n        Runner::class => 'createRunner',\n        InstallTools::class => 'createInstallToolsUseCase',\n        ListTools::class => 'createListToolsUseCase',\n        TestTools::class => 'createTestToolsUseCase',\n        Tools::class => 'createTools',\n    ];\n\n    private array $runtimeServices = [\n        InputInterface::class => null,\n        OutputInterface::class => null,\n    ];\n\n    public function set(string $id, /*object */$service): void\n    {\n        if (!\\array_key_exists($id, $this->runtimeServices)) {\n            throw new class(\\sprintf('The \"%s\" runtime service is not expected.', $id)) extends RuntimeException implements ContainerExceptionInterface {\n            };\n        }\n\n        $this->runtimeServices[$id] = $service;\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function get(string $id)\n    {\n        if (isset($this->runtimeServices[$id])) {\n            return $this->runtimeServices[$id];\n        }\n\n        if (isset($this->services[$id])) {\n            return \\call_user_func([$this, $this->services[$id]]);\n        }\n\n        throw new class(\\sprintf('The \"%s\" service is not registered in the service container.', $id)) extends RuntimeException implements NotFoundExceptionInterface {\n        };\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function has(string $id): bool\n    {\n        return isset($this->services[$id]) || isset($this->runtimeServices[$id]);\n    }\n\n    /**\n     * @throws ContainerExceptionInterface\n     * @throws NotFoundExceptionInterface\n     */\n    private function createInstallCommand(): InstallCommand\n    {\n        return new InstallCommand($this->get(InstallTools::class), $this->get(Runner::class));\n    }\n\n    /**\n     * @throws ContainerExceptionInterface\n     * @throws NotFoundExceptionInterface\n     */\n    private function createListCommand(): ListCommand\n    {\n        return new ListCommand($this->get(ListTools::class));\n    }\n\n    /**\n     * @throws ContainerExceptionInterface\n     * @throws NotFoundExceptionInterface\n     */\n    private function createTestCommand(): TestCommand\n    {\n        return new TestCommand($this->get(TestTools::class), $this->get(Runner::class));\n    }\n\n    private function createRunner(): Runner\n    {\n        return new LazyRunner(new RunnerFactory($this));\n    }\n\n    /**\n     * @throws ContainerExceptionInterface\n     * @throws NotFoundExceptionInterface\n     */\n    private function createInstallToolsUseCase(): InstallTools\n    {\n        return new InstallTools($this->get(Tools::class));\n    }\n\n    /**\n     * @throws ContainerExceptionInterface\n     * @throws NotFoundExceptionInterface\n     */\n    private function createListToolsUseCase(): ListTools\n    {\n        return new ListTools($this->get(Tools::class));\n    }\n\n    /**\n     * @throws ContainerExceptionInterface\n     * @throws NotFoundExceptionInterface\n     */\n    private function createTestToolsUseCase(): TestTools\n    {\n        return new TestTools($this->get(Tools::class));\n    }\n\n    private function createTools(): Tools\n    {\n        return new JsonTools(function (): array {\n            return $this->get(InputInterface::class)->getOption('tools');\n        });\n    }\n}\n"
  },
  {
    "path": "src/Json/Factory/Assert.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Json\\Factory;\n\nfinal class Assert\n{\n    public static function requireFields(array $fields, array $data, string $type): void\n    {\n        $missingFields = \\array_filter($fields, function (string $field) use ($data) {\n            return !isset($data[$field]);\n        });\n\n        if (!empty($missingFields)) {\n            throw new \\InvalidArgumentException(\\sprintf('Missing fields \"%s\" in the %s: `%s`.', \\implode(', ', $missingFields), $type, \\json_encode($data)));\n        }\n    }\n}\n"
  },
  {
    "path": "src/Json/Factory/BoxBuildCommandFactory.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Json\\Factory;\n\nuse Zalas\\Toolbox\\Tool\\Command;\nuse Zalas\\Toolbox\\Tool\\Command\\BoxBuildCommand;\n\nfinal class BoxBuildCommandFactory\n{\n    public static function import(array $definition): Command\n    {\n        Assert::requireFields(['repository', 'phar', 'bin'], $definition, 'BoxBuildCommand');\n\n        return new BoxBuildCommand($definition['repository'], $definition['phar'], $definition['bin'], \\sys_get_temp_dir(), $definition['version'] ?? null);\n    }\n}\n"
  },
  {
    "path": "src/Json/Factory/ComposerBinPluginCommandFactory.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Json\\Factory;\n\nuse Zalas\\Toolbox\\Tool\\Collection;\nuse Zalas\\Toolbox\\Tool\\Command;\nuse Zalas\\Toolbox\\Tool\\Command\\ComposerBinPluginCommand;\nuse Zalas\\Toolbox\\Tool\\Command\\ComposerBinPluginLinkCommand;\n\nfinal class ComposerBinPluginCommandFactory\n{\n    public static function import(array $command): Command\n    {\n        Assert::requireFields(['package', 'namespace'], $command, 'ComposerBinPluginCommand');\n\n        return new ComposerBinPluginCommand($command['package'], $command['namespace'], self::importLinks($command));\n    }\n\n    private static function importLinks(array $command): Collection\n    {\n        $links = $command['links'] ?? [];\n        $namespace = $command['namespace'];\n\n        return Collection::create(\n            \\array_map(function (string $source, string $target) use ($namespace) {\n                return new ComposerBinPluginLinkCommand($source, $target, $namespace);\n            }, $links, \\array_keys($links))\n        );\n    }\n}\n"
  },
  {
    "path": "src/Json/Factory/ComposerGlobalInstallCommandFactory.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Json\\Factory;\n\nuse Zalas\\Toolbox\\Tool\\Command;\nuse Zalas\\Toolbox\\Tool\\Command\\ComposerGlobalInstallCommand;\n\nfinal class ComposerGlobalInstallCommandFactory\n{\n    public static function import(array $command): Command\n    {\n        Assert::requireFields(['package'], $command, 'ComposerGlobalInstallCommand');\n\n        return new ComposerGlobalInstallCommand($command['package']);\n    }\n}\n"
  },
  {
    "path": "src/Json/Factory/ComposerInstallCommandFactory.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Json\\Factory;\n\nuse Zalas\\Toolbox\\Tool\\Command;\nuse Zalas\\Toolbox\\Tool\\Command\\ComposerInstallCommand;\n\nfinal class ComposerInstallCommandFactory\n{\n    public static function import(array $command): Command\n    {\n        Assert::requireFields(['repository', 'target-dir'], $command, 'ComposerInstallCommand');\n\n        return new ComposerInstallCommand($command['repository'], $command['target-dir'], $command['version'] ?? null);\n    }\n}\n"
  },
  {
    "path": "src/Json/Factory/FileDownloadCommandFactory.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Json\\Factory;\n\nuse Zalas\\Toolbox\\Tool\\Command;\nuse Zalas\\Toolbox\\Tool\\Command\\FileDownloadCommand;\n\nfinal class FileDownloadCommandFactory\n{\n    public static function import(array $command): Command\n    {\n        Assert::requireFields(['url', 'file'], $command, 'FileDownloadCommand');\n\n        return new FileDownloadCommand($command['url'], $command['file']);\n    }\n}\n"
  },
  {
    "path": "src/Json/Factory/PharDownloadCommandFactory.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Json\\Factory;\n\nuse Zalas\\Toolbox\\Tool\\Command;\nuse Zalas\\Toolbox\\Tool\\Command\\PharDownloadCommand;\n\nfinal class PharDownloadCommandFactory\n{\n    public static function import(array $command): Command\n    {\n        Assert::requireFields(['phar', 'bin'], $command, 'PharDownloadCommand');\n\n        return new PharDownloadCommand($command['phar'], $command['bin']);\n    }\n}\n"
  },
  {
    "path": "src/Json/Factory/PhiveInstallCommandFactory.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Json\\Factory;\n\nuse Zalas\\Toolbox\\Tool\\Command;\nuse Zalas\\Toolbox\\Tool\\Command\\PhiveInstallCommand;\n\nfinal class PhiveInstallCommandFactory\n{\n    public static function import(array $command): Command\n    {\n        Assert::requireFields(['alias', 'bin'], $command, 'PhiveInstallCommand');\n\n        return new PhiveInstallCommand($command['alias'], $command['bin'], $command['sig'] ?? null);\n    }\n}\n"
  },
  {
    "path": "src/Json/Factory/ShCommandFactory.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Json\\Factory;\n\nuse Zalas\\Toolbox\\Tool\\Command;\nuse Zalas\\Toolbox\\Tool\\Command\\ShCommand;\n\nfinal class ShCommandFactory\n{\n    public static function import(array $command): Command\n    {\n        Assert::requireFields(['command'], $command, 'ShCommand');\n\n        return new ShCommand($command['command']);\n    }\n}\n"
  },
  {
    "path": "src/Json/Factory/ToolFactory.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Json\\Factory;\n\nuse Zalas\\Toolbox\\Tool\\Collection;\nuse Zalas\\Toolbox\\Tool\\Command;\nuse Zalas\\Toolbox\\Tool\\Command\\MultiStepCommand;\nuse Zalas\\Toolbox\\Tool\\Command\\TestCommand;\nuse Zalas\\Toolbox\\Tool\\Tool;\n\nfinal class ToolFactory\n{\n    public static function import(array $tool): Tool\n    {\n        Assert::requireFields(['name', 'summary', 'website', 'command', 'test'], $tool, 'tool');\n\n        return new Tool(\n            $tool['name'],\n            $tool['summary'],\n            $tool['website'],\n            $tool['tags'] ?? [],\n            self::importCommand($tool),\n            new TestCommand($tool['test'], $tool['name'])\n        );\n    }\n\n    private static function importCommand(array $tool): Command\n    {\n        $commands = Collection::create([]);\n\n        foreach ($tool['command'] as $type => $command) {\n            $commands = $commands->merge(self::createCommands($type, $command));\n        }\n\n        if (0 === $commands->count()) {\n            throw new \\RuntimeException(\\sprintf('No valid command defined for the tool: %s', \\json_encode($tool)));\n        }\n\n        return 1 === $commands->count() ? $commands->toArray()[0] : new MultiStepCommand($commands);\n    }\n\n    private static function createCommands($type, $command): Collection\n    {\n        $factories = [\n            'phar-download' => \\sprintf('%s::import', PharDownloadCommandFactory::class),\n            'file-download' => \\sprintf('%s::import', FileDownloadCommandFactory::class),\n            'box-build' => \\sprintf('%s::import', BoxBuildCommandFactory::class),\n            'composer-install' => \\sprintf('%s::import', ComposerInstallCommandFactory::class),\n            'phive-install' => \\sprintf('%s::import', PhiveInstallCommandFactory::class),\n            'composer-global-install' => \\sprintf('%s::import', ComposerGlobalInstallCommandFactory::class),\n            'composer-bin-plugin' => \\sprintf('%s::import', ComposerBinPluginCommandFactory::class),\n            'sh' => \\sprintf('%s::import', ShCommandFactory::class),\n        ];\n\n        if (!isset($factories[$type])) {\n            throw new \\RuntimeException(\\sprintf('Unrecognised command: \"%s\". Supported commands are: \"%s\".', $type, \\implode(', ', \\array_keys($factories))));\n        }\n\n        $command = !\\is_numeric(\\key($command)) ? [$command] : $command;\n\n        return Collection::create(\\array_map(function ($c) use ($type, $factories) {\n            return $factories[$type]($c);\n        }, $command));\n    }\n}\n"
  },
  {
    "path": "src/Json/JsonTools.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Json;\n\nuse InvalidArgumentException;\nuse RuntimeException;\nuse Zalas\\Toolbox\\Json\\Factory\\ToolFactory;\nuse Zalas\\Toolbox\\Tool\\Collection;\nuse Zalas\\Toolbox\\Tool\\Filter;\nuse Zalas\\Toolbox\\Tool\\Tools;\n\nfinal class JsonTools implements Tools\n{\n    /**\n     * @var callable\n     */\n    private $resourceLocator;\n\n    public function __construct(callable $resourceLocator)\n    {\n        $this->resourceLocator = $resourceLocator;\n    }\n\n    /**\n     * @param Filter $filter\n     * @return Collection\n     */\n    public function all(Filter $filter): Collection\n    {\n        return $this->loadTools()->filter($filter);\n    }\n\n    private function loadTools(): Collection\n    {\n        return \\array_reduce($this->resources(), function (Collection $tools, string $resource): Collection {\n            return $tools->merge(Collection::create(\n                \\array_map(\\sprintf('%s::import', ToolFactory::class), $this->loadJson($resource))\n            ));\n        }, Collection::create([]));\n    }\n\n    private function loadJson(string $resource): array\n    {\n        $json = \\json_decode(\\file_get_contents($resource), true);\n\n        if (!$json) {\n            throw new RuntimeException(\\sprintf('Failed to parse json: \"%s\"', $resource));\n        }\n\n        if (!isset($json['tools']) || !\\is_array($json['tools'])) {\n            throw new RuntimeException(\\sprintf('Failed to find any tools in: \"%s\".', $resource));\n        }\n\n        return $json['tools'];\n    }\n\n    private function resources(): array\n    {\n        $resources = \\call_user_func($this->resourceLocator);\n\n        return \\array_map(function (string $resource) {\n            if (!\\is_readable($resource)) {\n                throw new InvalidArgumentException(\\sprintf('Could not read the file: \"%s\".', $resource));\n            }\n\n            return $resource;\n        }, $resources);\n    }\n}\n"
  },
  {
    "path": "src/Runner/ParametrisedRunner.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Runner;\n\nuse Zalas\\Toolbox\\Tool\\Command;\n\nfinal class ParametrisedRunner implements Runner\n{\n    private Runner $decoratedRunner;\n    private array $parameters;\n\n    public function __construct(Runner $decoratedRunner, array $parameters)\n    {\n        $this->decoratedRunner = $decoratedRunner;\n        $this->parameters = $parameters;\n    }\n\n    public function run(Command $command): int\n    {\n        return $this->decoratedRunner->run(new class($command, $this->parameters) implements Command {\n            private Command $command;\n            private array $parameters;\n\n            public function __construct(Command $command, array $parameters)\n            {\n                $this->command = $command;\n                $this->parameters = $parameters;\n            }\n\n            public function __toString(): string\n            {\n                return \\strtr((string) $this->command, $this->parameters);\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "src/Runner/PassthruRunner.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Runner;\n\nuse Zalas\\Toolbox\\Tool\\Command;\n\nfinal class PassthruRunner implements Runner\n{\n    public function run(Command $command): int\n    {\n        \\passthru((string) $command, $status);\n\n        return $status;\n    }\n}\n"
  },
  {
    "path": "src/Runner/Runner.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Runner;\n\nuse Zalas\\Toolbox\\Tool\\Command;\n\ninterface Runner\n{\n    public function run(Command $command): int;\n}\n"
  },
  {
    "path": "src/Tool/Collection.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tool;\n\nuse Countable;\nuse IteratorAggregate;\nuse Traversable;\n\nclass Collection implements IteratorAggregate, Countable\n{\n    private array $elements;\n\n    private function __construct(array $elements)\n    {\n        $this->elements = $elements;\n    }\n\n    public static function create(array $elements): Collection\n    {\n        return new self($elements);\n    }\n\n    public function getIterator(): Traversable\n    {\n        yield from $this->elements;\n    }\n\n    public function merge(Collection $other): Collection\n    {\n        return self::create(\\array_merge($this->elements, $other->elements));\n    }\n\n    public function filter(callable $f): Collection\n    {\n        return self::create(\\array_values(\\array_filter($this->elements, $f)));\n    }\n\n    public function map(callable $f): Collection\n    {\n        return self::create(\\array_map($f, $this->elements));\n    }\n\n    public function reduce($initial, callable $param)\n    {\n        return \\array_reduce($this->elements, $param, $initial);\n    }\n\n    public function sort(callable $f): Collection\n    {\n        $elements = $this->elements;\n        \\usort($elements, $f);\n\n        return self::create($elements);\n    }\n\n    public function toArray(): array\n    {\n        return $this->elements;\n    }\n\n    public function count(): int\n    {\n        return \\count($this->elements);\n    }\n\n    public function empty(): bool\n    {\n        return empty($this->elements);\n    }\n}\n"
  },
  {
    "path": "src/Tool/Command/BoxBuildCommand.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tool\\Command;\n\nuse Zalas\\Toolbox\\Tool\\Command;\n\nfinal class BoxBuildCommand implements Command\n{\n    private string $repository;\n    private string $phar;\n    private string $bin;\n    private string $workDir;\n    private ?string $version;\n\n    public function __construct(string $repository, string $phar, string $bin, string $workDir, ?string $version = null)\n    {\n        $this->repository = $repository;\n        $this->phar = $phar;\n        $this->bin = $bin;\n        $this->workDir = $workDir;\n        $this->version = $version;\n    }\n\n    public function __toString(): string\n    {\n        return \\sprintf(\n            'git clone %s %s&& cd %s && git checkout %s && composer install --no-dev --prefer-dist -n && box compile && mv %s %s && chmod +x %s && cd && rm -rf %s',\n            $this->repository,\n            $this->targetDir(),\n            $this->targetDir(),\n            $this->version ?? '$(git describe --tags $(git rev-list --tags --max-count=1) 2>/dev/null)',\n            $this->phar,\n            $this->bin,\n            $this->bin,\n            $this->targetDir()\n        );\n    }\n\n    private function targetDir(): string\n    {\n        $targetDir = \\preg_replace('#^.*/(.*?)(.git)?$#', '$1', $this->repository);\n\n        return  \\sprintf('%s/%s', $this->workDir, $targetDir !== $this->repository ? $targetDir : 'tmp');\n    }\n}\n"
  },
  {
    "path": "src/Tool/Command/ComposerBinPluginCommand.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tool\\Command;\n\nuse Zalas\\Toolbox\\Tool\\Collection;\nuse Zalas\\Toolbox\\Tool\\Command;\n\nfinal class ComposerBinPluginCommand implements Command\n{\n    private string $package;\n\n    private string $namespace;\n\n    private Collection $links;\n\n    public function __construct(string $package, string $namespace, Collection $links)\n    {\n        $this->package = $package;\n        $this->namespace = $namespace;\n        $this->links = $links;\n    }\n\n    public function __toString(): string\n    {\n        return \\sprintf('composer global bin %s require --prefer-dist --update-no-dev -n %s%s', $this->namespace, $this->package, $this->linkCommand());\n    }\n\n    public function package(): string\n    {\n        return $this->package;\n    }\n\n    public function namespace(): string\n    {\n        return $this->namespace;\n    }\n\n    public function links(): Collection\n    {\n        return $this->links;\n    }\n\n    private function linkCommand(): string\n    {\n        return $this->links->reduce('', function (string $command, ComposerBinPluginLinkCommand $link) {\n            return $command.' && '.$link;\n        });\n    }\n}\n"
  },
  {
    "path": "src/Tool/Command/ComposerBinPluginLinkCommand.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tool\\Command;\n\nuse Zalas\\Toolbox\\Tool\\Command;\n\nfinal class ComposerBinPluginLinkCommand implements Command\n{\n    private string $source;\n    private string $target;\n    private string $namespace;\n\n    public function __construct(string $source, string $target, string $namespace)\n    {\n        $this->source = $source;\n        $this->target = $target;\n        $this->namespace = $namespace;\n    }\n\n    public function __toString(): string\n    {\n        return \\sprintf(\n            'ln -sf ${COMPOSER_HOME:-\"~/.composer\"}/vendor-bin/%s/vendor/bin/%s %s',\n            $this->namespace,\n            $this->source,\n            $this->target\n        );\n    }\n}\n"
  },
  {
    "path": "src/Tool/Command/ComposerGlobalInstallCommand.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tool\\Command;\n\nuse Zalas\\Toolbox\\Tool\\Command;\n\nfinal class ComposerGlobalInstallCommand implements Command\n{\n    private string $package;\n\n    public function __construct(string $package)\n    {\n        $this->package = $package;\n    }\n\n    public function __toString(): string\n    {\n        return \\sprintf('composer global require --prefer-dist --update-no-dev -n %s', $this->package);\n    }\n\n    public function package(): string\n    {\n        return $this->package;\n    }\n}\n"
  },
  {
    "path": "src/Tool/Command/ComposerGlobalMultiInstallCommand.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tool\\Command;\n\nuse InvalidArgumentException;\nuse Zalas\\Toolbox\\Tool\\Collection;\nuse Zalas\\Toolbox\\Tool\\Command;\n\nfinal class ComposerGlobalMultiInstallCommand implements Command\n{\n    private Collection $commands;\n\n    public function __construct(Collection $commands)\n    {\n        if ($commands->empty()) {\n            throw new InvalidArgumentException('Collection of composer global install commands cannot be empty.');\n        }\n\n        $this->commands = $commands->filter(function (ComposerGlobalInstallCommand $c) {\n            return $c;\n        });\n    }\n\n    public function __toString(): string\n    {\n        $packages = \\implode(' ', \\array_map(function (ComposerGlobalInstallCommand $command) {\n            return $command->package();\n        }, $this->commands->toArray()));\n\n        return \\sprintf('composer global require --prefer-dist --update-no-dev -n %s', $packages);\n    }\n}\n"
  },
  {
    "path": "src/Tool/Command/ComposerInstallCommand.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tool\\Command;\n\nuse Zalas\\Toolbox\\Tool\\Command;\n\nfinal class ComposerInstallCommand implements Command\n{\n    private string $repository;\n    private string $targetDir;\n    private ?string $version;\n\n    public function __construct(string $repository, string $targetDir, ?string $version = null)\n    {\n        $this->repository = $repository;\n        $this->targetDir = $targetDir;\n        $this->version = $version;\n    }\n\n    public function __toString(): string\n    {\n        return \\sprintf(\n            'git clone %s %s && cd %s && git checkout %s && composer install --no-dev --prefer-dist -n',\n            $this->repository,\n            $this->targetDir,\n            $this->targetDir,\n            $this->version ?? '$(git describe --tags $(git rev-list --tags --max-count=1) 2>/dev/null)'\n        );\n    }\n}\n"
  },
  {
    "path": "src/Tool/Command/FileDownloadCommand.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tool\\Command;\n\nuse Zalas\\Toolbox\\Tool\\Command;\n\nfinal class FileDownloadCommand implements Command\n{\n    private string $url;\n    private string $file;\n\n    public function __construct(string $url, string $file)\n    {\n        $this->url = $url;\n        $this->file = $file;\n    }\n\n    public function __toString(): string\n    {\n        return \\sprintf('curl -Ls -w %%{filename_effective}\\'\\n\\' %s -o %s', $this->url, $this->file);\n    }\n}\n"
  },
  {
    "path": "src/Tool/Command/MultiStepCommand.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tool\\Command;\n\nuse InvalidArgumentException;\nuse Zalas\\Toolbox\\Tool\\Collection;\nuse Zalas\\Toolbox\\Tool\\Command;\n\nfinal class MultiStepCommand implements Command\n{\n    private Collection $commands;\n    private mixed $glue;\n\n    public function __construct(Collection $commands, $glue = ' && ')\n    {\n        if ($commands->empty()) {\n            throw new InvalidArgumentException('Collection of commands cannot be empty.');\n        }\n\n        $this->commands = $commands->filter(function (Command $c) {\n            return $c;\n        });\n        $this->glue = $glue;\n    }\n\n    public function __toString(): string\n    {\n        return \\implode($this->glue, $this->commands->toArray());\n    }\n}\n"
  },
  {
    "path": "src/Tool/Command/OptimisedComposerBinPluginCommand.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tool\\Command;\n\nuse InvalidArgumentException;\nuse Zalas\\Toolbox\\Tool\\Collection;\nuse Zalas\\Toolbox\\Tool\\Command;\n\nfinal class OptimisedComposerBinPluginCommand implements Command\n{\n    private Collection $commands;\n\n    public function __construct(Collection $commands)\n    {\n        if ($commands->empty()) {\n            throw new InvalidArgumentException('Collection of composer bin plugin commands cannot be empty.');\n        }\n\n        $this->commands = $commands->filter(function (ComposerBinPluginCommand $command) {\n            return $command;\n        });\n    }\n\n    public function __toString(): string\n    {\n        return \\implode(' && ', \\array_merge($this->commandsToRun($this->packagesGroupedByNamespace()), $this->linksToCreate()));\n    }\n\n    private function packagesGroupedByNamespace(): array\n    {\n        return $this->commands->reduce([], function (array $packages, ComposerBinPluginCommand $command) {\n            $packages[$command->namespace()][] = $command->package();\n\n            return $packages;\n        });\n    }\n\n    private function commandToRun(string $namespace, array $packages): string\n    {\n        return \\sprintf('composer global bin %s require --prefer-dist --update-no-dev -n %s', $namespace, \\implode(' ', $packages));\n    }\n\n    private function commandsToRun(array $packagesGrouped): array\n    {\n        return \\array_map([$this, 'commandToRun'], \\array_keys($packagesGrouped), $packagesGrouped);\n    }\n\n    private function linksToCreate(): array\n    {\n        return $this->commands\n            ->filter(function (ComposerBinPluginCommand $command) {\n                return !$command->links()->empty();\n            })\n            ->map(function (ComposerBinPluginCommand $command) {\n                return $command->links()->reduce('', function (string $command, ComposerBinPluginLinkCommand $link) {\n                    return !empty($command) ? $command.' && '.$link : $link;\n                });\n            })\n            ->toArray();\n    }\n}\n"
  },
  {
    "path": "src/Tool/Command/PharDownloadCommand.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tool\\Command;\n\nuse Zalas\\Toolbox\\Tool\\Command;\n\nfinal class PharDownloadCommand implements Command\n{\n    private string $phar;\n    private string $bin;\n\n    public function __construct(string $phar, string $bin)\n    {\n        $this->phar = $phar;\n        $this->bin = $bin;\n    }\n\n    public function __toString(): string\n    {\n        return \\sprintf('curl -Ls -w %%{filename_effective}\\'\\n\\' %s -o %s && chmod +x %s', $this->phar, $this->bin, $this->bin);\n    }\n}\n"
  },
  {
    "path": "src/Tool/Command/PhiveInstallCommand.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tool\\Command;\n\nuse Zalas\\Toolbox\\Tool\\Command;\n\nfinal class PhiveInstallCommand implements Command\n{\n    private string $alias;\n    private string $bin;\n    private ?string $sig;\n\n    public function __construct(string $alias, string $bin, ?string $sig = null)\n    {\n        $this->alias = $alias;\n        $this->bin = $bin;\n        $this->sig = $sig;\n    }\n\n    public function __toString(): string\n    {\n        $home = \\sprintf('%s/.phive', \\dirname($this->bin));\n        $tmp = \\sprintf('%s/tmp/%s', $home, \\md5($this->alias));\n\n        return \\sprintf(\n            'phive --no-progress --home %s install %s %s -t %s && mv %s/* %s',\n            $home,\n            $this->sig ? '--trust-gpg-keys '.$this->sig : '--force-accept-unsigned',\n            $this->alias,\n            $tmp,\n            $tmp,\n            $this->bin\n        );\n    }\n}\n"
  },
  {
    "path": "src/Tool/Command/ShCommand.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tool\\Command;\n\nuse Zalas\\Toolbox\\Tool\\Command;\n\nfinal class ShCommand implements Command\n{\n    private string $command;\n\n    public function __construct(string $command)\n    {\n        $this->command = $command;\n    }\n\n    public function __toString(): string\n    {\n        return $this->command;\n    }\n}\n"
  },
  {
    "path": "src/Tool/Command/TestCommand.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tool\\Command;\n\nuse Zalas\\Toolbox\\Tool\\Command;\n\nfinal class TestCommand implements Command\n{\n    private string $command;\n    private string $name;\n\n    public function __construct(string $command, string $name)\n    {\n        $this->command = $command;\n        $this->name = $name;\n    }\n\n    public function __toString(): string\n    {\n        return \\sprintf('(output=$(%s 2>&1) && echo -e \"\\e[0;32m✔\\e[0m︎%s\" || (echo -e \"\\e[1;31m✘\\e[0m%s\\n$output\" && false))', $this->command, $this->name, $this->name);\n    }\n}\n"
  },
  {
    "path": "src/Tool/Command.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tool;\n\ninterface Command\n{\n    public function __toString(): string;\n}\n"
  },
  {
    "path": "src/Tool/Filter.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tool;\n\nclass Filter\n{\n    /**\n     * @var string[]\n     */\n    private array $excludedTags;\n\n    /**\n     * @var string[]\n     */\n    private array $tags;\n\n    /**\n     * @param string[] $excludedTags\n     * @param string[] $tags\n     */\n    public function __construct(array $excludedTags, array $tags)\n    {\n        $this->excludedTags = $excludedTags;\n        $this->tags = $tags;\n    }\n\n    public function __invoke(Tool $tool): bool\n    {\n        return $this->excludedTags === \\array_diff($this->excludedTags, $tool->tags())\n            && (empty($this->tags) || \\array_intersect($this->tags, $tool->tags()));\n    }\n}\n"
  },
  {
    "path": "src/Tool/Tool.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tool;\n\nclass Tool\n{\n    private string $name;\n    private string $summary;\n    private string $website;\n    private Command $command;\n    private Command $testCommand;\n    private array $tags;\n\n    public function __construct(string $name, string $summary, string $website, array $tags, Command $command, Command $testCommand)\n    {\n        $this->name = $name;\n        $this->summary = $summary;\n        $this->website = $website;\n        $this->tags = \\array_map(function (string $tag) {\n            return $tag;\n        }, $tags);\n        $this->command = $command;\n        $this->testCommand = $testCommand;\n    }\n\n    public function name(): string\n    {\n        return $this->name;\n    }\n\n    public function summary(): string\n    {\n        return $this->summary;\n    }\n\n    public function website(): string\n    {\n        return $this->website;\n    }\n\n    public function command(): Command\n    {\n        return $this->command;\n    }\n\n    public function testCommand(): Command\n    {\n        return $this->testCommand;\n    }\n\n    /**\n     * @return array|string[]\n     */\n    public function tags(): array\n    {\n        return $this->tags;\n    }\n}\n"
  },
  {
    "path": "src/Tool/Tools.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tool;\n\nuse RuntimeException;\n\ninterface Tools\n{\n    /**\n     * @param Filter $filter\n     * @return Collection\n     * @throws RuntimeException in case tools cannot be loaded\n     */\n    public function all(Filter $filter): Collection;\n}\n"
  },
  {
    "path": "src/UseCase/InstallTools.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\UseCase;\n\nuse Closure;\nuse Zalas\\Toolbox\\Tool\\Collection;\nuse Zalas\\Toolbox\\Tool\\Command;\nuse Zalas\\Toolbox\\Tool\\Command\\BoxBuildCommand;\nuse Zalas\\Toolbox\\Tool\\Command\\ComposerBinPluginCommand;\nuse Zalas\\Toolbox\\Tool\\Command\\ComposerGlobalInstallCommand;\nuse Zalas\\Toolbox\\Tool\\Command\\ComposerGlobalMultiInstallCommand;\nuse Zalas\\Toolbox\\Tool\\Command\\ComposerInstallCommand;\nuse Zalas\\Toolbox\\Tool\\Command\\FileDownloadCommand;\nuse Zalas\\Toolbox\\Tool\\Command\\MultiStepCommand;\nuse Zalas\\Toolbox\\Tool\\Command\\OptimisedComposerBinPluginCommand;\nuse Zalas\\Toolbox\\Tool\\Command\\PharDownloadCommand;\nuse Zalas\\Toolbox\\Tool\\Command\\PhiveInstallCommand;\nuse Zalas\\Toolbox\\Tool\\Command\\ShCommand;\nuse Zalas\\Toolbox\\Tool\\Filter;\nuse Zalas\\Toolbox\\Tool\\Tool;\nuse Zalas\\Toolbox\\Tool\\Tools;\n\nclass InstallTools\n{\n    public const PRE_INSTALLATION_TAG = 'pre-installation';\n\n    private Tools $tools;\n\n    public function __construct(Tools $tools)\n    {\n        $this->tools = $tools;\n    }\n\n    public function __invoke(Filter $filter): Command\n    {\n        $tools = $this->tools->all($filter);\n        $installationCommands = $this->installationCommands($tools);\n        $commandFilter = $this->commandFilter($this->toolCommands($tools));\n\n        return new MultiStepCommand(\n            $installationCommands\n                ->merge($commandFilter(ShCommand::class))\n                ->merge($commandFilter(FileDownloadCommand::class))\n                ->merge($commandFilter(PharDownloadCommand::class))\n                ->merge($commandFilter(PhiveInstallCommand::class))\n                ->merge($commandFilter(MultiStepCommand::class))\n                ->merge($this->groupComposerGlobalInstallCommands($commandFilter(ComposerGlobalInstallCommand::class)))\n                ->merge($this->groupComposerBinPluginCommands($commandFilter(ComposerBinPluginCommand::class)))\n                ->merge($commandFilter(ComposerInstallCommand::class))\n                ->merge($commandFilter(BoxBuildCommand::class))\n        );\n    }\n\n    private function commandFilter(Collection $commands): Closure\n    {\n        return function ($type) use ($commands) {\n            return $commands->filter(function (Command $command) use ($type) {\n                return $command instanceof $type;\n            });\n        };\n    }\n\n    private function installationCommands(Collection $tools): Collection\n    {\n        return $tools->filter(function (Tool $tool) {\n            return \\in_array(self::PRE_INSTALLATION_TAG, $tool->tags());\n        })->map(function (Tool $tool) {\n            return $tool->command();\n        });\n    }\n\n    private function toolCommands(Collection $tools): Collection\n    {\n        return $tools->filter(function (Tool $tool) {\n            return !\\in_array(self::PRE_INSTALLATION_TAG, $tool->tags());\n        })->map(function (Tool $tool) {\n            return $tool->command();\n        });\n    }\n\n    private function groupComposerGlobalInstallCommands(Collection $commands): Collection\n    {\n        $commands = $commands->empty() ? [] : [new ComposerGlobalMultiInstallCommand($commands)];\n\n        return Collection::create($commands);\n    }\n\n    private function groupComposerBinPluginCommands(Collection $commands): Collection\n    {\n        $commands = $commands->empty() ? [] : [new OptimisedComposerBinPluginCommand($commands)];\n\n        return Collection::create($commands);\n    }\n}\n"
  },
  {
    "path": "src/UseCase/ListTools.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\UseCase;\n\nuse Zalas\\Toolbox\\Tool\\Collection;\nuse Zalas\\Toolbox\\Tool\\Filter;\nuse Zalas\\Toolbox\\Tool\\Tools;\n\nclass ListTools\n{\n    private Tools $tools;\n\n    public function __construct(Tools $tools)\n    {\n        $this->tools = $tools;\n    }\n\n    public function __invoke(Filter $filter): Collection\n    {\n        return $this->tools->all($filter);\n    }\n}\n"
  },
  {
    "path": "src/UseCase/TestTools.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\UseCase;\n\nuse Zalas\\Toolbox\\Tool\\Command;\nuse Zalas\\Toolbox\\Tool\\Command\\MultiStepCommand;\nuse Zalas\\Toolbox\\Tool\\Filter;\nuse Zalas\\Toolbox\\Tool\\Tool;\nuse Zalas\\Toolbox\\Tool\\Tools;\n\nclass TestTools\n{\n    private Tools $tools;\n\n    public function __construct(Tools $tools)\n    {\n        $this->tools = $tools;\n    }\n\n    public function __invoke(Filter $filter): Command\n    {\n        return new MultiStepCommand(\n            $this->tools->all($filter)->map(function (Tool $tool) {\n                return $tool->testCommand();\n            })\n        );\n    }\n}\n"
  },
  {
    "path": "tests/Cli/ApplicationTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Cli;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Symfony\\Component\\Console\\Application as CliApplication;\nuse Symfony\\Component\\Console\\Input\\ArrayInput;\nuse Symfony\\Component\\Console\\Output\\NullOutput;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\nuse Zalas\\PHPUnit\\Globals\\Attribute\\Putenv;\nuse Zalas\\Toolbox\\Cli\\Application;\nuse Zalas\\Toolbox\\Cli\\Command\\InstallCommand;\nuse Zalas\\Toolbox\\Cli\\Command\\ListCommand;\nuse Zalas\\Toolbox\\Cli\\ServiceContainer;\n\nclass ApplicationTest extends TestCase\n{\n    private const VERSION = 'test';\n\n    private Application $app;\n\n    protected function setUp(): void\n    {\n        $container = $this->createStub(ServiceContainer::class);\n        $this->app = new Application(self::VERSION, $container);\n    }\n\n    public function test_it_is_a_cli_application()\n    {\n        $this->assertInstanceOf(CliApplication::class, $this->app);\n    }\n\n    public function test_it_defines_the_app_name_and_version()\n    {\n        $this->assertSame('toolbox', $this->app->getName());\n        $this->assertSame(self::VERSION, $this->app->getVersion());\n    }\n\n    public function test_it_defines_tools_option()\n    {\n        $this->assertTrue($this->app->getDefinition()->hasOption('tools'));\n        $this->assertEquals(\n            [\n                \\realpath(__DIR__.'/../../src/Cli/').'/../../resources/pre-installation.json',\n                \\realpath(__DIR__.'/../../src/Cli/').'/../../resources/architecture.json',\n                \\realpath(__DIR__.'/../../src/Cli/').'/../../resources/checkstyle.json',\n                \\realpath(__DIR__.'/../../src/Cli/').'/../../resources/compatibility.json',\n                \\realpath(__DIR__.'/../../src/Cli/').'/../../resources/composer.json',\n                \\realpath(__DIR__.'/../../src/Cli/').'/../../resources/deprecation.json',\n                \\realpath(__DIR__.'/../../src/Cli/').'/../../resources/documentation.json',\n                \\realpath(__DIR__.'/../../src/Cli/').'/../../resources/linting.json',\n                \\realpath(__DIR__.'/../../src/Cli/').'/../../resources/metrics.json',\n                \\realpath(__DIR__.'/../../src/Cli/').'/../../resources/phpcs.json',\n                \\realpath(__DIR__.'/../../src/Cli/').'/../../resources/phpstan.json',\n                \\realpath(__DIR__.'/../../src/Cli/').'/../../resources/psalm.json',\n                \\realpath(__DIR__.'/../../src/Cli/').'/../../resources/refactoring.json',\n                \\realpath(__DIR__.'/../../src/Cli/').'/../../resources/security.json',\n                \\realpath(__DIR__.'/../../src/Cli/').'/../../resources/test.json',\n                \\realpath(__DIR__.'/../../src/Cli/').'/../../resources/tools.json'\n            ],\n            $this->app->getDefinition()->getOption('tools')->getDefault()\n        );\n    }\n\n    #[Putenv('TOOLBOX_JSON', 'resources/pre.json,resources/tools.json')]\n    public function test_it_takes_the_tools_option_default_from_environment_if_present()\n    {\n        $this->assertSame(['resources/pre.json', 'resources/tools.json'], $this->app->getDefinition()->getOption('tools')->getDefault());\n    }\n\n    #[Putenv('TOOLBOX_JSON', 'resources/pre.json , resources/tools.json')]\n    public function test_it_trims_the_tools_option()\n    {\n        $this->assertSame(['resources/pre.json', 'resources/tools.json'], $this->app->getDefinition()->getOption('tools')->getDefault());\n    }\n\n    /**\n     * @group integration\n     */\n    public function test_it_allows_to_override_tools_location()\n    {\n        $app = new Application(self::VERSION, new ServiceContainer());\n        $result = $app->doRun(\n            new ArrayInput([\n                'command' => ListCommand::NAME,\n                '--tools' => [__DIR__.'/../resources/tools.json'],\n                '--no-interaction' => true,\n            ]),\n            new NullOutput()\n        );\n\n        $this->assertSame(0, $result);\n    }\n\n    /**\n     * @group integration\n     */\n    public function test_it_runs_the_command_in_dry_run_mode()\n    {\n        $output = $this->givenOutputThatExpectsMessageWritten('composer global bin phpstan require');\n\n        $app = new Application(self::VERSION, new ServiceContainer());\n        $app->doRun(\n            new ArrayInput([\n                'command' => InstallCommand::NAME,\n                '--dry-run' => true,\n                '--tools' => [__DIR__.'/../resources/tools.json'],\n                '--no-interaction' => true,\n            ]),\n            $output\n        );\n    }\n\n    public function givenOutputThatExpectsMessageWritten(string $message): OutputInterface\n    {\n        $output = $this->createMock(OutputInterface::class);\n        $output->expects(self::once())\n            ->method('writeln')\n            ->with(self::stringContains($message));\n\n        return $output;\n    }\n}\n"
  },
  {
    "path": "tests/Cli/Command/InstallCommandTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Cli\\Command;\n\nuse PHPUnit\\Framework\\MockObject\\Stub;\nuse Zalas\\PHPUnit\\Globals\\Attribute\\Putenv;\nuse Zalas\\Toolbox\\Cli\\Command\\InstallCommand;\nuse Zalas\\Toolbox\\Runner\\Runner;\nuse Zalas\\Toolbox\\Tool\\Command;\nuse Zalas\\Toolbox\\Tool\\Command\\ShCommand;\nuse Zalas\\Toolbox\\Tool\\Filter;\nuse Zalas\\Toolbox\\UseCase\\InstallTools;\n\nclass InstallCommandTest extends ToolboxCommandTestCase\n{\n    protected const CLI_COMMAND_NAME = InstallCommand::NAME;\n\n    private Runner|Stub $runner;\n\n    private InstallTools|Stub $useCase;\n\n    protected function setUp(): void\n    {\n        $this->runner = $this->createStub(Runner::class);\n        $this->useCase = $this->createStub(InstallTools::class);\n\n        parent::setUp();\n    }\n\n    public function test_it_runs_the_install_tools_use_case()\n    {\n        $command = $this->createCommand();\n        $this->useCase->method('__invoke')->willReturn($command);\n        $this->runner->method('run')->with($command)->willReturn(0);\n\n        $tester = $this->executeCliCommand();\n\n        $this->assertSame(0, $tester->getStatusCode());\n    }\n\n    public function test_it_returns_the_status_code_of_the_run()\n    {\n        $this->useCase->method('__invoke')->willReturn($this->createCommand());\n        $this->runner->method('run')->willReturn(1);\n\n        $tester = $this->executeCliCommand();\n\n        $this->assertSame(1, $tester->getStatusCode());\n    }\n\n    public function test_it_filters_by_tags()\n    {\n        $this->useCase\n            ->method('__invoke')\n            ->with(new Filter(['foo'], ['bar']))\n            ->willReturn($this->createCommand());\n        $this->runner->method('run')->willReturn(0);\n\n        $tester = $this->executeCliCommand(['--exclude-tag' => ['foo'], '--tag' => ['bar']]);\n\n        $this->assertSame(0, $tester->getStatusCode());\n    }\n\n    public function test_it_defines_dry_run_option()\n    {\n        $this->assertTrue($this->cliCommand()->getDefinition()->hasOption('dry-run'));\n    }\n\n    public function test_it_defines_target_dir_option()\n    {\n        $this->assertTrue($this->cliCommand()->getDefinition()->hasOption('target-dir'));\n        $this->assertSame('/usr/local/bin', $this->cliCommand()->getDefinition()->getOption('target-dir')->getDefault());\n    }\n\n    #[Putenv('TOOLBOX_TARGET_DIR', '/tmp')]\n    public function test_it_takes_the_target_dir_option_default_from_environment_if_present()\n    {\n        $this->assertSame('/tmp', $this->cliCommand()->getDefinition()->getOption('target-dir')->getDefault());\n    }\n\n    public function test_it_defines_exclude_tag_option()\n    {\n        $this->assertTrue($this->cliCommand()->getDefinition()->hasOption('exclude-tag'));\n        $this->assertSame([], $this->cliCommand()->getDefinition()->getOption('exclude-tag')->getDefault());\n    }\n\n    #[Putenv('TOOLBOX_EXCLUDED_TAGS', 'foo,bar,baz')]\n    public function test_it_takes_the_excluded_tag_option_default_from_environment_if_present()\n    {\n        $this->assertSame(['foo', 'bar', 'baz'], $this->cliCommand()->getDefinition()->getOption('exclude-tag')->getDefault());\n    }\n\n    public function test_it_defines_tag_option()\n    {\n        $this->assertTrue($this->cliCommand()->getDefinition()->hasOption('tag'));\n        $this->assertSame([], $this->cliCommand()->getDefinition()->getOption('tag')->getDefault());\n    }\n\n    #[Putenv('TOOLBOX_TAGS', 'foo,bar,baz')]\n    public function test_it_takes_the_tag_option_default_from_environment_if_present()\n    {\n        $this->assertSame(['foo', 'bar', 'baz'], $this->cliCommand()->getDefinition()->getOption('tag')->getDefault());\n    }\n\n    protected function getContainerTestDoubles(): array\n    {\n        return [\n            Runner::class => $this->runner,\n            InstallTools::class => $this->useCase,\n        ];\n    }\n\n    private function createCommand(): Command\n    {\n        return new ShCommand('echo \"foo\"');\n    }\n}\n"
  },
  {
    "path": "tests/Cli/Command/ListCommandTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Cli\\Command;\n\nuse PHPUnit\\Framework\\MockObject\\Stub;\nuse Zalas\\PHPUnit\\Globals\\Attribute\\Putenv;\nuse Zalas\\Toolbox\\Cli\\Command\\ListCommand;\nuse Zalas\\Toolbox\\Tool\\Collection;\nuse Zalas\\Toolbox\\Tool\\Command\\ShCommand;\nuse Zalas\\Toolbox\\Tool\\Command\\TestCommand;\nuse Zalas\\Toolbox\\Tool\\Filter;\nuse Zalas\\Toolbox\\Tool\\Tool;\nuse Zalas\\Toolbox\\UseCase\\ListTools;\n\nclass ListCommandTest extends ToolboxCommandTestCase\n{\n    protected const CLI_COMMAND_NAME = ListCommand::NAME;\n\n    private ListTools|Stub $useCase;\n\n    protected function setUp(): void\n    {\n        $this->useCase = $this->createStub(ListTools::class);\n\n        parent::setUp();\n    }\n\n    public function test_it_runs_the_list_tools_use_case()\n    {\n        $this->useCase->method('__invoke')->willReturn(Collection::create([\n            $this->createTool('Behat', 'Tests business expectations', 'http://behat.org'),\n        ]));\n\n        $tester = $this->executeCliCommand();\n\n        $this->assertSame(0, $tester->getStatusCode());\n        $this->assertMatchesRegularExpression('#Available tools#i', $tester->getDisplay());\n        $this->assertMatchesRegularExpression('#Behat.*?Tests business expectations.*?http://behat.org#smi', $tester->getDisplay());\n    }\n\n    public function test_it_filters_by_tags()\n    {\n        $this->useCase->method('__invoke')\n            ->with(new Filter(['foo'], ['bar']))\n            ->willReturn(Collection::create([\n                 $this->createTool('Behat', 'Tests business expectations', 'http://behat.org'),\n            ]));\n\n        $tester = $this->executeCliCommand(['--exclude-tag' => ['foo'], '--tag' => ['bar']]);\n\n        $this->assertSame(0, $tester->getStatusCode());\n    }\n\n    public function test_it_defines_exclude_tag_option()\n    {\n        $this->assertTrue($this->cliCommand()->getDefinition()->hasOption('exclude-tag'));\n        $this->assertSame([], $this->cliCommand()->getDefinition()->getOption('exclude-tag')->getDefault());\n    }\n\n    #[Putenv('TOOLBOX_EXCLUDED_TAGS', 'foo,bar,baz')]\n    public function test_it_takes_the_excluded_tag_option_default_from_environment_if_present()\n    {\n        $this->assertSame(['foo', 'bar', 'baz'], $this->cliCommand()->getDefinition()->getOption('exclude-tag')->getDefault());\n    }\n\n    public function test_it_defines_tag_option()\n    {\n        $this->assertTrue($this->cliCommand()->getDefinition()->hasOption('tag'));\n    }\n\n    #[Putenv('TOOLBOX_TAGS', 'foo,bar,baz')]\n    public function test_it_takes_the_tag_option_default_from_environment_if_present()\n    {\n        $this->assertSame(['foo', 'bar', 'baz'], $this->cliCommand()->getDefinition()->getOption('tag')->getDefault());\n    }\n\n    protected function getContainerTestDoubles(): array\n    {\n        return [\n            ListTools::class => $this->useCase,\n        ];\n    }\n\n    private function createTool(string $name, string $summary, string $website): Tool\n    {\n        return new Tool(\n            $name,\n            $summary,\n            $website,\n            [],\n            new ShCommand('any command'),\n            new TestCommand('any test command', 'any')\n        );\n    }\n}\n"
  },
  {
    "path": "tests/Cli/Command/TestCommandTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Cli\\Command;\n\nuse PHPUnit\\Framework\\MockObject\\Stub;\nuse Zalas\\PHPUnit\\Globals\\Attribute\\Putenv;\nuse Zalas\\Toolbox\\Cli\\Command\\TestCommand;\nuse Zalas\\Toolbox\\Runner\\Runner;\nuse Zalas\\Toolbox\\Tool\\Command;\nuse Zalas\\Toolbox\\Tool\\Command\\ShCommand;\nuse Zalas\\Toolbox\\Tool\\Filter;\nuse Zalas\\Toolbox\\UseCase\\TestTools;\n\nclass TestCommandTest extends ToolboxCommandTestCase\n{\n    protected const CLI_COMMAND_NAME = TestCommand::NAME;\n\n    private Runner|Stub $runner;\n\n    private TestTools|Stub $useCase;\n\n    protected function setUp(): void\n    {\n        $this->runner = $this->createStub(Runner::class);\n        $this->useCase = $this->createStub(TestTools::class);\n\n        parent::setUp();\n    }\n\n    public function test_it_runs_the_test_tools_use_case()\n    {\n        $command = $this->createCommand();\n        $this->useCase->method('__invoke')->willReturn($command);\n        $this->runner->method('run')->with($command)->willReturn(0);\n\n        $tester = $this->executeCliCommand();\n\n        $this->assertSame(0, $tester->getStatusCode());\n    }\n\n    public function test_it_returns_the_status_code_of_the_run()\n    {\n        $this->useCase->method('__invoke')->willReturn($this->createCommand());\n        $this->runner->method('run')->willReturn(1);\n\n        $tester = $this->executeCliCommand();\n\n        $this->assertSame(1, $tester->getStatusCode());\n    }\n\n    public function test_it_filters_by_tags()\n    {\n        $this->useCase->method('__invoke')->with(new Filter(['foo'], ['bar']))->willReturn($this->createCommand());\n        $this->runner->method('run')->willReturn(0);\n\n        $tester = $this->executeCliCommand(['--exclude-tag' => ['foo'], '--tag' => ['bar']]);\n\n        $this->assertSame(0, $tester->getStatusCode());\n    }\n\n    public function test_it_defines_dry_run_option()\n    {\n        $this->assertTrue($this->cliCommand()->getDefinition()->hasOption('dry-run'));\n    }\n\n    public function test_it_defines_target_dir_option()\n    {\n        $this->assertSame('/usr/local/bin', $this->cliCommand()->getDefinition()->getOption('target-dir')->getDefault());\n        $this->assertTrue($this->cliCommand()->getDefinition()->hasOption('target-dir'));\n    }\n\n    #[Putenv('TOOLBOX_TARGET_DIR', '/tmp')]\n    public function test_it_takes_the_target_dir_option_default_from_environment_if_present()\n    {\n        $this->assertSame('/tmp', $this->cliCommand()->getDefinition()->getOption('target-dir')->getDefault());\n    }\n\n    public function test_it_defines_exclude_tag_option()\n    {\n        $this->assertTrue($this->cliCommand()->getDefinition()->hasOption('exclude-tag'));\n        $this->assertSame([], $this->cliCommand()->getDefinition()->getOption('exclude-tag')->getDefault());\n    }\n\n    #[Putenv('TOOLBOX_EXCLUDED_TAGS', 'foo,bar,baz')]\n    public function test_it_takes_the_excluded_tag_option_default_from_environment_if_present()\n    {\n        $this->assertSame(['foo', 'bar', 'baz'], $this->cliCommand()->getDefinition()->getOption('exclude-tag')->getDefault());\n    }\n\n    public function test_it_defines_tag_option()\n    {\n        $this->assertTrue($this->cliCommand()->getDefinition()->hasOption('tag'));\n    }\n\n    #[Putenv('TOOLBOX_TAGS', 'foo,bar,baz')]\n    public function test_it_takes_the_tag_option_default_from_environment_if_present()\n    {\n        $this->assertSame(['foo', 'bar', 'baz'], $this->cliCommand()->getDefinition()->getOption('tag')->getDefault());\n    }\n\n    protected function getContainerTestDoubles(): array\n    {\n        return [\n            Runner::class => $this->runner,\n            TestTools::class => $this->useCase,\n        ];\n    }\n\n    private function createCommand(): Command\n    {\n        return new ShCommand('true');\n    }\n}\n"
  },
  {
    "path": "tests/Cli/Command/ToolboxCommandTestCase.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Cli\\Command;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Symfony\\Component\\Console\\Command\\Command;\nuse Symfony\\Component\\Console\\Tester\\CommandTester;\nuse Zalas\\Toolbox\\Cli\\Application;\nuse Zalas\\Toolbox\\Cli\\ServiceContainer;\n\nabstract class ToolboxCommandTestCase extends TestCase\n{\n    protected const CLI_COMMAND_NAME = '';\n\n    protected Application $app;\n\n    protected function setUp(): void\n    {\n        $this->app = new Application('test', $this->createServiceContainer());\n    }\n\n    public function test_it_provides_help()\n    {\n        $this->assertNotEmpty($this->cliCommand()->getDescription());\n    }\n\n    protected function getContainerTestDoubles(): array\n    {\n        return [];\n    }\n\n    protected function executeCliCommand(array $input = []): CommandTester\n    {\n        $tester = new CommandTester($this->cliCommand());\n        $tester->execute($input);\n\n        return $tester;\n    }\n\n    protected function cliCommand(): Command\n    {\n        return $this->app->find(static::CLI_COMMAND_NAME);\n    }\n\n    private function createServiceContainer(): ServiceContainer\n    {\n        return new class($this->getContainerTestDoubles()) extends ServiceContainer {\n            private array $services;\n\n            public function __construct(array $services)\n            {\n                $this->services = $services;\n            }\n\n            public function get($id)\n            {\n                if (isset($this->services[$id])) {\n                    return $this->services[$id];\n                }\n\n                return parent::get($id);\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "tests/Cli/Runner/DryRunnerTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Cli\\Runner;\n\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\nuse Zalas\\Toolbox\\Cli\\Runner\\DryRunner;\nuse Zalas\\Toolbox\\Runner\\Runner;\nuse Zalas\\Toolbox\\Tool\\Command;\n\nclass DryRunnerTest extends TestCase\n{\n    private DryRunner $runner;\n\n    private OutputInterface|MockObject $out;\n\n    protected function setUp(): void\n    {\n        $this->out = $this->createMock(OutputInterface::class);\n        $this->runner = new DryRunner($this->out);\n    }\n\n    public function test_it_is_a_runner()\n    {\n        $this->assertInstanceOf(Runner::class, $this->runner);\n    }\n\n    public function test_it_sends_the_command_to_the_output()\n    {\n        $this->out->expects(self::once())\n            ->method('writeln')\n            ->with('echo \"Foo\"');\n\n        $result = $this->runner->run(new class implements Command {\n            public function __toString(): string\n            {\n                return 'echo \"Foo\"';\n            }\n        });\n\n        $this->assertSame(0, $result);\n    }\n}\n"
  },
  {
    "path": "tests/Cli/ServiceContainer/LazyRunnerTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Cli\\ServiceContainer;\n\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\Toolbox\\Cli\\ServiceContainer\\LazyRunner;\nuse Zalas\\Toolbox\\Cli\\ServiceContainer\\RunnerFactory;\nuse Zalas\\Toolbox\\Runner\\Runner;\nuse Zalas\\Toolbox\\Tool\\Command;\n\nclass LazyRunnerTest extends TestCase\n{\n    private LazyRunner $lazyRunner;\n\n    private RunnerFactory|MockObject $factory;\n\n    protected function setUp(): void\n    {\n        $this->factory = $this->createMock(RunnerFactory::class);\n\n        $this->lazyRunner = new LazyRunner($this->factory);\n    }\n\n    public function test_it_is_a_runner()\n    {\n        $this->assertInstanceOf(Runner::class, $this->lazyRunner);\n    }\n\n    public function test_it_returns_status_code_of_returned_by_the_created_runner()\n    {\n        $command = $this->command();\n\n        $runner = $this->givenRunner(command: $command, result: 1);\n        $this->givenFactoryCreates($runner);\n\n        $this->assertSame(1, $this->lazyRunner->run($command));\n    }\n\n    public function test_it_only_initializes_the_runner_once()\n    {\n        $command = $this->command();\n\n        $runner = $this->givenRunner($command, 0);\n\n        $this->factory\n            ->expects(self::once())\n            ->method('createRunner')\n            ->willReturn($runner);\n\n        $this->lazyRunner->run($command);\n        $this->lazyRunner->run($command);\n    }\n\n    public function givenRunner(Command $command, int $result): Runner\n    {\n        $runner = $this->createStub(Runner::class);\n        $runner->method('run')->with($command)->willReturn($result);\n\n        return $runner;\n    }\n\n    private function command(): Command\n    {\n        return new Command\\ShCommand('any command');\n    }\n\n    private function givenFactoryCreates(Runner $runner): void\n    {\n        $this->factory->method('createRunner')->willReturn($runner);\n    }\n}\n"
  },
  {
    "path": "tests/Cli/ServiceContainer/RunnerFactoryTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Cli\\ServiceContainer;\n\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\ContainerInterface;\nuse Symfony\\Component\\Console\\Input\\ArrayInput;\nuse Symfony\\Component\\Console\\Input\\InputDefinition;\nuse Symfony\\Component\\Console\\Input\\InputInterface;\nuse Symfony\\Component\\Console\\Input\\InputOption;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\nuse Zalas\\Toolbox\\Cli\\Runner\\DryRunner;\nuse Zalas\\Toolbox\\Cli\\ServiceContainer\\RunnerFactory;\nuse Zalas\\Toolbox\\Runner\\ParametrisedRunner;\nuse Zalas\\Toolbox\\Runner\\PassthruRunner;\nuse Zalas\\Toolbox\\Tool\\Command;\n\nclass RunnerFactoryTest extends TestCase\n{\n    private RunnerFactory $runnerFactory;\n\n    private InputInterface $input;\n\n    private OutputInterface|MockObject $output;\n\n    protected function setUp(): void\n    {\n        $this->input = $this->givenInput([]);\n        $this->output = $this->createMock(OutputInterface::class);\n\n        $container = new class([ InputInterface::class => &$this->input, OutputInterface::class => &$this->output, ]) implements ContainerInterface {\n\n            public function __construct(private readonly array $services)\n            {\n            }\n\n            public function get(string $id)\n            {\n                return $this->services[$id];\n            }\n\n            public function has(string $id): bool\n            {\n                return isset($this->services[$id]);\n            }\n        };\n\n        $this->runnerFactory = new RunnerFactory($container);\n    }\n\n    public function test_it_creates_the_passthru_runner_by_default()\n    {\n        $runner = $this->runnerFactory->createRunner();\n\n        $this->assertInstanceOf(PassthruRunner::class, $runner);\n    }\n\n    public function test_it_creates_the_dry_runner_if_dry_run_option_is_passed()\n    {\n        $this->givenInput(['--dry-run' => true]);\n\n        $runner = $this->runnerFactory->createRunner();\n\n        $this->assertInstanceOf(DryRunner::class, $runner);\n    }\n\n    public function test_it_creates_the_parametrised_runner_if_target_dir_option_is_present()\n    {\n        $this->givenInput(['--target-dir' => '/usr/local/bin']);\n\n        $runner = $this->runnerFactory->createRunner();\n\n        $this->assertInstanceOf(ParametrisedRunner::class, $runner);\n    }\n\n    public function test_the_parametrised_runner_includes_the_target_dir_parameter()\n    {\n        $this->givenInput(['--target-dir' => '/usr/local/bin', '--dry-run' => true]);\n\n        $this->output->expects(self::once())->method('writeln')->with('ls /usr/local/bin');\n\n        $runner = $this->runnerFactory->createRunner();\n\n        $runner->run(new class implements Command {\n            public function __toString(): string\n            {\n                return 'ls %target-dir%';\n            }\n        });\n    }\n\n    public function test_it_throws_an_exception_if_target_dir_does_not_exist()\n    {\n        $this->expectException(ContainerExceptionInterface::class);\n\n        $this->givenInput(['--target-dir' => '/foo/bar/baz']);\n\n        $this->runnerFactory->createRunner();\n    }\n\n    public function test_it_uses_the_real_path_as_target_dir()\n    {\n        $this->givenInput(['--target-dir' => __DIR__.'/../../../bin', '--dry-run' => true]);\n\n        $this->output->expects(self::once())->method('writeln')->with(\\sprintf('ls %s', \\realpath(__DIR__.'/../../../bin')));\n\n        $runner = $this->runnerFactory->createRunner();\n        $runner->run(new class implements Command {\n            public function __toString(): string\n            {\n                return 'ls %target-dir%';\n            }\n        });\n    }\n\n    private function givenInput(array $parameters): InputInterface\n    {\n        $this->input = new ArrayInput($parameters, new InputDefinition(\\array_filter([\n            new InputOption('dry-run', null, InputOption::VALUE_NONE),\n            isset($parameters['--target-dir']) ? new InputOption('target-dir', null, InputOption::VALUE_REQUIRED) : null,\n        ])));\n\n        return $this->input;\n    }\n}\n"
  },
  {
    "path": "tests/Cli/ServiceContainerTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Cli;\n\nuse PHPUnit\\Framework\\Attributes\\DataProvider;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\ContainerInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Symfony\\Component\\Console\\Input\\InputInterface;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\nuse Zalas\\Toolbox\\Cli\\Command\\InstallCommand;\nuse Zalas\\Toolbox\\Cli\\Command\\ListCommand;\nuse Zalas\\Toolbox\\Cli\\Command\\TestCommand;\nuse Zalas\\Toolbox\\Cli\\ServiceContainer;\nuse Zalas\\Toolbox\\Cli\\ServiceContainer\\LazyRunner;\nuse Zalas\\Toolbox\\Runner\\Runner;\n\nclass ServiceContainerTest extends TestCase\n{\n    private ServiceContainer $container;\n\n    protected function setUp(): void\n    {\n        $this->container = new ServiceContainer();\n        $this->container->set(InputInterface::class, $this->createStub(InputInterface::class));\n        $this->container->set(OutputInterface::class, $this->createStub(OutputInterface::class));\n    }\n\n    public function test_it_is_a_psr_container()\n    {\n        $this->assertInstanceOf(ContainerInterface::class, $this->container);\n    }\n\n    public function test_it_returns_false_if_service_is_not_registered()\n    {\n        $this->assertFalse($this->container->has('foo'));\n    }\n\n    #[DataProvider('provideApplicationServices')]\n    public function test_it_creates_application_services(string $serviceId, string $expectedType)\n    {\n        $this->assertTrue($this->container->has($serviceId));\n        $this->assertInstanceOf($expectedType, $this->container->get($serviceId));\n    }\n\n    public static function provideApplicationServices(): \\Generator\n    {\n        yield [InstallCommand::class, InstallCommand::class];\n        yield [ListCommand::class, ListCommand::class];\n        yield [TestCommand::class, TestCommand::class];\n        yield [Runner::class, LazyRunner::class];\n    }\n\n    public function test_it_throws_an_exception_if_unregistered_service_is_accessed()\n    {\n        $this->expectException(NotFoundExceptionInterface::class);\n        $this->expectExceptionMessage('The \"foo\" service is not registered in the service container.');\n\n        $this->container->get('foo');\n    }\n\n    public function test_it_registers_a_runtime_service()\n    {\n        $service = $this->createStub(InputInterface::class);\n\n        $this->container->set(InputInterface::class, $service);\n\n        $this->assertTrue($this->container->has(InputInterface::class));\n        $this->assertSame($service, $this->container->get(InputInterface::class));\n    }\n\n    public function test_it_returns_false_if_runtime_service_has_not_been_defined()\n    {\n        $this->container = new ServiceContainer();\n\n        $this->assertFalse($this->container->has(InputInterface::class));\n    }\n\n    public function test_it_throws_an_exception_if_missing_runtime_service_is_accessed()\n    {\n        $this->expectException(NotFoundExceptionInterface::class);\n\n        $this->container = new ServiceContainer();\n        $this->container->get(InputInterface::class);\n    }\n\n    public function test_it_throws_an_exception_if_unknown_runtime_service_is_provided()\n    {\n        $this->expectException(ContainerExceptionInterface::class);\n        $this->expectExceptionMessage('The \"foo\" runtime service is not expected.');\n\n        $this->container->set('foo', new \\stdClass());\n    }\n}\n"
  },
  {
    "path": "tests/Json/Factory/AssertTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Json\\Factory;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\Toolbox\\Json\\Factory\\Assert;\n\nclass AssertTest extends TestCase\n{\n    public function test_it_throws_an_exception_if_a_field_is_missing()\n    {\n        $this->expectException(\\InvalidArgumentException::class);\n        $this->expectExceptionMessage('Missing fields \"b, d\" in the Test: `{\"a\":\"A\",\"c\":\"C\"}`.');\n\n        Assert::requireFields(['a', 'b', 'c', 'd'], ['a' => 'A', 'c' => 'C'], 'Test');\n    }\n}\n"
  },
  {
    "path": "tests/Json/Factory/BoxBuildCommandFactoryTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Json\\Factory;\n\nuse PHPUnit\\Framework\\Attributes\\DataProvider;\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\Toolbox\\Json\\Factory\\BoxBuildCommandFactory;\nuse Zalas\\Toolbox\\Tool\\Command\\BoxBuildCommand;\n\nclass BoxBuildCommandFactoryTest extends TestCase\n{\n    private const REPOSITORY = 'https://github.com/behat/behat.git';\n\n    private const PHAR = 'behat.phar';\n\n    private const BIN = '/usr/local/bin/behat';\n\n    private const VERSION = 'v3.4.0';\n\n    public function test_it_creates_a_command()\n    {\n        $command = BoxBuildCommandFactory::import([\n            'repository' => self::REPOSITORY,\n            'phar' => self::PHAR,\n            'bin' => self::BIN,\n            'version' => self::VERSION,\n        ]);\n\n        $this->assertInstanceOf(BoxBuildCommand::class, $command);\n        $this->assertMatchesRegularExpression('#git checkout '.self::VERSION.'#', (string) $command);\n    }\n\n    public function test_the_version_is_not_required()\n    {\n        $command = BoxBuildCommandFactory::import([\n            'repository' => self::REPOSITORY,\n            'phar' => self::PHAR,\n            'bin' => self::BIN,\n        ]);\n\n        $this->assertInstanceOf(BoxBuildCommand::class, $command);\n    }\n\n    #[DataProvider('provideRequiredProperties')]\n    public function test_it_complains_if_any_of_required_properties_is_missing(string $property)\n    {\n        $this->expectException(\\InvalidArgumentException::class);\n\n        $properties = [\n            'repository' => self::REPOSITORY,\n            'phar' => self::PHAR,\n            'bin' => self::BIN,\n        ];\n\n        unset($properties[$property]);\n\n        BoxBuildCommandFactory::import($properties);\n    }\n\n    public static function provideRequiredProperties(): \\Generator\n    {\n        yield ['repository'];\n        yield ['phar'];\n        yield ['bin'];\n    }\n}\n"
  },
  {
    "path": "tests/Json/Factory/ComposerBinPluginCommandFactoryTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Json\\Factory;\n\nuse PHPUnit\\Framework\\Attributes\\DataProvider;\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\Toolbox\\Json\\Factory\\ComposerBinPluginCommandFactory;\nuse Zalas\\Toolbox\\Tool\\Collection;\nuse Zalas\\Toolbox\\Tool\\Command\\ComposerBinPluginCommand;\nuse Zalas\\Toolbox\\Tool\\Command\\ComposerBinPluginLinkCommand;\n\nclass ComposerBinPluginCommandFactoryTest extends TestCase\n{\n    private const PACKAGE = 'phpstan/phpstan';\n    private const NAMESPACE = 'tools';\n\n    public function test_it_creates_a_command()\n    {\n        $command = ComposerBinPluginCommandFactory::import([\n            'package' => self::PACKAGE,\n            'namespace' => self::NAMESPACE,\n        ]);\n\n        $this->assertInstanceOf(ComposerBinPluginCommand::class, $command);\n    }\n\n    public function test_it_creates_a_command_with_links_in_tools()\n    {\n        $command = ComposerBinPluginCommandFactory::import([\n            'package' => self::PACKAGE,\n            'namespace' => self::NAMESPACE,\n            'links' => ['/tools/phpstan' => 'phpstan'],\n        ]);\n\n        $this->assertInstanceOf(ComposerBinPluginCommand::class, $command);\n        $this->assertEquals(\n            Collection::create([\n                new ComposerBinPluginLinkCommand('phpstan', '/tools/phpstan', self::NAMESPACE)\n            ]),\n            $command->links()\n        );\n    }\n\n    #[DataProvider('provideRequiredProperties')]\n    public function test_it_complains_if_any_of_required_properties_is_missing(string $property)\n    {\n        $this->expectException(\\InvalidArgumentException::class);\n\n        $properties = [\n            'package' => self::PACKAGE,\n            'namespace' => self::NAMESPACE,\n        ];\n\n        unset($properties[$property]);\n\n        ComposerBinPluginCommandFactory::import($properties);\n    }\n\n    public static function provideRequiredProperties(): \\Generator\n    {\n        yield ['package'];\n        yield ['namespace'];\n    }\n}\n"
  },
  {
    "path": "tests/Json/Factory/ComposerGlobalInstallCommandFactoryTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Json\\Factory;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\Toolbox\\Json\\Factory\\ComposerGlobalInstallCommandFactory;\nuse Zalas\\Toolbox\\Tool\\Command\\ComposerGlobalInstallCommand;\n\nclass ComposerGlobalInstallCommandFactoryTest extends TestCase\n{\n    private const PACKAGE = 'phan/phan';\n\n    public function test_creates_a_command()\n    {\n        $command = ComposerGlobalInstallCommandFactory::import([\n            'package' => self::PACKAGE,\n        ]);\n\n        $this->assertInstanceOf(ComposerGlobalInstallCommand::class, $command);\n    }\n\n    public function test_it_complains_if_package_is_missing()\n    {\n        $this->expectException(\\InvalidArgumentException::class);\n\n        ComposerGlobalInstallCommandFactory::import([]);\n    }\n}\n"
  },
  {
    "path": "tests/Json/Factory/ComposerInstallCommandFactoryTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Json\\Factory;\n\nuse PHPUnit\\Framework\\Attributes\\DataProvider;\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\Toolbox\\Json\\Factory\\ComposerInstallCommandFactory;\nuse Zalas\\Toolbox\\Tool\\Command\\ComposerInstallCommand;\n\nclass ComposerInstallCommandFactoryTest extends TestCase\n{\n    private const REPOSITORY = 'https://github.com/behat/behat.git';\n    private const LOCATION = '/tools';\n    private const VERSION = 'v3.4.0';\n\n    public function test_it_creates_a_command()\n    {\n        $command = ComposerInstallCommandFactory::import([\n            'repository' => self::REPOSITORY,\n            'target-dir' => self::LOCATION,\n            'version' => self::VERSION,\n        ]);\n\n        $this->assertInstanceOf(ComposerInstallCommand::class, $command);\n        $this->assertMatchesRegularExpression('#git checkout '.self::VERSION.'#', (string) $command);\n    }\n\n    #[DataProvider('provideRequiredProperties')]\n    public function test_it_complains_if_a_required_property_is_missing(string $property)\n    {\n        $this->expectException(\\InvalidArgumentException::class);\n\n        $properties = [\n            'repository' => self::REPOSITORY,\n            'target-dir' => self::LOCATION,\n            'version' => self::VERSION,\n        ];\n        unset($properties[$property]);\n\n        ComposerInstallCommandFactory::import($properties);\n    }\n\n    public static function provideRequiredProperties(): \\Generator\n    {\n        yield ['repository'];\n        yield ['target-dir'];\n    }\n}\n"
  },
  {
    "path": "tests/Json/Factory/FileDownloadCommandFactoryTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Json\\Factory;\n\nuse PHPUnit\\Framework\\Attributes\\DataProvider;\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\Toolbox\\Json\\Factory\\FileDownloadCommandFactory;\nuse Zalas\\Toolbox\\Tool\\Command\\FileDownloadCommand;\n\nclass FileDownloadCommandFactoryTest extends TestCase\n{\n    private const URL = 'https://example.com/file';\n    private const FILE = '/usr/local/bin/file.txt';\n    \n    public function test_it_creates_a_command()\n    {\n        $command = FileDownloadCommandFactory::import([\n            'url' => self::URL,\n            'file' => self::FILE,\n        ]);\n\n        $this->assertInstanceOf(FileDownloadCommand::class, $command);\n    }\n\n    #[DataProvider('provideRequiredProperties')]\n    public function test_it_complains_if_any_of_required_properties_is_missing(string $property)\n    {\n        $this->expectException(\\InvalidArgumentException::class);\n\n        $properties = [\n            'url' => self::URL,\n            'file' => self::FILE,\n        ];\n\n        unset($properties[$property]);\n\n        FileDownloadCommandFactory::import($properties);\n    }\n\n    public static function provideRequiredProperties(): \\Generator\n    {\n        yield ['url'];\n        yield ['file'];\n    }\n}\n"
  },
  {
    "path": "tests/Json/Factory/PharDownloadCommandFactoryTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Json\\Factory;\n\nuse PHPUnit\\Framework\\Attributes\\DataProvider;\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\Toolbox\\Json\\Factory\\PharDownloadCommandFactory;\nuse Zalas\\Toolbox\\Tool\\Command\\PharDownloadCommand;\n\nclass PharDownloadCommandFactoryTest extends TestCase\n{\n    private const PHAR = 'https://example.com/foo.phar';\n    private const BIN = '/usr/local/bin/foo';\n\n    public function test_it_creates_a_command()\n    {\n        $command = PharDownloadCommandFactory::import([\n            'phar' => self::PHAR,\n            'bin' => self::BIN,\n        ]);\n        \n        $this->assertInstanceOf(PharDownloadCommand::class, $command);\n    }\n\n    #[DataProvider('provideRequiredProperties')]\n    public function test_it_complains_if_any_of_required_properties_is_missing(string $property)\n    {\n        $this->expectException(\\InvalidArgumentException::class);\n\n        $properties = [\n            'phar' => self::PHAR,\n            'bin' => self::BIN,\n        ];\n\n        unset($properties[$property]);\n\n        PharDownloadCommandFactory::import($properties);\n    }\n\n    public static function provideRequiredProperties(): \\Generator\n    {\n        yield ['phar'];\n        yield ['bin'];\n    }\n}\n"
  },
  {
    "path": "tests/Json/Factory/PhiveInstallCommandFactoryTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Json\\Factory;\n\nuse PHPUnit\\Framework\\Attributes\\DataProvider;\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\Toolbox\\Json\\Factory\\PhiveInstallCommandFactory;\nuse Zalas\\Toolbox\\Tool\\Command\\PhiveInstallCommand;\n\nclass PhiveInstallCommandFactoryTest extends TestCase\n{\n    private const ALIAS = 'example/foo';\n    private const BIN = '/usr/local/bin/foo';\n    private const SIG = '0000000000000000';\n\n    public function test_it_creates_a_command()\n    {\n        $command = PhiveInstallCommandFactory::import([\n            'alias' => self::ALIAS,\n            'bin' => self::BIN,\n            'sig' => self::SIG\n        ]);\n        \n        $this->assertInstanceOf(PhiveInstallCommand::class, $command);\n        $this->assertStringNotContainsString('unsigned', (string)$command);\n    }\n\n    #[DataProvider('provideRequiredProperties')]\n    public function test_it_complains_if_any_of_required_properties_is_missing(string $property)\n    {\n        $this->expectException(\\InvalidArgumentException::class);\n\n        $properties = [\n            'alias' => self::ALIAS,\n            'bin' => self::BIN,\n        ];\n\n        unset($properties[$property]);\n\n        $command = PhiveInstallCommandFactory::import($properties);\n        $this->assertStringContainsString('unsigned', (string)$command);\n    }\n\n    public function test_it_accepts_unsigned_phars()\n    {\n        $properties = [\n            'alias' => self::ALIAS,\n            'bin' => self::BIN\n        ];\n\n        $command = PhiveInstallCommandFactory::import($properties);\n        $this->assertStringContainsString('unsigned', (string)$command);\n    }\n\n    public static function provideRequiredProperties(): \\Generator\n    {\n        yield ['alias'];\n        yield ['bin'];\n    }\n}\n"
  },
  {
    "path": "tests/Json/Factory/ShCommandFactoryTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Json\\Factory;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\Toolbox\\Json\\Factory\\ShCommandFactory;\nuse Zalas\\Toolbox\\Tool\\Command\\ShCommand;\n\nclass ShCommandFactoryTest extends TestCase\n{\n    public function test_creates_a_command()\n    {\n        $command = ShCommandFactory::import([\n            'command' => 'echo \"42\"',\n        ]);\n\n        $this->assertInstanceOf(ShCommand::class, $command);\n    }\n\n    public function test_it_complains_if_command_is_missing()\n    {\n        $this->expectException(\\InvalidArgumentException::class);\n\n        ShCommandFactory::import([]);\n    }\n}\n"
  },
  {
    "path": "tests/Json/Factory/ToolFactoryTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Json\\Factory;\n\nuse PHPUnit\\Framework\\Attributes\\DataProvider;\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\Toolbox\\Json\\Factory\\ToolFactory;\nuse Zalas\\Toolbox\\Tool\\Command;\nuse Zalas\\Toolbox\\Tool\\Command\\BoxBuildCommand;\nuse Zalas\\Toolbox\\Tool\\Command\\ComposerBinPluginCommand;\nuse Zalas\\Toolbox\\Tool\\Command\\ComposerGlobalInstallCommand;\nuse Zalas\\Toolbox\\Tool\\Command\\ComposerInstallCommand;\nuse Zalas\\Toolbox\\Tool\\Command\\FileDownloadCommand;\nuse Zalas\\Toolbox\\Tool\\Command\\MultiStepCommand;\nuse Zalas\\Toolbox\\Tool\\Command\\PharDownloadCommand;\nuse Zalas\\Toolbox\\Tool\\Command\\PhiveInstallCommand;\nuse Zalas\\Toolbox\\Tool\\Command\\ShCommand;\nuse Zalas\\Toolbox\\Tool\\Command\\TestCommand;\n\nclass ToolFactoryTest extends TestCase\n{\n    public function test_it_imports_tool_definition_from_an_array()\n    {\n        $tool = ToolFactory::import([\n            'name' => 'phpstan',\n            'summary' => 'Static analysis tool',\n            'website' => 'https://github.com/phpstan/phpstan',\n            'command' => [\n                'composer-bin-plugin' => [\n                    'package' => 'phpstan/phpstan',\n                    'namespace' => 'tools'\n                ]\n            ],\n            'test' => '/usr/bin/true',\n            'tags' => ['qa', 'static-analysis'],\n        ]);\n\n        $this->assertSame('phpstan', $tool->name());\n        $this->assertSame('Static analysis tool', $tool->summary());\n        $this->assertSame('https://github.com/phpstan/phpstan', $tool->website());\n        $this->assertSame(['qa', 'static-analysis'], $tool->tags());\n        $this->assertInstanceOf(Command::class, $tool->command());\n        $this->assertInstanceOf(TestCommand::class, $tool->testCommand());\n    }\n\n    public function test_it_imports_the_composer_bin_plugin_command()\n    {\n        $tool = ToolFactory::import($this->definition([\n            'command' => [\n                'composer-bin-plugin' => [\n                    'package' => 'phpstan/phpstan',\n                    'namespace' => 'tools'\n                ]\n            ]\n        ]));\n\n        $this->assertInstanceOf(ComposerBinPluginCommand::class, $tool->command());\n    }\n\n    public function test_it_imports_the_phar_download_command()\n    {\n        $tool = ToolFactory::import($this->definition([\n            'command' => [\n                'phar-download' => [\n                    'phar' => 'phpstan/phpstan',\n                    'bin' => 'tools'\n                ]\n            ]\n        ]));\n\n        $this->assertInstanceOf(PharDownloadCommand::class, $tool->command());\n    }\n\n    public function test_it_imports_the_phive_install_command()\n    {\n        $tool = ToolFactory::import($this->definition([\n            'command' => [\n                'phive-install' => [\n                    'alias' => 'phpstan/phpstan',\n                    'bin' => 'tools'\n                ]\n            ]\n        ]));\n\n        $this->assertInstanceOf(PhiveInstallCommand::class, $tool->command());\n    }\n\n    public function test_it_imports_the_file_download_command()\n    {\n        $tool = ToolFactory::import($this->definition([\n            'command' => [\n                'file-download' => [\n                    'url' => 'http://example.com/file',\n                    'file' => 'file'\n                ]\n            ]\n        ]));\n\n        $this->assertInstanceOf(FileDownloadCommand::class, $tool->command());\n    }\n\n    public function test_it_imports_the_box_build_command()\n    {\n        $tool = ToolFactory::import($this->definition([\n            'command' => [\n                'box-build' => [\n                    'repository' => 'https://github.com/behat/behat.git',\n                    'phar' => 'behat.phar',\n                    'bin' => '/usr/local/bin/behat',\n                    'version' => 'v3.4.0',\n                ]\n            ]\n        ]));\n\n        $this->assertInstanceOf(BoxBuildCommand::class, $tool->command());\n    }\n\n    public function test_it_imports_the_composer_install_command()\n    {\n        $tool = ToolFactory::import($this->definition([\n            'command' => [\n                'composer-install' => [\n                    'repository' => 'https://github.com/behat/behat.git',\n                    'target-dir' => '/usr/local/bin',\n                    'version' => 'v3.4.0',\n                ]\n            ]\n        ]));\n\n        $this->assertInstanceOf(ComposerInstallCommand::class, $tool->command());\n    }\n\n    public function test_it_imports_the_composer_global_install_command()\n    {\n        $tool = ToolFactory::import($this->definition([\n            'command' => [\n                'composer-global-install' => [\n                    'package' => 'behat/behat',\n                    'version' => 'v3.4.0',\n                ]\n            ]\n        ]));\n\n        $this->assertInstanceOf(ComposerGlobalInstallCommand::class, $tool->command());\n    }\n\n    public function test_it_imports_the_sh_command()\n    {\n        $tool = ToolFactory::import($this->definition([\n            'command' => [\n                'sh' => [\n                    'command' => 'echo \"42\"',\n                ]\n            ]\n        ]));\n\n        $this->assertInstanceOf(ShCommand::class, $tool->command());\n    }\n\n    public function test_it_imports_multiple_commands()\n    {\n        $tool = ToolFactory::import($this->definition([\n            'command' => [\n                'phar-download' => [\n                    'phar' => 'phpstan/phpstan',\n                    'bin' => 'tools'\n                ],\n                'file-download' => [\n                    [\n                        'url' => 'http://example.com/file1',\n                        'file' => 'file1'\n                    ],\n                    [\n                        'url' => 'http://example.com/file2',\n                        'file' => 'file2'\n                    ]\n                ]\n            ]\n        ]));\n\n        $this->assertInstanceOf(MultiStepCommand::class, $tool->command());\n    }\n\n    public function test_it_complains_if_it_cannot_recognise_the_command()\n    {\n        $this->expectException(\\RuntimeException::class);\n        $this->expectExceptionMessageMatches('/Unrecognised command: \"foo\". Supported commands are: \"phar-download,.*?\"/');\n\n        ToolFactory::import($this->definition(['command' => ['foo' => ['phar' => 'phpstan/phpstan']]]));\n    }\n\n    public function test_it_complains_if_the_command_is_empty()\n    {\n        $this->expectException(\\RuntimeException::class);\n\n        ToolFactory::import($this->definition(['command' => []]));\n    }\n\n    #[DataProvider('provideRequiredProperties')]\n    public function test_it_complains_if_any_of_required_properties_is_missing(string $property)\n    {\n        $this->expectException(\\InvalidArgumentException::class);\n\n        $properties = $this->definition();\n\n        unset($properties[$property]);\n\n        ToolFactory::import($properties);\n    }\n\n    public static function provideRequiredProperties(): \\Generator\n    {\n        yield ['name'];\n        yield ['summary'];\n        yield ['website'];\n        yield ['command'];\n        yield ['test'];\n    }\n\n    private function definition(array $overrides = []): array\n    {\n        return \\array_merge(\n            [\n                'name' => 'phpstan',\n                'summary' => 'Static analysis tool',\n                'website' => 'https://github.com/phpstan/phpstan',\n                'command' => [\n                    'composer-bin-plugin' => [\n                        'package' => 'phpstan/phpstan',\n                        'namespace' => 'tools'\n                    ]\n                ],\n                'test' => '/usr/bin/true',\n            ],\n            $overrides\n        );\n    }\n}\n"
  },
  {
    "path": "tests/Json/JsonToolsTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Json;\n\nuse InvalidArgumentException;\nuse PHPUnit\\Framework\\TestCase;\nuse RuntimeException;\nuse Zalas\\Toolbox\\Json\\JsonTools;\nuse Zalas\\Toolbox\\Tool\\Filter;\nuse Zalas\\Toolbox\\Tool\\Tools;\n\nclass JsonToolsTest extends TestCase\n{\n    public function test_it_is_a_tools_repository()\n    {\n        $this->assertInstanceOf(Tools::class, new JsonTools($this->locator([__DIR__.'/../resources/tools.json'])));\n    }\n\n    public function test_it_throws_an_exception_if_resource_is_missing()\n    {\n        $this->expectException(InvalidArgumentException::class);\n        $this->expectExceptionMessageMatches('/Could not read the file/');\n\n        $tools = new JsonTools($this->locator(['/foo/tools.json']));\n        $tools->all($this->filter());\n    }\n\n    public function test_it_throws_an_exception_if_resource_contains_invalid_json()\n    {\n        $this->expectException(RuntimeException::class);\n        $this->expectExceptionMessageMatches('/Failed to parse json/');\n\n        $tools = new JsonTools($this->locator([__DIR__.'/../resources/invalid.json']));\n        $tools->all($this->filter());\n    }\n\n    public function test_it_throws_an_exception_if_tools_are_not_present_in_the_resource()\n    {\n        $this->expectException(RuntimeException::class);\n        $this->expectExceptionMessageMatches('/Failed to find any tools/');\n\n        $tools = new JsonTools($this->locator([__DIR__.'/../resources/no-tools.json']));\n        $tools->all($this->filter());\n    }\n\n    public function test_it_throws_an_exception_if_tools_is_not_a_collection()\n    {\n        $this->expectException(RuntimeException::class);\n        $this->expectExceptionMessageMatches('/Failed to find any tools/');\n\n        $tools = new JsonTools($this->locator([__DIR__.'/../resources/invalid-tools.json']));\n        $tools->all($this->filter());\n    }\n\n    public function test_it_loads_tools_from_multiple_resources()\n    {\n        $tools = \\iterator_to_array(\n            (new JsonTools($this->locator([\n                __DIR__.'/../resources/pre-installation.json',\n                __DIR__.'/../resources/tools.json',\n            ])))->all($this->filter())\n        );\n\n        $this->assertCount(8, $tools);\n        $this->assertSame('composer', $tools[0]->name());\n        $this->assertSame('composer-bin-plugin', $tools[1]->name());\n        $this->assertSame('box', $tools[2]->name());\n        $this->assertSame('analyze', $tools[3]->name());\n        $this->assertSame('behat', $tools[4]->name());\n        $this->assertSame('deptrac', $tools[5]->name());\n        $this->assertSame('infection', $tools[6]->name());\n        $this->assertSame('phpstan', $tools[7]->name());\n    }\n\n    public function test_it_filters_out_tools()\n    {\n        $tools = \\iterator_to_array(\n            (new JsonTools($this->locator([__DIR__.'/../resources/tools.json'])))->all($this->filter(['static-analysis', 'testing']))\n        );\n\n        $this->assertCount(1, $tools);\n        $this->assertSame('phpstan', $tools[0]->name());\n    }\n\n    private function locator(array $resources): callable\n    {\n        return function () use ($resources): array {\n            return $resources;\n        };\n    }\n\n    private function filter(array $excludedTags = []): Filter\n    {\n        return new Filter($excludedTags, []);\n    }\n}\n"
  },
  {
    "path": "tests/Runner/ParametrisedRunnerTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Runner;\n\nuse PHPUnit\\Framework\\MockObject\\Stub;\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\Toolbox\\Runner\\ParametrisedRunner;\nuse Zalas\\Toolbox\\Runner\\Runner;\nuse Zalas\\Toolbox\\Tool\\Command;\nuse Zalas\\Toolbox\\Tool\\Command\\ShCommand;\n\nclass ParametrisedRunnerTest extends TestCase\n{\n    private ParametrisedRunner $runner;\n\n    private Runner|Stub $decoratedRunner;\n\n    protected function setUp(): void\n    {\n        $this->decoratedRunner = $this->createStub(Runner::class);\n        $this->runner = new ParametrisedRunner($this->decoratedRunner, ['%foo%' => 'ABC']);\n    }\n\n    public function test_it_is_a_runner()\n    {\n        $this->assertInstanceOf(Runner::class, $this->runner);\n    }\n\n    public function test_it_replaces_parameter_holders_in_the_command_before_running_it()\n    {\n        $command = $this->command('echo \"%foo%\"');\n\n        $this->decoratedRunner->method('run')\n            ->with(self::callback(function (Command $c) {\n                if ('echo \"ABC\"' !== $c->__toString()) {\n                    throw new \\RuntimeException(\\sprintf('Expected `echo \"ABC\"`, but got `%s`.', $c->__toString()));\n                }\n\n                return true;\n            }))\n            ->willReturn(42);\n\n        $exitCode = $this->runner->run($command);\n\n        $this->assertSame(42, $exitCode);\n    }\n\n    private function command(string $commandString): Command\n    {\n        return new ShCommand($commandString);\n    }\n}\n"
  },
  {
    "path": "tests/Runner/PassthruRunnerTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Runner;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\Toolbox\\Runner\\PassthruRunner;\nuse Zalas\\Toolbox\\Runner\\Runner;\nuse Zalas\\Toolbox\\Tool\\Command\\ShCommand;\n\nclass PassthruRunnerTest extends TestCase\n{\n    public function test_it_is_a_runner()\n    {\n        $this->assertInstanceOf(Runner::class, new PassthruRunner());\n    }\n\n    public function test_it_returns_the_exit_code_of_the_run_command()\n    {\n        $runner = new PassthruRunner();\n        $this->assertSame(0, $runner->run(new ShCommand('true')));\n        $this->assertSame(1, $runner->run(new ShCommand('false')));\n    }\n\n    public function test_it_outputs_commands_output()\n    {\n        $runner = new PassthruRunner();\n\n        \\ob_start();\n        $runner->run(new ShCommand('echo \"ABC\"'));\n\n        $this->assertSame('ABC'.PHP_EOL, \\ob_get_clean());\n    }\n}\n"
  },
  {
    "path": "tests/Tool/CollectionTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Tool;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\Toolbox\\Tool\\Collection;\n\nclass CollectionTest extends TestCase\n{\n    public function test_it_iterates_over_its_elements()\n    {\n        $elements = [1, 2, 3];\n\n        $c = Collection::create($elements);\n\n        $this->assertIterates($elements, $c);\n    }\n\n    public function test_it_is_cast_to_an_array()\n    {\n        $elements = [1, 2, 3];\n\n        $c = Collection::create($elements);\n\n        $this->assertSame($elements, $c->toArray());\n    }\n\n    public function test_it_merges_two_collections()\n    {\n        $c1 = Collection::create([1, 2, 3]);\n        $c2 = Collection::create([4, 5]);\n\n        $c = $c1->merge($c2);\n\n        $this->assertNotSame($c1, $c, 'merge() creates a new collection');\n        $this->assertNotSame($c2, $c, 'merge() creates a new collection');\n        $this->assertIterates([1, 2, 3, 4, 5], $c);\n    }\n\n    public function test_it_filters_elements_in_the_collection()\n    {\n        $c = Collection::create([1, 2, 3, 4]);\n        $filtered = $c->filter(function (int $e) {\n            return 0 === $e % 2;\n        });\n\n        $this->assertNotSame($c, $filtered, 'filter() creates a new collection');\n        $this->assertIterates([2, 4], $filtered);\n    }\n\n    public function test_it_maps_elements_in_the_collection()\n    {\n        $c = Collection::create([1, 2, 3, 4]);\n        $mapped = $c->map(function (int $e) {\n            return $e * 2;\n        });\n\n        $this->assertNotSame($c, $mapped, 'map() creates a new collection');\n        $this->assertIterates([2, 4, 6, 8], $mapped);\n    }\n\n    public function test_it_folds_the_collection_left()\n    {\n        $c = Collection::create(['a', 'b', 'c']);\n        $reduced = $c->reduce('d', function (string $a, string $b): string {\n            return $a.$b;\n        });\n\n        $this->assertNotSame($c, $reduced, 'reduce() creates a new collection');\n        $this->assertSame('dabc', $reduced);\n    }\n\n    public function test_it_counts_its_elements()\n    {\n        $this->assertSame(3, Collection::create(['a', 'b', 'c'])->count());\n        $this->assertSame(3, \\count(Collection::create(['a', 'b', 'c'])));\n    }\n\n    public function test_it_checks_if_collection_is_empty()\n    {\n        $this->assertFalse(Collection::create(['a', 'b', 'c'])->empty());\n        $this->assertTrue(Collection::create([])->empty());\n    }\n\n    public function test_it_sorts_the_collection()\n    {\n        $c = Collection::create(['ab', 'c', 'aa', 'aaa']);\n        $sorted = $c->sort(function ($left, $right) {\n            return \\strcasecmp($left, $right);\n        });\n\n        $this->assertIterates(['aa', 'aaa', 'ab', 'c'], $sorted);\n        $this->assertIterates(['ab', 'c', 'aa', 'aaa'], $c, 'The original collection is not modified');\n    }\n\n    private function assertIterates(array $elements, Collection $c, string $message = ''): void\n    {\n        $this->assertSame($elements, \\iterator_to_array($c), $message);\n    }\n}\n"
  },
  {
    "path": "tests/Tool/Command/BoxBuildCommandTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Tool\\Command;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\Toolbox\\Tool\\Command;\nuse Zalas\\Toolbox\\Tool\\Command\\BoxBuildCommand;\n\nclass BoxBuildCommandTest extends TestCase\n{\n    private const REPOSITORY = 'https://github.com/behat/behat.git';\n\n    private const PHAR = 'behat.phar';\n\n    private const BIN = '/usr/local/bin/behat';\n\n    private const TMP_DIR = '/tools';\n\n    private const VERSION = 'v3.4.0';\n\n    public function test_it_is_a_command()\n    {\n        $command = new BoxBuildCommand(\n            self::REPOSITORY,\n            self::PHAR,\n            self::BIN,\n            self::TMP_DIR,\n            self::VERSION\n        );\n\n        $this->assertInstanceOf(Command::class, $command);\n    }\n\n    public function test_it_generates_the_installation_command()\n    {\n        $command = new BoxBuildCommand(\n            self::REPOSITORY,\n            self::PHAR,\n            self::BIN,\n            self::TMP_DIR,\n            self::VERSION\n        );\n\n        $this->assertMatchesRegularExpression('#git clone '.self::REPOSITORY.'#', (string) $command);\n        $this->assertMatchesRegularExpression('#cd /tools/behat#', (string) $command);\n        $this->assertMatchesRegularExpression('#git checkout '.self::VERSION.'#', (string) $command);\n        $this->assertMatchesRegularExpression('#composer install --no-dev --prefer-dist -n#', (string) $command);\n        $this->assertMatchesRegularExpression('#box compile#', (string) $command);\n    }\n\n    public function test_it_tries_to_guess_version_number_if_not_given_one()\n    {\n        $command = new BoxBuildCommand(\n            self::REPOSITORY,\n            self::PHAR,\n            self::BIN,\n            self::TMP_DIR\n        );\n\n        $this->assertMatchesRegularExpression('#git checkout \\$\\(git describe --tags .*?\\)#', (string) $command);\n    }\n\n    public function test_it_uses_a_generic_directory_if_name_cannot_be_guessed_from_the_repository()\n    {\n        $command = new BoxBuildCommand(\n            'example.com:foo.git',\n            self::PHAR,\n            self::BIN,\n            self::TMP_DIR\n        );\n\n        $this->assertMatchesRegularExpression('#cd /tools/tmp#', (string) $command);\n    }\n}\n"
  },
  {
    "path": "tests/Tool/Command/ComposerBinPluginCommandTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Tool\\Command;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\Toolbox\\Tool\\Collection;\nuse Zalas\\Toolbox\\Tool\\Command;\nuse Zalas\\Toolbox\\Tool\\Command\\ComposerBinPluginCommand;\nuse Zalas\\Toolbox\\Tool\\Command\\ComposerBinPluginLinkCommand;\n\nclass ComposerBinPluginCommandTest extends TestCase\n{\n    private const PACKAGE = 'phpstan/phpstan';\n    private const NAMESPACE = 'tools';\n\n    private ComposerBinPluginCommand $command;\n\n    protected function setUp(): void\n    {\n        $this->command = new ComposerBinPluginCommand(\n            self::PACKAGE,\n            self::NAMESPACE,\n            Collection::create([])\n        );\n    }\n\n    public function test_it_is_a_command()\n    {\n        $this->assertInstanceOf(Command::class, $this->command);\n    }\n\n    public function test_it_generates_the_installation_command()\n    {\n        $this->assertMatchesRegularExpression('#composer global bin tools require .*? phpstan/phpstan#', (string) $this->command);\n    }\n\n    public function test_it_exposes_the_package_and_namespace()\n    {\n        $this->assertSame(self::PACKAGE, $this->command->package());\n        $this->assertSame(self::NAMESPACE, $this->command->namespace());\n    }\n\n    public function test_it_optionally_creates_a_symlink()\n    {\n        $links =  Collection::create([\n            new ComposerBinPluginLinkCommand('phpstan', '/tools/phpstan', self::NAMESPACE)\n        ]);\n        $this->command = new ComposerBinPluginCommand(self::PACKAGE, self::NAMESPACE, $links);\n\n        $this->assertSame($links, $this->command->links());\n        $this->assertMatchesRegularExpression('#composer global bin tools require .*? phpstan/phpstan#', (string) $this->command);\n        $this->assertMatchesRegularExpression('# && ln -sf.*?phpstan /tools/phpstan#', (string) $this->command);\n    }\n\n    public function test_it_does_not_create_a_symlink_if_links_option_was_not_given()\n    {\n        $this->assertDoesNotMatchRegularExpression('#ln -s#', (string) $this->command);\n    }\n}\n"
  },
  {
    "path": "tests/Tool/Command/ComposerBinPluginLinkCommandTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Tool\\Command;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\Toolbox\\Tool\\Command;\nuse Zalas\\Toolbox\\Tool\\Command\\ComposerBinPluginLinkCommand;\n\nfinal class ComposerBinPluginLinkCommandTest extends TestCase\n{\n    private const SOURCE = 'churn';\n    private const TARGET = '/tools/churn';\n    private const NAMESPACE = 'tools';\n\n    private ComposerBinPluginLinkCommand $command;\n\n    protected function setUp(): void\n    {\n        $this->command = new ComposerBinPluginLinkCommand(self::SOURCE, self::TARGET, self::NAMESPACE);\n    }\n\n    public function test_it_is_a_command()\n    {\n        $this->assertInstanceOf(Command::class, $this->command);\n    }\n\n    public function test_it_generates_a_symlink_command()\n    {\n        $this->assertMatchesRegularExpression('#ln -sf \\$\\{COMPOSER_HOME:-\"~/.composer\"\\}/vendor-bin/tools/vendor/bin/churn /tools/churn#', (string) $this->command);\n    }\n}\n"
  },
  {
    "path": "tests/Tool/Command/ComposerGlobalInstallCommandTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Tool\\Command;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\Toolbox\\Tool\\Command;\nuse Zalas\\Toolbox\\Tool\\Command\\ComposerGlobalInstallCommand;\n\nclass ComposerGlobalInstallCommandTest extends TestCase\n{\n    private const PACKAGE = 'phan/phan';\n\n    private ComposerGlobalInstallCommand $command;\n\n    protected function setUp(): void\n    {\n        $this->command = new ComposerGlobalInstallCommand(self::PACKAGE);\n    }\n\n    public function test_it_is_a_command()\n    {\n        $this->assertInstanceOf(Command::class, $this->command);\n    }\n\n    public function test_it_exposes_the_package_name()\n    {\n        $this->assertSame(self::PACKAGE, $this->command->package());\n    }\n\n    public function test_it_generates_the_installation_command()\n    {\n        $this->assertMatchesRegularExpression('#composer global require .*? phan/phan#', (string) $this->command);\n    }\n}\n"
  },
  {
    "path": "tests/Tool/Command/ComposerGlobalMultiInstallCommandTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Tool\\Command;\n\nuse InvalidArgumentException;\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\Toolbox\\Tool\\Collection;\nuse Zalas\\Toolbox\\Tool\\Command;\nuse Zalas\\Toolbox\\Tool\\Command\\ComposerGlobalInstallCommand;\nuse Zalas\\Toolbox\\Tool\\Command\\ComposerGlobalMultiInstallCommand;\n\nclass ComposerGlobalMultiInstallCommandTest extends TestCase\n{\n    public function test_it_is_a_command()\n    {\n        $command = new ComposerGlobalMultiInstallCommand(Collection::create([\n            new ComposerGlobalInstallCommand('phan/phan'),\n            new ComposerGlobalInstallCommand('phpstan/phpstan'),\n        ]));\n\n        $this->assertInstanceOf(Command::class, $command);\n    }\n\n    public function test_it_generates_a_single_installation_command()\n    {\n        $command = new ComposerGlobalMultiInstallCommand(Collection::create([\n            new ComposerGlobalInstallCommand('phan/phan'),\n            new ComposerGlobalInstallCommand('phpstan/phpstan'),\n        ]));\n\n        $this->assertMatchesRegularExpression('#composer global require .*? phan/phan phpstan/phpstan#', (string) $command);\n    }\n\n    public function test_it_throws_an_exception_if_there_is_no_commands()\n    {\n        $this->expectException(InvalidArgumentException::class);\n\n        new ComposerGlobalMultiInstallCommand(Collection::create([]));\n    }\n}\n"
  },
  {
    "path": "tests/Tool/Command/ComposerInstallCommandTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Tool\\Command;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\Toolbox\\Tool\\Command;\nuse Zalas\\Toolbox\\Tool\\Command\\ComposerInstallCommand;\n\nclass ComposerInstallCommandTest extends TestCase\n{\n    private const REPOSITORY = 'https://github.com/behat/behat.git';\n    private const VERSION = 'v3.4.0';\n    private const TARGET_DIR = '/tools';\n\n    public function test_it_is_a_command()\n    {\n        $command = new ComposerInstallCommand(self::REPOSITORY, self::TARGET_DIR, self::VERSION);\n\n        $this->assertInstanceOf(Command::class, $command);\n    }\n\n    public function test_it_generates_the_installation_command()\n    {\n        $command = new ComposerInstallCommand(self::REPOSITORY, self::TARGET_DIR, self::VERSION);\n\n        $this->assertMatchesRegularExpression('#git clone '.self::REPOSITORY.'#', (string) $command);\n        $this->assertMatchesRegularExpression('#git checkout '.self::VERSION.'#', (string) $command);\n        $this->assertMatchesRegularExpression('#composer install --no-dev --prefer-dist -n#', (string) $command);\n    }\n\n    public function test_it_tries_to_guess_version_number_if_not_given_one()\n    {\n        $command = new ComposerInstallCommand(self::REPOSITORY, self::TARGET_DIR);\n\n        $this->assertMatchesRegularExpression('#git checkout \\$\\(git describe --tags .*?\\)#', (string) $command);\n    }\n}\n"
  },
  {
    "path": "tests/Tool/Command/FileDownloadCommandTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Tool\\Command;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\Toolbox\\Tool\\Command;\nuse Zalas\\Toolbox\\Tool\\Command\\FileDownloadCommand;\n\nclass FileDownloadCommandTest extends TestCase\n{\n    private const URL = 'https://example.com/file';\n    private const FILE = '/usr/local/bin/file.txt';\n\n    private FileDownloadCommand $command;\n\n    protected function setUp(): void\n    {\n        $this->command = new FileDownloadCommand(self::URL, self::FILE);\n    }\n\n    public function test_it_is_a_command()\n    {\n        $this->assertInstanceOf(Command::class, $this->command);\n    }\n\n    public function test_it_generates_the_installation_command()\n    {\n        $this->assertMatchesRegularExpression(\\sprintf('#curl .*? %s -o %s#', self::URL, self::FILE), (string) $this->command);\n    }\n}\n"
  },
  {
    "path": "tests/Tool/Command/MultiStepCommandTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Tool\\Command;\n\nuse InvalidArgumentException;\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\Toolbox\\Tool\\Collection;\nuse Zalas\\Toolbox\\Tool\\Command;\nuse Zalas\\Toolbox\\Tool\\Command\\MultiStepCommand;\nuse Zalas\\Toolbox\\Tool\\Command\\ShCommand;\n\nclass MultiStepCommandTest extends TestCase\n{\n    public function test_it_is_a_command()\n    {\n        $command = new MultiStepCommand(Collection::create([$this->command('echo \"A\"')]));\n\n        $this->assertInstanceOf(Command::class, $command);\n    }\n\n    public function test_it_glues_all_its_subcommands()\n    {\n        $command1 = $this->command('echo \"A\"');\n        $command2 = $this->command('echo \"B\"');\n\n        $command = new MultiStepCommand(Collection::create([$command1, $command2]));\n\n        $this->assertSame('echo \"A\" && echo \"B\"', (string) $command);\n    }\n\n    public function test_it_glues_all_its_subcommands_with_a_custom_glue()\n    {\n        $command1 = $this->command('echo \"A\"');\n        $command2 = $this->command('echo \"B\"');\n\n        $command = new MultiStepCommand(Collection::create([$command1, $command2]), ' ; ');\n\n        $this->assertSame('echo \"A\" ; echo \"B\"', (string) $command);\n    }\n\n    public function test_it_throws_an_exception_if_there_is_no_steps()\n    {\n        $this->expectException(InvalidArgumentException::class);\n\n        new MultiStepCommand(Collection::create([]));\n    }\n\n    private function command(string $commandString): Command\n    {\n        return new ShCommand($commandString);\n    }\n}\n"
  },
  {
    "path": "tests/Tool/Command/OptimisedComposerBinPluginCommandTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Tool\\Command;\n\nuse InvalidArgumentException;\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\Toolbox\\Tool\\Collection;\nuse Zalas\\Toolbox\\Tool\\Command;\nuse Zalas\\Toolbox\\Tool\\Command\\ComposerBinPluginCommand;\nuse Zalas\\Toolbox\\Tool\\Command\\ComposerBinPluginLinkCommand;\nuse Zalas\\Toolbox\\Tool\\Command\\OptimisedComposerBinPluginCommand;\n\nclass OptimisedComposerBinPluginCommandTest extends TestCase\n{\n    public function test_it_is_a_command()\n    {\n        $this->assertInstanceOf(Command::class, new OptimisedComposerBinPluginCommand(Collection::create([new ComposerBinPluginCommand('phpstan/phpstan', 'phpstan', Collection::create([]))])));\n    }\n\n    public function test_it_groups_composer_bin_command_by_namespace()\n    {\n        $commands = [\n            new ComposerBinPluginCommand('phpstan/phpstan', 'phpstan', Collection::create([])),\n            new ComposerBinPluginCommand('phan/phan', 'tools', Collection::create([])),\n            new ComposerBinPluginCommand('behat/behat', 'tools', Collection::create([])),\n        ];\n\n        $command = new OptimisedComposerBinPluginCommand(Collection::create($commands));\n\n        $this->assertMatchesRegularExpression('#composer global bin phpstan require .*? phpstan/phpstan && composer global bin tools require .*? phan/phan behat/behat#', (string) $command);\n    }\n\n    public function test_it_throws_an_exception_if_there_is_no_commands()\n    {\n        $this->expectException(InvalidArgumentException::class);\n\n        new OptimisedComposerBinPluginCommand(Collection::create([]));\n    }\n\n    public function test_it_creates_links_to_composer_bin_commands()\n    {\n        $commands = [\n            new ComposerBinPluginCommand(\n                'phpstan/phpstan',\n                'phpstan',\n                Collection::create([\n                    new ComposerBinPluginLinkCommand('phpstan', '/tools/phpstan', 'phpstan'),\n                    new ComposerBinPluginLinkCommand('phpstan', '/other/path/phpstan', 'phpstan'),\n                ])\n            ),\n            new ComposerBinPluginCommand(\n                'phan/phan',\n                'tools',\n                Collection::create([\n                    new ComposerBinPluginLinkCommand('phan', '/tools/phan', 'tools'),\n                ])\n            ),\n            new ComposerBinPluginCommand(\n                'behat/behat',\n                'tools',\n                Collection::create([\n                    new ComposerBinPluginLinkCommand('behat', '/tools/behat', 'tools'),\n                ])\n            ),\n        ];\n\n        $command = new OptimisedComposerBinPluginCommand(Collection::create($commands));\n\n        $this->assertMatchesRegularExpression('#composer global bin phpstan require .*? phpstan/phpstan && composer global bin tools require .*? phan/phan behat/behat#', (string) $command);\n        $this->assertMatchesRegularExpression('# && ln -sf.*?phpstan /tools/phpstan#', (string) $command);\n        $this->assertMatchesRegularExpression('# && ln -sf.*?phpstan /other/path/phpstan#', (string) $command);\n        $this->assertMatchesRegularExpression('# && ln -sf.*?phan /tools/phan#', (string) $command);\n        $this->assertMatchesRegularExpression('# && ln -sf.*?behat /tools/behat#', (string) $command);\n        $this->assertDoesNotMatchRegularExpression('#&&\\s*&&#', (string) $command, 'It does not generate empty commands');\n    }\n\n    public function test_it_does_not_create_links_if_commands_have_no_links_defined()\n    {\n        $commands = [\n            new ComposerBinPluginCommand('phpstan/phpstan', 'phpstan', Collection::create([])),\n            new ComposerBinPluginCommand('phan/phan', 'tools', Collection::create([])),\n            new ComposerBinPluginCommand('behat/behat', 'tools', Collection::create([])),\n        ];\n\n        $command = new OptimisedComposerBinPluginCommand(Collection::create($commands));\n\n        $this->assertDoesNotMatchRegularExpression('#ln -s#', (string) $command);\n        $this->assertDoesNotMatchRegularExpression('#&&\\s*&&#', (string) $command, 'It does not generate empty commands');\n    }\n}\n"
  },
  {
    "path": "tests/Tool/Command/PharDownloadCommandTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Tool\\Command;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\Toolbox\\Tool\\Command;\nuse Zalas\\Toolbox\\Tool\\Command\\PharDownloadCommand;\n\nclass PharDownloadCommandTest extends TestCase\n{\n    private const PHAR = 'https://example.com/foo.phar';\n    private const BIN = '/usr/local/bin/foo';\n\n    private PharDownloadCommand $command;\n\n    protected function setUp(): void\n    {\n        $this->command = new PharDownloadCommand(self::PHAR, self::BIN);\n    }\n\n    public function test_it_is_a_command()\n    {\n        $this->assertInstanceOf(Command::class, $this->command);\n    }\n\n    public function test_it_generates_the_installation_command()\n    {\n        $this->assertMatchesRegularExpression(\\sprintf('#curl .*? %s -o %s#', self::PHAR, self::BIN), (string) $this->command);\n    }\n}\n"
  },
  {
    "path": "tests/Tool/Command/PhiveInstallCommandTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Tool\\Command;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\Toolbox\\Tool\\Command;\nuse Zalas\\Toolbox\\Tool\\Command\\PhiveInstallCommand;\n\nclass PhiveInstallCommandTest extends TestCase\n{\n    private const ALIAS = 'example/foo';\n    private const BIN = '/usr/local/bin/foo';\n    private const SIG = '0000000000000000';\n\n    private PhiveInstallCommand $command;\n\n    protected function setUp(): void\n    {\n        $this->command = new PhiveInstallCommand(self::ALIAS, self::BIN, self::SIG);\n    }\n\n    public function test_it_is_a_command()\n    {\n        $this->assertInstanceOf(Command::class, $this->command);\n    }\n\n    public function test_it_generates_the_installation_command()\n    {\n        $this->assertMatchesRegularExpression(\\sprintf('#phive --no-progress --home [^\\s]*? install --trust-gpg-keys %s %s -t [^\\s]++ && mv [^\\s]+? %s#', self::SIG, self::ALIAS, self::BIN), (string) $this->command);\n    }\n\n    public function test_it_accepts_unsigned_phar_command()\n    {\n        $command = new PhiveInstallCommand(self::ALIAS, self::BIN);\n        $this->assertMatchesRegularExpression(\\sprintf('#phive --no-progress --home [^\\s]*? install --force-accept-unsigned %s -t [^\\s]++ && mv [^\\s]+?#', self::ALIAS, self::BIN), (string) $command);\n    }\n}\n"
  },
  {
    "path": "tests/Tool/Command/ShCommandTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Tool\\Command;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\Toolbox\\Tool\\Command;\nuse Zalas\\Toolbox\\Tool\\Command\\ShCommand;\n\nclass ShCommandTest extends TestCase\n{\n    public function test_it_is_a_command()\n    {\n        $this->assertInstanceOf(Command::class, new ShCommand('echo'));\n    }\n\n    public function test_it_returns_the_command()\n    {\n        $this->assertSame('echo \"A\"', (string) new ShCommand('echo \"A\"'));\n    }\n}\n"
  },
  {
    "path": "tests/Tool/Command/TestCommandTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Tool\\Command;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\Toolbox\\Tool\\Command;\nuse Zalas\\Toolbox\\Tool\\Command\\TestCommand;\n\nclass TestCommandTest extends TestCase\n{\n    public function test_it_is_a_command()\n    {\n        $this->assertInstanceOf(Command::class, new TestCommand('/usr/bin/true', 'true'));\n    }\n\n    public function test_it_generates_the_command()\n    {\n        $this->assertMatchesRegularExpression(\n            '#\\(\\(/usr/bin/true > /dev/null && echo -e .*?✔\\.*?\\) || \\(echo -e .*?✘.*?\" && false\\)\\)#',\n            (string) new TestCommand('/usr/bin/true', 'true')\n        );\n    }\n}\n"
  },
  {
    "path": "tests/Tool/FilterTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Tool;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\Toolbox\\Tool\\Command\\ShCommand;\nuse Zalas\\Toolbox\\Tool\\Command\\TestCommand;\nuse Zalas\\Toolbox\\Tool\\Filter;\nuse Zalas\\Toolbox\\Tool\\Tool;\n\nclass FilterTest extends TestCase\n{\n    public function test_it_returns_true_if_no_excluded_tags_were_defined()\n    {\n        $filter = new Filter([], []);\n\n        $this->assertTrue($filter($this->tool(['phpspec', 'phpstan'])));\n    }\n\n    public function test_it_returns_true_if_no_excluded_tags_match()\n    {\n        $filter = new Filter(['exclude-php:7.3'], []);\n\n        $this->assertTrue($filter($this->tool(['phpspec', 'phpstan'])));\n    }\n\n    public function test_it_returns_true_if_tool_has_no_tags()\n    {\n        $filter = new Filter(['exclude-php:7.3'], []);\n\n        $this->assertTrue($filter($this->tool([])));\n    }\n\n    public function test_it_returns_true_if_neither_tool_nor_excluded_tags_were_defined()\n    {\n        $filter = new Filter([], []);\n\n        $this->assertTrue($filter($this->tool([])));\n    }\n\n    public function test_it_returns_false_if_one_excluded_tag_matches()\n    {\n        $filter = new Filter(['exclude-php:7.3'], []);\n\n        $this->assertFalse($filter($this->tool(['phpspec', 'phpstan', 'exclude-php:7.3'])));\n    }\n\n    public function test_it_returns_false_if_multiple_excluded_tags_match()\n    {\n        $filter = new Filter(['exclude-php:7.3', 'phpstan'], []);\n\n        $this->assertFalse($filter($this->tool(['phpspec', 'phpstan', 'exclude-php:7.3'])));\n    }\n\n    public function test_it_returns_false_if_all_excluded_tags_match()\n    {\n        $filter = new Filter(['exclude-php:7.3', 'phpspec', 'phpstan'], []);\n\n        $this->assertFalse($filter($this->tool(['phpspec', 'phpstan', 'exclude-php:7.3'])));\n    }\n\n    public function test_it_returns_true_if_a_tag_matches()\n    {\n        $filter = new Filter([], ['phpspec']);\n\n        $this->assertTrue($filter($this->tool(['phpspec', 'phpstan'])));\n    }\n\n    public function test_it_returns_true_if_all_tags_match_exactly()\n    {\n        $filter = new Filter([], ['phpspec', 'phpstan']);\n\n        $this->assertTrue($filter($this->tool(['phpspec', 'phpstan'])));\n    }\n\n    public function test_it_returns_true_if_all_tags_match()\n    {\n        $filter = new Filter([], ['phpspec', 'phpstan', 'foo', 'bar']);\n\n        $this->assertTrue($filter($this->tool(['phpspec', 'phpstan'])));\n    }\n\n    public function test_it_returns_false_if_the_tool_has_no_tags_to_match()\n    {\n        $filter = new Filter([], ['phpspec']);\n\n        $this->assertFalse($filter($this->tool(['phpstan'])));\n    }\n\n    public function test_it_returns_false_if_a_tag_is_both_included_and_excluded()\n    {\n        $filter = new Filter(['phpstan'], ['phpspec', 'phpstan']);\n\n        $this->assertFalse($filter($this->tool(['phpspec', 'phpstan'])));\n    }\n\n    private function tool(array $tags): Tool\n    {\n        return new Tool(\n            'any name',\n            'any summary',\n            'https://example.com',\n            $tags,\n            new ShCommand('any command'),\n            new TestCommand('any test command', 'any')\n        );\n    }\n}\n"
  },
  {
    "path": "tests/Tool/ToolTest.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Tool;\n\nuse PHPUnit\\Framework\\TestCase;\nuse TypeError;\nuse Zalas\\Toolbox\\Tool\\Command\\ShCommand;\nuse Zalas\\Toolbox\\Tool\\Tool;\n\nclass ToolTest extends TestCase\n{\n    public function test_it_exposes_its_properties()\n    {\n        $command = $this->anyCommand();\n        $testCommand = $this->anyCommand();\n\n        $tool = new Tool('phpstan', 'Static analysis tool', 'https://github.com/phpstan/phpstan', ['qa', 'static-analysis'], $command, $testCommand);\n\n        $this->assertSame('phpstan', $tool->name());\n        $this->assertSame('Static analysis tool', $tool->summary());\n        $this->assertSame('https://github.com/phpstan/phpstan', $tool->website());\n        $this->assertSame(['qa', 'static-analysis'], $tool->tags());\n        $this->assertSame($command, $tool->command());\n        $this->assertSame($testCommand, $tool->testCommand());\n    }\n\n    public function test_tags_can_only_be_strings()\n    {\n        $this->expectException(TypeError::class);\n        $command = $this->anyCommand();\n        $testCommand = $this->anyCommand();\n\n        new Tool('phpstan', 'Static analysis tool', 'https://github.com/phpstan/phpstan', [['qa'], ['static-analysis']], $command, $testCommand);\n    }\n\n    /**\n     * @return object\n     */\n    public function anyCommand(): object\n    {\n        return new ShCommand('any command');\n    }\n}\n"
  },
  {
    "path": "tests/UseCase/InstallToolsTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\UseCase;\n\nuse PHPUnit\\Framework\\MockObject\\Stub;\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\Toolbox\\Tool\\Collection;\nuse Zalas\\Toolbox\\Tool\\Command;\nuse Zalas\\Toolbox\\Tool\\Command\\BoxBuildCommand;\nuse Zalas\\Toolbox\\Tool\\Command\\ComposerBinPluginCommand;\nuse Zalas\\Toolbox\\Tool\\Command\\ComposerGlobalInstallCommand;\nuse Zalas\\Toolbox\\Tool\\Command\\ComposerInstallCommand;\nuse Zalas\\Toolbox\\Tool\\Command\\FileDownloadCommand;\nuse Zalas\\Toolbox\\Tool\\Command\\MultiStepCommand;\nuse Zalas\\Toolbox\\Tool\\Command\\PharDownloadCommand;\nuse Zalas\\Toolbox\\Tool\\Command\\PhiveInstallCommand;\nuse Zalas\\Toolbox\\Tool\\Command\\ShCommand;\nuse Zalas\\Toolbox\\Tool\\Command\\TestCommand;\nuse Zalas\\Toolbox\\Tool\\Filter;\nuse Zalas\\Toolbox\\Tool\\Tool;\nuse Zalas\\Toolbox\\Tool\\Tools;\nuse Zalas\\Toolbox\\UseCase\\InstallTools;\n\nclass InstallToolsTest extends TestCase\n{\n    private InstallTools $useCase;\n\n    private Tools|Stub $tools;\n\n    protected function setUp(): void\n    {\n        $this->tools = $this->createStub(Tools::class);\n        $this->useCase = new InstallTools($this->tools);\n    }\n\n    public function test_it_returns_a_multi_step_command()\n    {\n        $filter = $this->filter();\n\n        $this->givenToolsFor($filter, Collection::create([$this->tool(new ShCommand('echo \"Foo\"'))]));\n\n        $command = $this->useCase->__invoke($filter);\n\n        $this->assertInstanceOf(MultiStepCommand::class, $command);\n    }\n\n    public function test_it_groups_composer_global_install_commands()\n    {\n        $this->givenTools(Collection::create([\n            $this->tool(new ComposerGlobalInstallCommand('phpstan/phpstan')),\n            $this->tool(new ComposerGlobalInstallCommand('phan/phan')),\n        ]));\n\n        $command = $this->useCase->__invoke($this->filter());\n\n        $this->assertMatchesRegularExpression('#composer global require .* phpstan/phpstan phan/phan#', (string)$command);\n    }\n\n    public function test_it_does_not_include_empty_commands()\n    {\n        $this->givenTools(Collection::create([\n            $this->tool(new ShCommand('echo \"Foo\"')),\n        ]));\n\n        $command = $this->useCase->__invoke($this->filter());\n\n        $this->assertDoesNotMatchRegularExpression('#composer global require#', (string)$command, 'Composer commands are not grouped if there is none.');\n        $this->assertDoesNotMatchRegularExpression('#&&\\s*$#', (string)$command, 'Empty commands are not included.');\n    }\n\n    public function test_it_groups_composer_bin_plugin_commands()\n    {\n        $this->givenTools(Collection::create([\n            $this->tool(new ComposerBinPluginCommand('phpstan/phpstan', 'tools', Collection::create([]))),\n            $this->tool(new ComposerBinPluginCommand('phan/phan', 'tools', Collection::create([]))),\n        ]));\n\n        $command = $this->useCase->__invoke($this->filter());\n\n        $this->assertMatchesRegularExpression('#composer global bin tools require .* phpstan/phpstan phan/phan#', (string)$command);\n    }\n\n    public function test_it_includes_installation_tagged_commands_before_other_ones()\n    {\n        $this->givenTools(Collection::create([\n            $this->tool(new ShCommand('echo \"Foo\"')),\n            $this->tool(new ShCommand('echo \"Installation\"'), [InstallTools::PRE_INSTALLATION_TAG]),\n        ]));\n\n        $command = $this->useCase->__invoke($this->filter());\n\n        $this->assertMatchesRegularExpression('#echo \"Installation\".*echo \"Foo\"#smi', (string)$command, 'Installation commands are included before other ones.');\n        $this->assertDoesNotMatchRegularExpression('#echo \"Installation\".*echo \"Installation\"#smi', (string)$command, 'Installation commands are not duplicated.');\n    }\n\n    public function test_it_includes_shell_commands()\n    {\n        $this->givenTools(Collection::create([\n            $this->tool(new ShCommand('echo \"Foo\"')),\n        ]));\n\n        $command = $this->useCase->__invoke($this->filter());\n\n        $this->assertMatchesRegularExpression('#echo \"Foo\"#', (string)$command);\n    }\n\n    public function test_it_includes_multi_step_commands()\n    {\n        $this->givenTools(Collection::create([\n            $this->tool(new MultiStepCommand(Collection::create([\n                new ShCommand('echo \"Foo\"'),\n                new ShCommand('echo \"Bar\"')\n            ]))),\n        ]));\n\n        $command = $this->useCase->__invoke($this->filter());\n\n        $this->assertMatchesRegularExpression('#echo \"Foo\" && echo \"Bar\"#', (string)$command);\n    }\n\n    public function test_it_includes_composer_install_commands()\n    {\n        $this->givenTools(Collection::create([\n            $this->tool(new ComposerInstallCommand('git@github.com:phpspec/phpspec.git', '/usr/local/bin')),\n        ]));\n\n        $command = $this->useCase->__invoke($this->filter());\n\n        $this->assertMatchesRegularExpression('#git clone git@github.com:phpspec/phpspec.git#', (string)$command);\n    }\n\n    public function test_it_includes_box_build_commands()\n    {\n        $this->givenTools(Collection::create([\n            $this->tool(new BoxBuildCommand('https://github.com/behat/behat.git', 'behat.phar', '/tools/behat', '/tmp')),\n        ]));\n\n        $command = $this->useCase->__invoke($this->filter());\n\n        $this->assertMatchesRegularExpression('#box compile#', (string)$command);\n    }\n\n    public function test_it_includes_phar_download_commands()\n    {\n        $this->givenTools(Collection::create([\n            $this->tool(new PharDownloadCommand('https://github.com/sensiolabs-de/deptrac/releases/download/0.2.0/deptrac-0.2.0.phar', '/tools/phar')),\n        ]));\n\n        $command = $this->useCase->__invoke($this->filter());\n\n        $this->assertMatchesRegularExpression('#curl[^&]*?deptrac-0.2.0.phar#', (string)$command);\n    }\n\n    public function test_it_includes_phive_install_commands()\n    {\n        $this->givenTools(Collection::create([\n            $this->tool(new PhiveInstallCommand('phpunit', '/tools/phpunit')),\n        ]));\n\n        $command = $this->useCase->__invoke($this->filter());\n        $this->assertMatchesRegularExpression('#phive --no-progress --home /tools/.phive install[^&]*?phpunit[^&]*? [^\\s]++ && mv [^\\s]++ /tools/phpunit#', (string)$command);\n    }\n\n    public function test_it_includes_file_download_commands()\n    {\n        $this->givenTools(Collection::create([\n            $this->tool(new FileDownloadCommand('https://github.com/fabpot/local-php-security-checker/releases/download/v1.0.0/local-php-security-checker_1.0.0_linux_amd64', '/tools/security-checker')),\n        ]));\n\n        $command = $this->useCase->__invoke($this->filter());\n\n        $this->assertMatchesRegularExpression('#curl[^&]*?local-php-security-checker_1.0.0_linux_amd64#', (string)$command);\n    }\n\n    private function filter(): Filter\n    {\n        return new Filter([], []);\n    }\n\n    private function tool(Command $command, array $tags = []): Tool\n    {\n        return new Tool(\n            \"any name\",\n            \"any summary\",\n            \"https://example.com\",\n            $tags,\n            $command,\n            new TestCommand(\"any test command\", \"any test name\")\n        );\n    }\n\n    private function givenToolsFor(Filter $filter, Collection $tools): void\n    {\n        $this->tools->method('all')\n            ->with($filter)\n            ->willReturn($tools);\n    }\n\n    private function givenTools(Collection $tools): void\n    {\n        $this->tools->method('all')\n            ->willReturn($tools);\n    }\n}\n"
  },
  {
    "path": "tests/UseCase/ListToolsTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\UseCase;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\Toolbox\\Tool\\Collection;\nuse Zalas\\Toolbox\\Tool\\Command;\nuse Zalas\\Toolbox\\Tool\\Command\\TestCommand;\nuse Zalas\\Toolbox\\Tool\\Filter;\nuse Zalas\\Toolbox\\Tool\\Tool;\nuse Zalas\\Toolbox\\Tool\\Tools;\nuse Zalas\\Toolbox\\UseCase\\ListTools;\n\nclass ListToolsTest extends TestCase\n{\n    public function test_it_returns_loaded_tools()\n    {\n        $tools = Collection::create([$this->anyTool(), $this->anyTool()]);\n        $filter = $this->filter();\n\n        $repository = $this->givenToolsFor($filter, $tools);\n\n        $useCase = new ListTools($repository);\n\n        $this->assertSame($tools, $useCase($filter));\n    }\n\n    private function filter(): Filter\n    {\n        return new Filter([], []);\n    }\n\n    private function anyTool(): Tool\n    {\n        return new Tool(\n            \"any name\",\n            \"any summary\",\n            \"https://example.com\",\n            [],\n            new Command\\ShCommand(\"any command\"),\n            new TestCommand(\"any test command\", \"any test name\")\n        );\n    }\n\n    private function givenToolsFor(Filter $filter, Collection $tools): Tools\n    {\n        $repository = $this->createStub(Tools::class);\n        $repository->method('all')->with($filter)->willReturn($tools);\n\n        return $repository;\n    }\n}\n"
  },
  {
    "path": "tests/UseCase/TestToolsTest.php",
    "content": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\UseCase;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\Toolbox\\Tool\\Collection;\nuse Zalas\\Toolbox\\Tool\\Command;\nuse Zalas\\Toolbox\\Tool\\Command\\ShCommand;\nuse Zalas\\Toolbox\\Tool\\Filter;\nuse Zalas\\Toolbox\\Tool\\Tool;\nuse Zalas\\Toolbox\\Tool\\Tools;\nuse Zalas\\Toolbox\\UseCase\\TestTools;\n\nclass TestToolsTest extends TestCase\n{\n    public function test_it_returns_test_aggregated_test_command()\n    {\n        $testCommands = [\n            $this->command('echo \"a\"'),\n            $this->command('echo \"b\"'),\n        ];\n        $filter = $this->filter();\n        $tools = $this->tools($testCommands, $filter);\n\n        $useCase = new TestTools($tools);\n\n        $this->assertSame('echo \"a\" && echo \"b\"', (string) $useCase($filter));\n    }\n\n    private function tool(Command $testCommand): Tool\n    {\n        return new Tool(\n            \"any name\",\n            \"any summary\",\n            \"https://example.com\",\n            [],\n            new Command\\ShCommand(\"any command\"),\n            $testCommand\n        );\n    }\n\n    private function command(string $command): Command\n    {\n        return new ShCommand($command);\n    }\n\n    private function tools(array $testCommands, Filter $filter): Tools\n    {\n        $tools = $this->createStub(Tools::class);\n        $tools->method('all')->with($filter)->willReturn(Collection::create(\n            \\array_map(fn ($command) => $this->tool($command), $testCommands)\n        ));\n\n        return $tools;\n    }\n\n    private function filter(): Filter\n    {\n        return new Filter([], []);\n    }\n}\n"
  },
  {
    "path": "tests/resources/invalid-tools.json",
    "content": "{\"tools\": \"bar\"}"
  },
  {
    "path": "tests/resources/invalid.json",
    "content": "{\"tools\":}"
  },
  {
    "path": "tests/resources/no-tools.json",
    "content": "{\"foo\": \"bar\"}"
  },
  {
    "path": "tests/resources/pre-installation.json",
    "content": "{\n  \"tools\": [\n    {\n      \"name\": \"composer\",\n      \"summary\": \"Dependency Manager for PHP\",\n      \"website\": \"https://getcomposer.org/\",\n      \"command\": {\n        \"sh\": {\n          \"command\": \"composer self-update\"\n        }\n      },\n      \"test\": \"composer list\"\n    },\n    {\n      \"name\": \"composer-bin-plugin\",\n      \"summary\": \"Composer plugin to install bin vendors in isolated locations\",\n      \"website\": \"https://github.com/bamarni/composer-bin-plugin\",\n      \"command\": {\n        \"sh\": {\n          \"command\": \"composer global require bamarni/composer-bin-plugin\"\n        }\n      },\n      \"test\": \"composer global show bamarni/composer-bin-plugin\"\n    },\n    {\n      \"name\": \"box\",\n      \"summary\": \"An application for building and managing Phars\",\n      \"website\": \"https://box-project.github.io/box2/\",\n      \"command\": {\n        \"sh\": {\n          \"command\": \"curl -Ls https://box-project.github.io/box2/installer.php | php && mv box.phar /usr/local/bin/box && chmod +x /usr/local/bin/box\"\n        }\n      },\n      \"test\": \"box list\"\n    }\n  ]\n}"
  },
  {
    "path": "tests/resources/tools.json",
    "content": "{\n  \"tools\": [\n    {\n      \"name\": \"analyze\",\n      \"summary\": \"Visualizes metrics and source code\",\n      \"website\": \"https://github.com/Qafoo/QualityAnalyzer\",\n      \"command\": {\n        \"composer-install\": {\n          \"repository\": \"https://github.com/Qafoo/QualityAnalyzer.git\",\n          \"target-dir\": \"/usr/local/bin/QualityAnalyzer\"\n        }\n      },\n      \"test\": \"analyze list\",\n      \"tags\": [\"static-analysis\"]\n    },\n    {\n      \"name\": \"behat\",\n      \"summary\": \"Helps to test business expectations\",\n      \"website\": \"http://behat.org/\",\n      \"command\": {\n        \"box-build\": {\n          \"repository\": \"https://github.com/behat/behat.git\",\n          \"phar\": \"behat.phar\",\n          \"bin\": \"/usr/local/bin/behat\"\n        }\n      },\n      \"test\": \"behat --version\",\n      \"tags\": [\"testing\"]\n    },\n    {\n      \"name\": \"deptrac\",\n      \"summary\": \"Enforces dependency rules\",\n      \"website\": \"https://github.com/sensiolabs-de/deptrac\",\n      \"command\": {\n        \"phar-download\": {\n          \"phar\": \"http://get.sensiolabs.de/deptrac.phar\",\n          \"bin\": \"/usr/local/bin/deptrac\"\n        }\n      },\n      \"test\": \"deptrac list\",\n      \"tags\": [\"static-analysis\"]\n    },\n    {\n      \"name\": \"infection\",\n      \"summary\": \"AST based PHP Mutation Testing Framework\",\n      \"website\": \"https://infection.github.io/\",\n      \"command\": {\n        \"file-download\": {\n          \"url\": \"https://github.com/infection/infection/releases/download/0.10.0/infection.phar.asc\",\n          \"file\": \"/usr/local/bin/infection.phar.asc\"\n        },\n        \"phar-download\": {\n          \"phar\": \"https://github.com/infection/infection/releases/download/0.10.0/infection.phar\",\n          \"bin\": \"/usr/local/bin/infection\"\n        }\n      },\n      \"test\": \"infection --version\",\n      \"tags\": [\"testing\"]\n    },\n    {\n      \"name\": \"phpstan\",\n      \"summary\": \"Static Analysis Tool\",\n      \"website\": \"https://github.com/phpstan/phpstan\",\n      \"command\": {\n        \"composer-bin-plugin\": {\n          \"package\": \"phpstan/phpstan\",\n          \"namespace\": \"phpstan\"\n        }\n      },\n      \"test\": \"phpstan --version\",\n      \"tags\": [\"phpstan\"]\n    }\n  ]\n}"
  },
  {
    "path": "tools/.gitignore",
    "content": "*\n!.gitignore"
  }
]