Full Code of jakzal/toolbox for AI

master 2ecc0003c306 cached
131 files
235.8 KB
62.6k tokens
465 symbols
1 requests
Download .txt
Showing preview only (266K chars total). Download the full file or copy to clipboard to get everything.
Repository: jakzal/toolbox
Branch: master
Commit: 2ecc0003c306
Files: 131
Total size: 235.8 KB

Directory structure:
gitextract_t8u0t2r9/

├── .gitattributes
├── .github/
│   ├── FUNDING.yml
│   ├── pull_request_template.md
│   └── workflows/
│       ├── build.yml
│       ├── publish-website.yml
│       └── update-phars.yml
├── .gitignore
├── .php-cs-fixer.dist.php
├── .scrutinizer.yml
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── Makefile
├── README.md
├── bin/
│   ├── devkit.php
│   └── toolbox.php
├── box-devkit.json.dist
├── box.json.dist
├── composer.json
├── deptrac.yaml
├── infection.json.dist
├── phpunit.xml.dist
├── resources/
│   ├── architecture.json
│   ├── checkstyle.json
│   ├── compatibility.json
│   ├── composer.json
│   ├── deprecation.json
│   ├── documentation.json
│   ├── linting.json
│   ├── metrics.json
│   ├── phpcs.json
│   ├── phpstan.json
│   ├── pre-installation.json
│   ├── psalm.json
│   ├── refactoring.json
│   ├── security.json
│   ├── test.json
│   └── tools.json
├── scoper.inc.php
├── src/
│   ├── Cli/
│   │   ├── Application.php
│   │   ├── Command/
│   │   │   ├── DefaultTag.php
│   │   │   ├── DefaultTargetDir.php
│   │   │   ├── InstallCommand.php
│   │   │   ├── ListCommand.php
│   │   │   └── TestCommand.php
│   │   ├── Runner/
│   │   │   └── DryRunner.php
│   │   ├── ServiceContainer/
│   │   │   ├── LazyRunner.php
│   │   │   └── RunnerFactory.php
│   │   └── ServiceContainer.php
│   ├── Json/
│   │   ├── Factory/
│   │   │   ├── Assert.php
│   │   │   ├── BoxBuildCommandFactory.php
│   │   │   ├── ComposerBinPluginCommandFactory.php
│   │   │   ├── ComposerGlobalInstallCommandFactory.php
│   │   │   ├── ComposerInstallCommandFactory.php
│   │   │   ├── FileDownloadCommandFactory.php
│   │   │   ├── PharDownloadCommandFactory.php
│   │   │   ├── PhiveInstallCommandFactory.php
│   │   │   ├── ShCommandFactory.php
│   │   │   └── ToolFactory.php
│   │   └── JsonTools.php
│   ├── Runner/
│   │   ├── ParametrisedRunner.php
│   │   ├── PassthruRunner.php
│   │   └── Runner.php
│   ├── Tool/
│   │   ├── Collection.php
│   │   ├── Command/
│   │   │   ├── BoxBuildCommand.php
│   │   │   ├── ComposerBinPluginCommand.php
│   │   │   ├── ComposerBinPluginLinkCommand.php
│   │   │   ├── ComposerGlobalInstallCommand.php
│   │   │   ├── ComposerGlobalMultiInstallCommand.php
│   │   │   ├── ComposerInstallCommand.php
│   │   │   ├── FileDownloadCommand.php
│   │   │   ├── MultiStepCommand.php
│   │   │   ├── OptimisedComposerBinPluginCommand.php
│   │   │   ├── PharDownloadCommand.php
│   │   │   ├── PhiveInstallCommand.php
│   │   │   ├── ShCommand.php
│   │   │   └── TestCommand.php
│   │   ├── Command.php
│   │   ├── Filter.php
│   │   ├── Tool.php
│   │   └── Tools.php
│   └── UseCase/
│       ├── InstallTools.php
│       ├── ListTools.php
│       └── TestTools.php
├── tests/
│   ├── Cli/
│   │   ├── ApplicationTest.php
│   │   ├── Command/
│   │   │   ├── InstallCommandTest.php
│   │   │   ├── ListCommandTest.php
│   │   │   ├── TestCommandTest.php
│   │   │   └── ToolboxCommandTestCase.php
│   │   ├── Runner/
│   │   │   └── DryRunnerTest.php
│   │   ├── ServiceContainer/
│   │   │   ├── LazyRunnerTest.php
│   │   │   └── RunnerFactoryTest.php
│   │   └── ServiceContainerTest.php
│   ├── Json/
│   │   ├── Factory/
│   │   │   ├── AssertTest.php
│   │   │   ├── BoxBuildCommandFactoryTest.php
│   │   │   ├── ComposerBinPluginCommandFactoryTest.php
│   │   │   ├── ComposerGlobalInstallCommandFactoryTest.php
│   │   │   ├── ComposerInstallCommandFactoryTest.php
│   │   │   ├── FileDownloadCommandFactoryTest.php
│   │   │   ├── PharDownloadCommandFactoryTest.php
│   │   │   ├── PhiveInstallCommandFactoryTest.php
│   │   │   ├── ShCommandFactoryTest.php
│   │   │   └── ToolFactoryTest.php
│   │   └── JsonToolsTest.php
│   ├── Runner/
│   │   ├── ParametrisedRunnerTest.php
│   │   └── PassthruRunnerTest.php
│   ├── Tool/
│   │   ├── CollectionTest.php
│   │   ├── Command/
│   │   │   ├── BoxBuildCommandTest.php
│   │   │   ├── ComposerBinPluginCommandTest.php
│   │   │   ├── ComposerBinPluginLinkCommandTest.php
│   │   │   ├── ComposerGlobalInstallCommandTest.php
│   │   │   ├── ComposerGlobalMultiInstallCommandTest.php
│   │   │   ├── ComposerInstallCommandTest.php
│   │   │   ├── FileDownloadCommandTest.php
│   │   │   ├── MultiStepCommandTest.php
│   │   │   ├── OptimisedComposerBinPluginCommandTest.php
│   │   │   ├── PharDownloadCommandTest.php
│   │   │   ├── PhiveInstallCommandTest.php
│   │   │   ├── ShCommandTest.php
│   │   │   └── TestCommandTest.php
│   │   ├── FilterTest.php
│   │   └── ToolTest.php
│   ├── UseCase/
│   │   ├── InstallToolsTest.php
│   │   ├── ListToolsTest.php
│   │   └── TestToolsTest.php
│   └── resources/
│       ├── invalid-tools.json
│       ├── invalid.json
│       ├── no-tools.json
│       ├── pre-installation.json
│       └── tools.json
└── tools/
    └── .gitignore

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitattributes
================================================
/.gitattributes export-ignore
/.github export-ignore
/.gitignore export-ignore
/.php_cs export-ignore
/.travis.yml export-ignore
/CODE_OF_CONDUCT.md export-ignore
/CONTRIBUTING.md export-ignore
/Makefile export-ignore
/box.json.dist export-ignore
/composer.json export-ignore
/depfile.yml export-ignore
/infection.json.dist export-ignore
/phpunit.xml.dist export-ignore
/tests export-ignore
/tools export-ignore
/scoper.inc.php export-ignore


================================================
FILE: .github/FUNDING.yml
================================================
github: [jakzal]


================================================
FILE: .github/pull_request_template.md
================================================
<!--
Please read the CONTRIBUTING.md to learn about contributing to this project.

Please also note that this project is released with a Contributor Code of Conduct.
By participating in this project you agree to abide by its terms.
The Code of Conduct can be found in CODE_OF_CONDUCT.md.
-->



================================================
FILE: .github/workflows/build.yml
================================================
name: Build

on:
    push:
        branches: [master]
    pull_request:
    release:
        types: [created]
    schedule:
        -   cron: '0 4 * * *'

env:
    GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
    # for phive
    GITHUB_AUTH_TOKEN: ${{ secrets.ACCESS_TOKEN }}

jobs:
    tests:
        runs-on: ubuntu-latest
        name: Build and test
        strategy:
            matrix:
                php: [8.2, 8.3, 8.4, 8.5]
                deps: [high]
                include:
                    -   php: 8.2
                        deps: low

        steps:
            -   uses: actions/checkout@v4

            -   name: Setup PHP
                uses: shivammathur/setup-php@v2
                with:
                    php-version: "${{ matrix.php }}"
                    tools: composer
                    ini-values: "phar.readonly=0"
                    coverage: pcov

            -   if: matrix.deps == 'high'
                run: make update test package package-devkit

            -   if: matrix.deps == 'low'
                run: make update-min test-min

            -   uses: actions/upload-artifact@v4
                if: matrix.php == '8.2' && matrix.deps == 'high'
                with:
                    name: toolbox.phar
                    path: build/toolbox.phar

            -   uses: actions/upload-artifact@v4
                if: matrix.php == '8.2' && matrix.deps == 'high'
                with:
                    name: devkit.phar
                    path: build/devkit.phar

    integration-tests:
        runs-on: ubuntu-latest
        name: Run integration tests
        needs: tests
        strategy:
            matrix:
                php: [8.2, 8.3, 8.4, 8.5]

        steps:
            -   uses: actions/checkout@v4

            -   name: Setup PHP
                uses: shivammathur/setup-php@v2
                with:
                    php-version: "${{ matrix.php }}"
                    tools: composer
                    ini-values: "phar.readonly=0"
                    coverage: none
                    extensions: bz2, zip

            -   uses: actions/download-artifact@v4
                with:
                    name: toolbox.phar
                    path: build/
            -   run: make test-integration
                env:
                    GITHUB_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

    publish-phars:
        runs-on: ubuntu-latest
        name: Publish PHARs
        needs: tests
        if: github.event_name == 'release'
        steps:
            -   uses: actions/download-artifact@v4
                with:
                    name: toolbox.phar
                    path: .
            -   uses: actions/download-artifact@v4
                with:
                    name: devkit.phar
                    path: .
            -   name: Upload toolbox.phar
                run: gh release upload ${{ github.event.release.tag_name }} toolbox.phar --clobber --repo github.com/jakzal/toolbox
                env:
                    GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
            -   name: Upload devkit.phar
                run: gh release upload ${{ github.event.release.tag_name }} devkit.phar --clobber --repo github.com/jakzal/toolbox
                env:
                    GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}



================================================
FILE: .github/workflows/publish-website.yml
================================================
name: Publish the website

on:
    push:
        branches: [master]
    release:
        types: [created]

jobs:
    publish-website:
        runs-on: ubuntu-latest
        name: Build and publish
        steps:
            -   uses: actions/checkout@v4
                with:
                    persist-credentials: false

            -   name: Setup PHP
                uses: shivammathur/setup-php@v2
                with:
                    php-version: "8.2"
                    ini-values: "phar.readonly=0"

            -   name: Build the website
                run: make package-devkit website

            -   name: Publish the website
                uses: JamesIves/github-pages-deploy-action@v4
                with:
                    ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
                    BRANCH: gh-pages
                    FOLDER: build/website


================================================
FILE: .github/workflows/update-phars.yml
================================================
name: Update PHARs

on:
    schedule:
        -   cron: '30 3 * * *'
jobs:
    update-phars:
        runs-on: ubuntu-latest
        name: Create a PR
        steps:
            -   uses: actions/checkout@v4

            -   name: Setup PHP
                uses: shivammathur/setup-php@v2
                with:
                    php-version: "8.2"
                    ini-values: "phar.readonly=0"

            -   name: Configure git
                run: git config user.email 'jakub@zalas.pl' && git config user.name 'Jakub Zalas'

            -   name: Install dependencies
                run: sudo apt-get update && sudo apt-get install -y hub

            -   name: Update PHARs
                run: make package-devkit update-phars

            -   name: Send a Pull Request
                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'"
                env:
                    GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}


================================================
FILE: .gitignore
================================================
/composer.lock
/.deptrac.cache
/.phpunit.result.cache
/.php-cs-fixer.php
/.php-cs-fixer.cache
/build/
/vendor/


================================================
FILE: .php-cs-fixer.dist.php
================================================
<?php

$finder = PhpCsFixer\Finder::create()
    ->in(['src', 'tests'])
;

return (new PhpCsFixer\Config())
    ->setRules([
        '@PSR2' => true,
        'array_syntax' => ['syntax' => 'short'],
        'blank_line_before_statement' => true,
        'concat_space' => ['spacing' => 'none'],
        'declare_strict_types' => true,
        'native_function_invocation' => ['include' => ['@internal']],
        'no_empty_comment' => true,
        'no_empty_phpdoc' => true,
        'no_empty_statement' => true,
        'no_extra_blank_lines' => true,
        'no_leading_import_slash' => true,
        'no_leading_namespace_whitespace' => true,
        'no_unused_imports' => true,
        'no_useless_else' => true,
        'ordered_class_elements' => true,
        'ordered_imports' => true,
        'phpdoc_add_missing_param_annotation' => ['only_untyped' => true],
        'protected_to_private' => true,
        'strict_comparison' => true,
        'ternary_operator_spaces' => true,
        'ternary_to_null_coalescing' => true,
        'yoda_style' => true,
    ])
    ->setFinder($finder)
;


================================================
FILE: .scrutinizer.yml
================================================
inherit: true

build:
  environment:
    php:
      version: 8.2
    variables:
      XDEBUG_MODE: coverage
  tests:
    override:
    - make phpunit
  project_setup:
  nodes:
    coverage:
      tests:
        override:
        - command: make phpunit
          coverage:
            file: build/coverage.xml
            format: clover
filter:
  paths: [src/*]

build_failure_conditions:
- 'elements.rating(<= B).new.exists'
- 'issues.label("coding-style").new.exists'
- 'issues.severity(>= MAJOR).new.exists'

checks:
  php: true

tools:
  php_code_sniffer: false
  php_cs_fixer: { config: { level: psr2 } }
  external_code_coverage: false
  php_code_coverage: true
  php_changetracking: true
  php_sim: true
  php_mess_detector: true
  php_pdepend: true
  php_analyzer: true
  sensiolabs_security_checker: true

coding_style:
  php:
    spaces:
      within:
        brackets: false
      before_parentheses:
        closure_definition: true


================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct

## Our Pledge

In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and
orientation.

## Our Standards

Examples of behavior that contributes to creating a positive environment
include:

* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members

Examples of unacceptable behavior by participants include:

* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
  address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
  professional setting

## Our Responsibilities

Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.

Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.

## Scope

This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at oss@zalas.pl. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.

Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [http://contributor-covenant.org/version/1/4][version]

[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/


================================================
FILE: CONTRIBUTING.md
================================================
# Contributing

When contributing to this repository send a new pull request.
If your change is big or complex, or you simply want to suggest an improvement,
please discuss the change you wish to make via an issue.

Please note we have a [code of conduct](CODE_OF_CONDUCT.md). Please follow it in all your interactions with the project.

## Pull Request Process

* Provide good commit messages describing what you've done.
* Provide tests for any code you write.
* Make sure all tests are passing.
* Prefer `phive`, `phar` or `composer-bin-plugin` installation over `composer global` installations to avoid dependency conflicts.
* Update the `resources/*.json` files with any new tools'd like to add.
* Update `README.md` with any new tools you added (`php bin/devkit.php update:readme`).

## Adding a new tool

To add support for a new tool, add it to the list in one of the `json` files in the `resources/` folder:

```json
{
  "name": "behat",
  "summary": "Helps to test business expectations",
  "website": "http://behat.org/",
  "command": {
    "composer-bin-plugin": {
      "package": "behat/behat",
      "namespace": "behat"
    }
  },
  "test": "behat --version",
  "tags": ["testing", "test", "bdd"]
}
```

Each tool should have the following properties specified:

* `name` - name of the tool, most of the time the name of executable;
* `summary` - shortly stated purpose of the tool;
* `website` - link to the tool's website;
* `command` - the command to install the tool. See supported commands below;
* `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;

Once you added a new tool to the list, update the list in `README.md` by running the following command:

```bash
php bin/devkit.php update:readme
```

### Commands

There are several supported ways to install tools.
All of them are listed below in order of preference.

#### phive

Downloads a phar for the given alias using phive and puts it into the specified location.

```json
{
  "command": {
    "phive-install": {
      "alias": "dephpend",
      "bin": "%target-dir%/dephpend",
      "sig": "76835C9464877BDD"
    }
  }
}
```

`sig` is optional, but it is recommended if the phar is signed.

#### phar-download

Downloads a phar from the given URL and puts it into the specified location.

```json
{
  "command": {
    "phar-download": {
      "phar": "https://github.com/phpspec/phpspec/releases/download/4.3.0/phpspec.phar",
      "bin": "/usr/local/bin/phpspec"
    }
  }
}
```

#### file-download

Downloads a file from the given URL and puts it into the specified location.

```json
{
  "command": {
    "file-download": {
      "url": "https://github.com/vimeo/psalm/releases/download/2.0.0/psalm.phar.asc",
      "file": "/usr/local/bin/psalm.phar.asc"
    }
  }
}
```


#### composer-bin-plugin

The `composer-bin-plugin` method uses the [bamarni/composer-bin-plugin](https://github.com/bamarni/composer-bin-plugin)
to install the package in isolated directory.
Thanks to the isolation we're less likely to run into problem with conflicting dependencies between tools.

```json
{
  "command": {
    "composer-bin-plugin": {
      "package": "behat/behat",
      "namespace": "behat",
      "links": {"/tools/behat": "behat"}
    }
  }
}
```

The `links` attribute is optional, but it's recommended for packages that provide commands.

#### box-build

Uses [box](https://box-project.github.io/box2/) to build a phar and puts it into the specified location.
It will clone the given repository and checkout the latest tag if available.

```json
{
  "command": {
    "box-build": {
      "repository": "https://github.com/behat/behat.git",
      "phar": "behat.phar",
      "bin": "/usr/local/bin/behat"
    }
  }
}
```

#### composer-global-install

Uses the `composer global require` command to install a composer package globally.

```json
{
  "command": {
    "composer-global-install": {
      "package": "bmitch/churn-php"
    }
  }
}
```

#### composer-install

Clones the specified repository, checkouts the latest tag (if available), and runs `composer install` inside.
Mostly useful with applications.

```json
{
  "command": {
    "composer-install": {
      "repository": "https://github.com/Qafoo/QualityAnalyzer.git"
    }
  }
}
```

#### Executing multiple commands

It's sometimes useful to run multiple installation commands i.e. when downloading a phar and its signature.

```json
{
  "command": {
    "file-download": {
      "url": "https://github.com/vimeo/psalm/releases/download/2.0.0/psalm.phar.asc",
      "file": "/usr/local/bin/psalm.phar.asc"
    },
    "phar-download": {
      "phar": "https://github.com/vimeo/psalm/releases/download/2.0.0/psalm.phar",
      "bin": "/usr/local/bin/psalm"
    }
  }
}
```


================================================
FILE: LICENSE
================================================
Copyright (c) 2017 Jakub Zalas <jakub@zalas.pl>

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.


================================================
FILE: Makefile
================================================
default: build

PHP_VERSION:=$(shell php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;')
TOOLBOX_VERSION?=dev

build: install test
.PHONY: build

install:
	composer install
.PHONY: install

update:
	composer update
.PHONY: update

update-min:
	composer update --prefer-stable --prefer-lowest
.PHONY: update-min

update-no-dev:
	composer update --prefer-stable --no-dev
.PHONY: update-no-dev

test: vendor cs deptrac phpunit infection
.PHONY: test

test-min: update-min cs deptrac phpunit infection
.PHONY: test-min

test-integration: build/toolbox.phar
	rm -rf ./build/tools && \
	  export PATH="$(shell pwd)/build/tools:$(shell pwd)/build/tools/.composer/vendor/bin:$(shell pwd)/build/tools/QualityAnalyzer/bin:$$PATH" && \
	  export COMPOSER_HOME=$(shell pwd)/build/tools/.composer && \
	  chmod +x build/toolbox.phar && \
	  mkdir -p ./build/tools && \
	  build/toolbox.phar install --target-dir ./build/tools --exclude-tag exclude-php:$(PHP_VERSION) && \
	  build/toolbox.phar test --target-dir ./build/tools --exclude-tag exclude-php:$(PHP_VERSION)
.PHONY: test-integration

cs: tools/php-cs-fixer
	PHP_CS_FIXER_IGNORE_ENV=true tools/php-cs-fixer --dry-run --allow-risky=yes --no-interaction --ansi fix

cs-fix: tools/php-cs-fixer
	PHP_CS_FIXER_IGNORE_ENV=true tools/php-cs-fixer --allow-risky=yes --no-interaction --ansi fix

deptrac: tools/deptrac
	tools/deptrac --no-interaction --ansi
.PHONY: deptrac

infection:
	./vendor/bin/infection --no-interaction --formatter=progress --min-msi=100 --min-covered-msi=100 --ansi
.PHONY: infection

phpunit: tools/phpunit
	tools/phpunit
.PHONY: phpunit

package: tools/box
	@rm -rf build/phar && mkdir -p build/phar build/phar/bin

	cp -r resources src LICENSE composer.json scoper.inc.php build/phar
	sed -e 's/Application('"'"'dev/Application('"'"'$(TOOLBOX_VERSION)/g' bin/toolbox.php > build/phar/bin/toolbox.php

	cd build/phar && \
	  composer config platform.php 8.2.0 && \
	  composer update --no-dev -o -a

	tools/box compile

	@rm -rf build/phar
.PHONY: package

package-devkit: tools/box
	@rm -rf build/devkit-phar && mkdir -p build/devkit-phar build/devkit-phar/bin build/devkit-phar/src

	cp -r resources LICENSE composer.json scoper.inc.php build/devkit-phar
	cp -r src/Json src/Runner src/Tool build/devkit-phar/src
	sed -e 's/\(Application(.*\)'"'"'dev/\1'"'"'$(TOOLBOX_VERSION)/g' bin/devkit.php > build/devkit-phar/bin/devkit.php

	cd build/devkit-phar && \
	  composer config platform.php 8.2.0 && \
	  composer update --no-dev -o -a

	tools/box compile -c box-devkit.json.dist

	@rm -rf build/devkit-phar
.PHONY: package-devkit

website: build/devkit.phar
	rm -rf build/website
	mkdir -p build/website
	php build/devkit.phar generate:html > build/website/index.html
	touch build/website/.nojekyll
.PHONY: website

publish-website: website
	cd build/website && \
	  git init . && \
	  git add . && \
	  git commit -m "Build the website" && \
	  git push --force --quiet "https://github.com/jakzal/toolbox.git" master:gh-pages
.PHONY: publish-website

update-phars: vendor
	php bin/devkit.php update:phars
	git diff --exit-code resources/ || \
	 	  ( \
	 	    git checkout -b tools-update && \
	 	    git add resources/*.json && \
	 	    git commit -m "Update tools" && \
	 	    git push origin tools-update \
	 	  )
.PHONY: update-phars

tools: tools/php-cs-fixer tools/deptrac tools/box
.PHONY: tools

clean:
	rm -rf build
	rm -rf vendor
	find tools -not -path '*/\.*' -type f -delete
.PHONY: clean

vendor: install

vendor/bin/phpunit: install

tools/phpunit: vendor/bin/phpunit
	ln -sf ../vendor/bin/phpunit tools/phpunit

tools/php-cs-fixer:
	curl -Ls https://cs.symfony.com/download/php-cs-fixer-v3.phar -o tools/php-cs-fixer && chmod +x tools/php-cs-fixer

tools/deptrac:
	ln -sf ../vendor/bin/deptrac tools/deptrac

tools/box:
	curl -Ls https://github.com/humbug/box/releases/download/4.2.0/box.phar -o tools/box && chmod +x tools/box


================================================
FILE: README.md
================================================
# Toolbox

[![Build Status](https://github.com/jakzal/toolbox/workflows/Build/badge.svg)](https://github.com/jakzal/toolbox/actions)
[![Build Status](https://scrutinizer-ci.com/g/jakzal/toolbox/badges/build.png?b=master)](https://scrutinizer-ci.com/g/jakzal/toolbox/build-status/master)

Helps to discover and install tools.

## Use cases

Toolbox [started its life](https://github.com/jakzal/phpqa/blob/49482ae447d4b6341cf77aac9d51390fe1176e8c/tools.php)
as a simple script in the [phpqa docker image](https://github.com/jakzal/phpqa).
Its purpose was to install set of tools while building the docker image and it's still its main goal.
It has been extracted as a separate project to make maintenance easier and enable new use cases.

## Available tools

| Name | Description | PHP 8.2 | PHP 8.3 | PHP 8.4 | PHP 8.5 |
| :--- | :---------- | :------ | :------ | :------ | :------ |
| behat | [Helps to test business expectations](http://behat.org/) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| box | [Fast, zero config application bundler with PHARs](https://github.com/humbug/box) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| churn | [Discovers good candidates for refactoring](https://github.com/bmitch/churn-php) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| codeception | [Codeception is a BDD-styled PHP testing framework](https://codeception.com/) | &#x2705; | &#x2705; | &#x2705; | &#x274C; |
| composer | [Dependency Manager for PHP](https://getcomposer.org/) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| composer-bin-plugin | [Composer plugin to install bin vendors in isolated locations](https://github.com/bamarni/composer-bin-plugin) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| 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; |
| composer-normalize | [Composer plugin to normalize composer.json files](https://github.com/ergebnis/composer-normalize) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| 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; |
| 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; |
| composer-unused | [Show unused packages by scanning your code](https://github.com/icanhazstring/composer-unused) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| 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; |
| dephpend | [Detect flaws in your architecture](https://dephpend.com/) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| deprecation-detector | [Finds usages of deprecated code](https://github.com/sensiolabs-de/deprecation-detector) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| deptrac | [Enforces dependency rules between software layers](https://github.com/deptrac/deptrac) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| diffFilter | [Applies QA tools to run on a single pull request](https://github.com/exussum12/coverageChecker) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| ecs | [Sets up and runs coding standard checks](https://github.com/Symplify/EasyCodingStandard) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| gherkin-lint-php | [Gherkin linter for PHP](https://github.com/dantleech/gherkin-lint-php) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| infection | [AST based PHP Mutation Testing Framework](https://infection.github.io/) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| jack | [Helps to upgrade outdated Composer dependencies incrementally](https://github.com/rectorphp/jack) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| kahlan | [Kahlan is a full-featured Unit & BDD test framework a la RSpec/JSpec](https://kahlan.github.io/docs/) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| larastan | [PHPStan extension for Laravel](https://github.com/nunomaduro/larastan) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| lines | [CLI tool for quick metrics of PHP projects](https://github.com/tomasVotruba/lines) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| local-php-security-checker | [Checks composer dependencies for known security vulnerabilities](https://github.com/fabpot/local-php-security-checker) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| parallel-lint | [Checks PHP file syntax](https://github.com/php-parallel-lint/PHP-Parallel-Lint) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| paratest | [Parallel testing for PHPUnit](https://github.com/paratestphp/paratest) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| pdepend | [Static Analysis Tool](https://pdepend.org/) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| phan | [Static Analysis Tool](https://github.com/phan/phan) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| phive | [PHAR Installation and Verification Environment](https://phar.io/) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| php-cs-fixer | [PHP Coding Standards Fixer](http://cs.symfony.com/) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| 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; |
| php-semver-checker | [Suggests a next version according to semantic versioning](https://github.com/tomzx/php-semver-checker) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| phpa | [Checks for weak assumptions](https://github.com/rskuipers/php-assumptions) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| phparkitect | [Helps to put architectural constraints in a PHP code base](https://github.com/phparkitect/arkitect) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| phpat | [Easy to use architecture testing tool](https://github.com/carlosas/phpat) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| phpbench | [PHP Benchmarking framework](https://github.com/phpbench/phpbench) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| phpca | [Finds usage of non-built-in extensions](https://github.com/wapmorgan/PhpCodeAnalyzer) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| phpcb | [PHP Code Browser](https://github.com/mayflower/PHP_CodeBrowser) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| phpcbf | [Automatically corrects coding standard violations](https://github.com/PHPCSStandards/PHP_CodeSniffer) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| phpcodesniffer-composer-install | [Easy installation of PHP_CodeSniffer coding standards (rulesets).](https://github.com/PHPCSStandards/composer-installer) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| phpcov | [a command-line frontend for the PHP_CodeCoverage library](https://github.com/sebastianbergmann/phpcov) | &#x274C; | &#x274C; | &#x2705; | &#x2705; |
| phpcpd | [Copy/Paste Detector](https://github.com/sebastianbergmann/phpcpd) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| phpcs | [Detects coding standard violations](https://github.com/PHPCSStandards/PHP_CodeSniffer) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| phpcs-security-audit | [Finds vulnerabilities and weaknesses related to security in PHP code](https://github.com/FloeDesignTechnologies/phpcs-security-audit) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| phpdd | [Finds usage of deprecated features](http://wapmorgan.github.io/PhpDeprecationDetector) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| phpDocumentor | [Documentation generator](https://www.phpdoc.org/) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| phpinsights | [Analyses code quality, style, architecture and complexity](https://phpinsights.com/) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| phplint | [Lints php files in parallel](https://github.com/overtrue/phplint) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| phploc | [A tool for quickly measuring the size of a PHP project](https://github.com/sebastianbergmann/phploc) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| phpmd | [A tool for finding problems in PHP code](https://phpmd.org/) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| phpmetrics | [Static Analysis Tool](http://www.phpmetrics.org/) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| phpmnd | [Helps to detect magic numbers](https://github.com/povils/phpmnd) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| phpspec | [SpecBDD Framework](http://www.phpspec.net/) | &#x2705; | &#x2705; | &#x2705; | &#x274C; |
| phpstan | [Static Analysis Tool](https://github.com/phpstan/phpstan) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| 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; |
| phpstan-beberlei-assert | [PHPStan extension for beberlei/assert](https://github.com/phpstan/phpstan-beberlei-assert) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| phpstan-deprecation-rules | [PHPStan rules for detecting deprecated code](https://github.com/phpstan/phpstan-deprecation-rules) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| phpstan-doctrine | [Doctrine extensions for PHPStan](https://github.com/phpstan/phpstan-doctrine) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| phpstan-ergebnis-rules | [Additional rules for PHPstan](https://github.com/ergebnis/phpstan-rules) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| phpstan-larastan | [Separate installation of phpstan for larastan](https://github.com/phpstan/phpstan) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| phpstan-phpunit | [PHPUnit extensions and rules for PHPStan](https://github.com/phpstan/phpstan-phpunit) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| phpstan-strict-rules | [Extra strict and opinionated rules for PHPStan](https://github.com/phpstan/phpstan-strict-rules) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| phpstan-symfony | [Symfony extension for PHPStan](https://github.com/phpstan/phpstan-symfony) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| phpstan-webmozart-assert | [PHPStan extension for webmozart/assert](https://github.com/phpstan/phpstan-webmozart-assert) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| phpunit | [The PHP testing framework](https://phpunit.de/) | &#x274C; | &#x274C; | &#x2705; | &#x2705; |
| phpunit-10 | [The PHP testing framework (10.x version)](https://phpunit.de/) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| phpunit-11 | [The PHP testing framework (11.x version)](https://phpunit.de/) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| phpunit-12 | [The PHP testing framework (12.x version)](https://phpunit.de/) | &#x274C; | &#x2705; | &#x2705; | &#x2705; |
| phpunit-8 | [The PHP testing framework (8.x version)](https://phpunit.de/) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| phpunit-9 | [The PHP testing framework (9.x version)](https://phpunit.de/) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| pint | [Opinionated PHP code style fixer for Laravel](https://github.com/laravel/pint) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| psalm | [Finds errors in PHP applications](https://psalm.dev/) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| psalm-plugin-doctrine | [Stubs to let Psalm understand Doctrine better](https://github.com/weirdan/doctrine-psalm-plugin) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| psalm-plugin-phpunit | [Psalm plugin for PHPUnit](https://github.com/psalm/psalm-plugin-phpunit) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| psalm-plugin-symfony | [Psalm Plugin for Symfony](https://github.com/psalm/psalm-plugin-symfony) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| psecio-parse | [Scans code for potential security-related issues](https://github.com/psecio/parse) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| rector | [Tool for instant code upgrades and refactoring](https://github.com/rectorphp/rector) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| 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; |
| 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; |
| twig-cs-fixer | [Automatically corrects twig files following the official coding standard rules](https://github.com/VincentLanglet/Twig-CS-Fixer) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| twig-lint | [Standalone cli twig 1.X linter](https://github.com/asm89/twig-lint) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| twig-linter | [Standalone cli twig 3.X linter](https://github.com/sserbin/twig-linter) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| twigcs | [The missing checkstyle for twig!](https://github.com/friendsoftwig/twigcs) | &#x2705; | &#x2705; | &#x2705; | &#x274C; |
| yaml-lint | [Compact command line utility for checking YAML file syntax](https://github.com/j13k/yaml-lint) | &#x2705; | &#x2705; | &#x2705; | &#x2705; |

### Removed tools

| Name | Summary |  
| :--- | :------ |
| analyze | [Visualizes metrics and source code](https://github.com/Qafoo/QualityAnalyzer) |
| box-legacy | [Legacy version of box](https://box-project.github.io/box2/) |
| design-pattern | [Detects design patterns](https://github.com/Halleck45/DesignPatternDetector) |
| parallel-lint | [Checks PHP file syntax](https://github.com/JakubOnderka/PHP-Parallel-Lint) |
| pest | [The elegant PHP Testing Framework](https://github.com/pestphp/pest) |
| php-coupling-detector | [Detects code coupling issues](https://akeneo.github.io/php-coupling-detector/) |
| php-formatter | [Custom coding standards fixer](https://github.com/mmoreram/php-formatter) |
| phpcf | [Finds usage of deprecated features](http://wapmorgan.github.io/PhpCodeFixer/) |
| phpda | [Generates dependency graphs](https://mamuz.github.io/PhpDependencyAnalysis/) |
| phpdoc-to-typehint | [Automatically adds type hints and return types based on PHPDocs](https://github.com/dunglas/phpdoc-to-typehint) |
| phpstan-exception-rules | [PHPStan rules for checked and unchecked exceptions](https://github.com/pepakriz/phpstan-exception-rules) |
| phpstan-localheinz-rules | [Additional rules for PHPstan](https://github.com/localheinz/phpstan-rules) |
| phpunit-5 | [The PHP testing framework (5.x version)](https://phpunit.de/) |
| phpunit-7 | [The PHP testing framework (7.x version)](https://phpunit.de/) |
| security-checker | [Checks composer dependencies for known security vulnerabilities](https://github.com/sensiolabs/security-checker) |
| testability | [Analyses and reports testability issues of a php codebase](https://github.com/edsonmedina/php_testability) |

## Installation

Get the `toolbox.phar` from the [latest release](https://github.com/jakzal/toolbox/releases/latest).
The command below should do the job:

```bash
curl -Ls https://github.com/jakzal/toolbox/releases/latest/download/toolbox.phar -o toolbox && chmod +x toolbox
```

## Usage

### List available tools

```
./toolbox list-tools
```

#### Filter tools by tags

To exclude some tools from the listing multiple `--exclude-tag` options can be added.
The `--tag` option can be used to filter tools by tags.

```
./toolbox list-tools --exclude-tag exclude-php:8.2 --exclude-tag foo --tag bar
```

### Install tools

```
./toolbox install
```

#### Install tools in a custom directory

By default tools are installed in the `/usr/local/bin` directory. To perform an installation in another location,
pass the `--target-dir` option to the `install` command. Also, to change the location composer packages are installed in,
export the `COMPOSER_HOME` environment variable.

```
mkdir /tools
export COMPOSER_HOME=/tools/.composer
export PATH="/tools:$COMPOSER_HOME/vendor/bin:$PATH"
./toolbox install --target-dir /tools
```

The target dir can also be configured with the `TOOLBOX_TARGET_DIR` environment variable.

#### Dry run

To only see what commands would be executed, use the dry run mode:

```
./toolbox install --dry-run
```

#### Filter tools by tags

To exclude some tools from the installation multiple `--exclude-tag` options can be added.
The `--tag` option can be used to filter tools by tags.

```
./toolbox install --exclude-tag exclude-php:8.2 --exclude-tag foo --tag bar
```

### Test if installed tools are usable

```
./toolbox test
```

#### Dry run

To only see what commands would be executed, use the dry run mode:

```
./toolbox test --dry-run
```

#### Filter tools by tags

To exclude some tools from the generated test command multiple `--exclude-tag` options can be added.
The `--tag` option can be used to filter tools by tags.

```
./toolbox test --exclude-tag exclude-php:8.2 --exclude-tag foo --tag bar
```

### Tools definitions

By default the following files are used to load tool definitions:

* `resources/pre-installation.json`
* `resources/architecture.json`
* `resources/checkstyle.json`
* `resources/compatibility.json`
* `resources/composer.json`
* `resources/deprecation.json`
* `resources/documentation.json`
* `resources/linting.json`
* `resources/metrics.json`
* `resources/phpstan.json`
* `resources/psalm.json`
* `resources/refactoring.json`
* `resources/security.json`
* `resources/test.json`
* `resources/tools.json`

Definitions can be loaded from customised files by passing the `--tools` option(s):

```
./toolbox list-tools --tools path/to/file1.json --tools path/to/file2.json
```

Tool definition location(s) can be also specified with the `TOOLBOX_JSON` environment variable:

```
TOOLBOX_JSON='path/to/file1.json,path/to/file2.json' ./toolbox list-tools
```

### Tool tags

Tools can be tagged in order to enable grouping and filtering them.

The tags below have a special meaning:

* `pre-installation` - these tools will be installed before any other tools.
* `exclude-php:8.2`, `exclude-php:8.3` etc - used to exclude installation on the specified php version.

## Contributing

Please read the [Contributing guide](CONTRIBUTING.md) to learn about contributing to this project.
Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md).
By participating in this project you agree to abide by its terms.


================================================
FILE: bin/devkit.php
================================================
#!/usr/bin/env php
<?php declare(strict_types=1);

require __DIR__ . '/../vendor/autoload.php';

use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command as CliCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Zalas\Toolbox\Runner\PassthruRunner;
use Zalas\Toolbox\Json\JsonTools;
use Zalas\Toolbox\Tool\Collection;
use Zalas\Toolbox\Tool\Command;
use Zalas\Toolbox\Tool\Command\ShCommand;
use Zalas\Toolbox\Tool\Filter;
use Zalas\Toolbox\Tool\Tool;

trait Tools
{
    private function toolsJsonDefault(): array
    {
        return \getenv('TOOLBOX_JSON')
            ? \array_map('trim', \explode(',', \getenv('TOOLBOX_JSON')))
            : [
                __DIR__ . '/../resources/pre-installation.json',
                __DIR__ . '/../resources/architecture.json',
                __DIR__ . '/../resources/checkstyle.json',
                __DIR__ . '/../resources/compatibility.json',
                __DIR__ . '/../resources/composer.json',
                __DIR__ . '/../resources/deprecation.json',
                __DIR__ . '/../resources/documentation.json',
                __DIR__ . '/../resources/linting.json',
                __DIR__ . '/../resources/metrics.json',
                __DIR__ . '/../resources/phpcs.json',
                __DIR__ . '/../resources/phpstan.json',
                __DIR__ . '/../resources/psalm.json',
                __DIR__ . '/../resources/refactoring.json',
                __DIR__ . '/../resources/security.json',
                __DIR__ . '/../resources/test.json',
                __DIR__ . '/../resources/tools.json'
            ];
    }

    private function loadTools($jsonPath, ?Filter $filter = null): Collection
    {
        return (new JsonTools(function () use ($jsonPath) {
            return $jsonPath;
        }))->all($filter ?? new Filter([], []));
    }
}

$application = new Application('Toolbox DevKit', 'dev');
$application->add(
    new class extends CliCommand
    {
        use Tools;

        protected function configure(): void
        {
            $this->setName('update:readme');
            $this->setDescription('Updates README.md with latest list of available tools');
            $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());
            $this->addOption('readme', null, InputOption::VALUE_REQUIRED, 'Path to the readme file', __DIR__ . '/../README.md');
        }

        protected function execute(InputInterface $input, OutputInterface $output): int
        {
            $jsonPath = $input->getOption('tools');
            $readmePath = $input->getOption('readme');
            $tools = $this->loadTools($jsonPath);

            $versions = ['8.2', '8.3', '8.4', '8.5'];

            $toolsList = '| Name | Description | '. implode(' ', array_map(fn($v) => sprintf('PHP %s |', $v), $versions))  . PHP_EOL;
            $toolsList .= '| :--- | :---------- | '. implode(' ', array_fill(0, count($versions), ':------ |')) . PHP_EOL;
            $toolsList .= $tools->sort(function (Tool $left, Tool $right) {
                return strcasecmp($left->name(), $right->name());
            })->reduce('', function ($acc, Tool $tool) use ($versions) {

                $args = [
                    $tool->name(),
                    $tool->summary(),
                    $tool->website(),
                ];
                foreach ($versions as $version) {
                    $args[] = in_array(sprintf('exclude-php:%s', $version), $tool->tags(), true) ? '&#x274C;' : '&#x2705;';
                }

                return $acc . vsprintf('| %s | [%s](%s) | '. implode(' ', array_fill(0, count($versions), '%s |')), $args) . PHP_EOL;
            });

            $readme = file_get_contents($readmePath);
            $readme = preg_replace('/(## Available tools\n\n).*?(\n#+ )/smi', '$1' . $toolsList . '$2', $readme);

            file_put_contents($readmePath, $readme);

            $output->writeln(sprintf('The <info>%s</info> was updated with latest tools found in <info>%s</info>.', $readmePath, implode(', ', $jsonPath)));

            return 0;
        }
    }
);
$application->add(
    new class extends CliCommand
    {
        use Tools;

        protected function configure(): void
        {
            $this->setName('update:phars');
            $this->setDescription('Attempts to update phar links to latest versions');
            $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());
        }

        protected function execute(InputInterface $input, OutputInterface $output): int
        {
            foreach ($input->getOption('tools') as $jsonPath) {
                $result = $this->updatePhars($jsonPath, $output);

                if ($result !== 0) {
                    return $result;
                }
            }

            return 0;
        }

        private function updatePhars(string $jsonPath, OutputInterface $output): int
        {
            $phars = $this->findLatestPhars($jsonPath);

            if (empty($phars)) {
                return 0;
            }

            $output->writeln('Found phars:');

            foreach ($phars as $phar) {
                $output->writeln(sprintf('* %s', $phar));
            }

            $output->writeln(sprintf('Updated <info>%s</info>.', $jsonPath));

            return (new PassthruRunner())->run($this->updatePharsCommand($jsonPath, $phars));
        }

        private function findLatestPharsCommand(string $jsonPath): Command
        {
            $command = <<<'CMD'
            grep -e 'github\.com.*releases.*\.phar"' %TOOLBOX_JSON% |
            grep -v -e '/latest/' |
            sed -e 's@.*github.com/\(.*\)/releases.*@\1@' |
            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" |
            sed -e 's/^[^:]*: "\([^"]*\)"/\1/'
CMD;
            $command = strtr($command, ['%TOOLBOX_JSON%' => $jsonPath]);

            return new ShCommand($command);
        }

        private function findLatestPhars(string $jsonPath): array
        {
            $phars = [];

            exec((string)$this->findLatestPharsCommand($jsonPath), $phars);

            return $phars;
        }

        private function updatePharsCommand(string $jsonPath, array $phars): Command
        {
            $replacements = implode(' ', array_map(
                function (string $phar) {
                    $project = preg_replace('@https://[^/]*/([^/]*/[^/]*).*@', '$1', $phar);

                    return strtr(
                        '-e "s@\"phar\": \"([^\"]*%PROJECT%[^\"]*)\"@\"phar\": \"%PHAR%\"@g"' .
                        ' ' .
                        '-e "s@\"url\": \"([^\"]*%PROJECT%[^\"]*\.phar(\.asc|\.pubkey))\"@\"url\": \"%PHAR%\\2\"@g"',
                        ['%PROJECT%' => $project, '%PHAR%' => $phar]
                    );
                },
                $phars
            ));

            return new ShCommand(sprintf('sed -i.bak -E %s %s', $replacements, $jsonPath));
        }
    }
);
$application->add(
    new class extends CliCommand
    {
        use Tools;

        protected function configure(): void
        {
            $this->setName('generate:html');
            $this->setDescription('Generates an html list of available tools');
            $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());
        }

        protected function execute(InputInterface $input, OutputInterface $output): int
        {
            $tools = $this->loadTools($input->getOption('tools'), new Filter(['pre-installation'], []));

            $output->writeln($this->renderPage($tools->map($this->toolToHtml())));

            return 0;
        }

        private function toolToHtml(): \Closure
        {
            $tagTemplate = '<li class="list-inline-item"><span class="badge badge-primary">%TAG%</span></li>';
            $toolTemplate = <<<'TEMPLATE'
<div class="card m-3">
    <div class="card-body">
        <h5 class="card-title">%NAME%</h5>
        <p class="card-text tool-summary">%SUMMARY%</p>
        <a href="%WEBSITE%" class="card-link" title="%NAME%">%WEBSITE_NAME%</a>
    </div>
    <div class="card-footer">
        <ul class="list-inline mb-1">
            %TAGS%
        </ul>
    </div>
</div>
TEMPLATE;

            return function (Tool $tool) use ($toolTemplate, $tagTemplate) {
                return strtr(
                    $toolTemplate,
                    [
                        '%NAME%' => $tool->name(),
                        '%SUMMARY%' => $tool->summary(),
                        '%WEBSITE%' => $tool->website(),
                        '%WEBSITE_NAME%' => preg_replace('#^(https?://(github.com/)?)(.*?)/?$#', '$3', $tool->website()),
                        '%TAGS%' => \implode(\array_map(function (string $tag) use ($tagTemplate) {
                            return strtr($tagTemplate, ['%TAG%' => $tag]);
                        }, $tool->tags()))
                    ]);
            };
        }

        private function renderPage(Collection $toolsHtml): string
        {
            $template = <<<'TEMPLATE'
<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css"
          integrity="sha384-GJzZqFGwb1QTTN6wy59ffF1BuGJpLSa9DkKMp0DgiMDm4iYMj70gZWKYbI706tWS" crossorigin="anonymous">
    <title>Quality Assurance Tools for PHP | Toolbox | PHPQA</title>
    <style>
        .tool-summary {
            font-size: 0.8em;
            height: 2em;
        }
        .card-link {
            font-size: 0.7em;
        }
    </style>
</style>
</head>
<body>
<div class="container-fluid p-5">

    <div class="jumbotron">
        <h1 class="display-4">Quality Assurance Tools for PHP</h1>
        <p class="lead">
          The below list of tools is provided by the <a href="https://hub.docker.com/r/jakzal/phpqa/">phpqa</a> docker image.
          <a href="https://github.com/jakzal/toolbox">Toolbox</a> is used to install them in the image.
        </p>
        <hr class="my-4">
        <a class="btn btn-primary btn-lg" href="https://github.com/jakzal/toolbox" role="button">toolbox repository</a>
        <a class="btn btn-primary btn-lg" href="https://hub.docker.com/r/jakzal/phpqa/" role="button">phpqa docker image</a>
        <a class="btn btn-primary btn-lg" href="https://github.com/jakzal/phpqa" role="button">phpqa repository</a>
    </div>

    %TOOLS%

    <hr class="my-4"/>

    <div class="text-center">Generated on %GENERATED_ON%.</div>
</body>
</html>
TEMPLATE;

            return strtr($template, [
                '%TOOLS%' => \implode(PHP_EOL, \array_map(
                    function ($htmls) {
                        return PHP_EOL . '<div class="card-deck">' . implode($htmls) . '</div>' . PHP_EOL;
                    },
                    \array_chunk($toolsHtml->toArray(), 4)
                )),
                '%GENERATED_ON%' => (new \DateTime('now', new \DateTimeZone('UTC')))->format('r'),
            ]);
        }
    }
);
$application->run();


================================================
FILE: bin/toolbox.php
================================================
#!/usr/bin/env php
<?php

require __DIR__ . '/../vendor/autoload.php';

(new Zalas\Toolbox\Cli\Application('dev', new Zalas\Toolbox\Cli\ServiceContainer()))->run();


================================================
FILE: box-devkit.json.dist
================================================
{
    "base-path": "build/devkit-phar",
    "output": "../devkit.phar",
    "compression": "GZ",
    "directories": ["."],
    "check-requirements": false,
    "main": "bin/devkit.php",
    "compactors": [
        "KevinGH\\Box\\Compactor\\PhpScoper"
    ],
    "banner": [
        "This file is part of the zalas/toolbox project.",
        "",
        "(c) Jakub Zalas <jakub@zalas.pl>"
    ]
}


================================================
FILE: box.json.dist
================================================
{
    "base-path": "build/phar",
    "output": "../toolbox.phar",
    "compression": "GZ",
    "directories": ["."],
    "check-requirements": false,
    "main": "bin/toolbox.php",
    "compactors": [
        "KevinGH\\Box\\Compactor\\PhpScoper"
    ],
    "banner": [
        "This file is part of the zalas/toolbox project.",
        "",
        "(c) Jakub Zalas <jakub@zalas.pl>"
    ]
}


================================================
FILE: composer.json
================================================
{
    "name": "zalas/toolbox",
    "description": "Helps to discover and install tools",
    "type": "project",
    "require": {
        "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
        "symfony/console": "^7.4 || ^8.0",
        "psr/container": "^2.0"
    },
    "require-dev": {
        "phpunit/phpunit": "^11.5.9 || ^12.0 || ^13.0",
        "zalas/phpunit-globals": "^4.0",
        "infection/infection": "^0.31",
        "deptrac/deptrac": "^4.0"
    },
    "autoload": {
        "psr-4": {
            "Zalas\\Toolbox\\": "src"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "Zalas\\Toolbox\\Tests\\": "tests"
        }
    },
    "license": "MIT",
    "authors": [
        {
            "name": "Jakub Zalas",
            "email": "jakub@zalas.pl"
        }
    ],
    "extra": {
        "branch-alias": {
            "dev-master": "1.x-dev"
        }
    },
    "config": {
        "allow-plugins": {
            "infection/extension-installer": true
        }
    }
}


================================================
FILE: deptrac.yaml
================================================
parameters:
  paths:
    - ./src
  exclude_files: []
  layers:
    - name: Cli
      collectors:
        - type: classLike
          value: ^Zalas\\Toolbox\\Cli\\.*
    - name: Json
      collectors:
        - type: classLike
          value: ^Zalas\\Toolbox\\Json\\.*
    - name: Runner
      collectors:
        - type: classLike
          value: ^Zalas\\Toolbox\\Runner\\.*
    - name: Tool
      collectors:
        - type: classLike
          value: ^Zalas\\Toolbox\\Tool\\.*
    - name: UseCase
      collectors:
        - type: classLike
          value: ^Zalas\\Toolbox\\UseCase\\.*
    - name: Psr Container
      collectors:
        - type: classLike
          value: ^Psr\\Container\\.*
    - name: Symfony Console
      collectors:
        - type: classLike
          value: ^Symfony\\Component\\Console\\.*
    - name: Other Vendors
      collectors:
        - type: bool
          must:
            # must be outside of global namespace
            - type: classLike
              value: '[\\]+'
          must_not:
            # must not be one of the known vendors
            - type: classLike
              value: ^Zalas\\Toolbox\\(Cli|Json|Runner|Tool|UseCase)\\.*
            - type: classLike
              value: ^Psr\\Container\\.*
            - type: classLike
              value: ^Symfony\\Component\\Console\\.*
  ruleset:
    Cli:
      - Tool
      - Json
      - Runner
      - UseCase
      - Symfony Console
      - Psr Container
    Json:
      - Tool
    Runner:
      - Tool
    Tool:
    UseCase:
      - Tool


================================================
FILE: infection.json.dist
================================================
{
    "timeout": 2,
    "source": {
        "directories": [
            "src"
        ]
    },
    "logs": {
        "text": "build/infection-log.txt"
    },
    "mutators": {
        "@default": true,
        "EqualIdentical": false,
        "NotIdenticalNotEqual": false,
        "Concat": false,
        "ConcatOperandRemoval": false,
        "ArrayItemRemoval": {
            "ignore": [
                "Zalas\\Toolbox\\Cli\\Command\\ListCommand::execute"
            ]
        }
    }
}


================================================
FILE: phpunit.xml.dist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<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">
  <coverage includeUncoveredFiles="true">
    <report>
      <clover outputFile="build/coverage.xml"/>
      <html outputDirectory="build/coverage" lowUpperBound="50" highLowerBound="95"/>
    </report>
  </coverage>
  <testsuites>
    <testsuite name="default">
      <directory suffix="Test.php">tests</directory>
    </testsuite>
  </testsuites>
  <php>
    <env name="TOOLBOX_TARGET_DIR" value="" force="true"/>
    <env name="TOOLBOX_TAGS" value="" force="true"/>
    <env name="TOOLBOX_EXCLUDED_TAGS" value="" force="true"/>
  </php>
  <extensions>
    <bootstrap class="Zalas\PHPUnit\Globals\AttributeExtension"/>
  </extensions>
  <logging/>
  <source>
    <include>
      <directory suffix=".php">src</directory>
    </include>
  </source>
</phpunit>


================================================
FILE: resources/architecture.json
================================================
{
    "tools": [
        {
            "name": "dephpend",
            "summary": "Detect flaws in your architecture",
            "website": "https://dephpend.com/",
            "command": {
                "phive-install": {
                    "alias": "dephpend",
                    "bin": "%target-dir%/dephpend",
                    "sig": "76835C9464877BDD"
                }
            },
            "test": "dephpend list",
            "tags": ["architecture"]
        },
        {
            "name": "deptrac",
            "summary": "Enforces dependency rules between software layers",
            "website": "https://github.com/deptrac/deptrac",
            "command": {
                "composer-global-install": {
                    "package": "deptrac/deptrac"
                }
            },
            "test": "deptrac list",
            "tags": ["featured", "architecture"]
        },
        {
            "name": "pdepend",
            "summary": "Static Analysis Tool",
            "website": "https://pdepend.org/",
            "command": {
                "phive-install": {
                    "alias": "pdepend/pdepend",
                    "bin": "%target-dir%/pdepend",
                    "sig": "508003DAED98C308"
                }
            },
            "test": "pdepend --version",
            "tags": ["featured", "architecture"]
        },
        {
            "name": "phparkitect",
            "summary": "Helps to put architectural constraints in a PHP code base",
            "website": "https://github.com/phparkitect/arkitect",
            "command": {
                "phar-download": {
                    "phar": "https://github.com/phparkitect/arkitect/releases/latest/download/phparkitect.phar",
                    "bin": "%target-dir%/phparkitect"
                }
            },
            "test": "phparkitect --version",
            "tags": ["architecture"]
        }
    ]
}


================================================
FILE: resources/checkstyle.json
================================================
{
    "tools": [
        {
            "name": "ecs",
            "summary": "Sets up and runs coding standard checks",
            "website": "https://github.com/Symplify/EasyCodingStandard",
            "command": {
                "composer-bin-plugin": {
                    "package": "symplify/easy-coding-standard",
                    "namespace": "ecs",
                    "links": {"%target-dir%/ecs": "ecs"}
                }
            },
            "test": "ecs -h",
            "tags": ["checkstyle"]
        },
        {
            "name": "pint",
            "summary": "Opinionated PHP code style fixer for Laravel",
            "website": "https://github.com/laravel/pint",
            "command": {
                "composer-bin-plugin": {
                    "package": "laravel/pint",
                    "namespace": "pint",
                    "links": {"%target-dir%/pint": "pint"}
                }
            },
            "test": "pint --version",
            "tags": ["checkstyle"]
        },
        {
            "name": "php-cs-fixer",
            "summary": "PHP Coding Standards Fixer",
            "website": "http://cs.symfony.com/",
            "command": {
                "phive-install": {
                    "alias": "php-cs-fixer",
                    "bin": "%target-dir%/php-cs-fixer",
                    "sig": "E82B2FB314E9906E"
                }
            },
            "test": "php-cs-fixer list",
            "tags": ["featured", "checkstyle"]
        },
        {
            "name": "phpcbf",
            "summary": "Automatically corrects coding standard violations",
            "website": "https://github.com/PHPCSStandards/PHP_CodeSniffer",
            "command": {
                "phive-install": {
                    "alias": "phpcbf",
                    "bin": "%target-dir%/phpcbf",
                    "sig": "97B02DD8E5071466"
                }
            },
            "test": "phpcbf --help",
            "tags": ["checkstyle"]
        },
        {
            "name": "twigcs",
            "summary": "The missing checkstyle for twig!",
            "website": "https://github.com/friendsoftwig/twigcs",
            "command": {
                "phive-install": {
                    "alias": "friendsoftwig/twigcs",
                    "bin": "%target-dir%/twigcs",
                    "sig": "C00543248C87FB13"
                }
            },
            "test": "twigcs --help",
            "tags": ["checkstyle", "exclude-php:8.5"]
        },
        {
            "name": "twig-cs-fixer",
            "summary": "Automatically corrects twig files following the official coding standard rules",
            "website": "https://github.com/VincentLanglet/Twig-CS-Fixer",
            "command": {
                "composer-bin-plugin": {
                    "package": "vincentlanglet/twig-cs-fixer",
                    "namespace": "twig-cs-fixer",
                    "links": {"%target-dir%/twig-cs-fixer": "twig-cs-fixer"}
                }
            },
            "test": "twig-cs-fixer --help",
            "tags": ["checkstyle"]
        }
    ]
}


================================================
FILE: resources/compatibility.json
================================================
{
    "tools": [
        {
            "name": "php-semver-checker",
            "summary": "Suggests a next version according to semantic versioning",
            "website": "https://github.com/tomzx/php-semver-checker",
            "command": {
                "phar-download": {
                    "phar": "https://github.com/tomzx/php-semver-checker/releases/download/v0.17.0/php-semver-checker.phar",
                    "bin": "%target-dir%/php-semver-checker"
                }
            },
            "test": "php-semver-checker list",
            "tags": ["compatibility"]
        },
        {
            "name": "roave-backward-compatibility-check",
            "summary": "Tool to compare two revisions of a class API to check for BC breaks",
            "website": "https://github.com/Roave/BackwardCompatibilityCheck",
            "command": {
                "sh": {
                    "command": "composer global bin roavebackwardcompatibilitycheck config allow-plugins.ocramius/package-versions true"
                },
                "composer-bin-plugin": {
                    "package": "roave/backward-compatibility-check",
                    "namespace": "roavebackwardcompatibilitycheck",
                    "links": {"%target-dir%/roave-backward-compatibility-check": "roave-backward-compatibility-check"}
                }
            },
            "test": "roave-backward-compatibility-check --version",
            "tags": ["compatibility"]
        }
    ]
}


================================================
FILE: resources/composer.json
================================================
{
    "tools": [
        {
            "name": "composer-normalize",
            "summary": "Composer plugin to normalize composer.json files",
            "website": "https://github.com/ergebnis/composer-normalize",
            "command": {
                "sh": {
                    "command": "composer config --global --json allow-plugins.ergebnis/composer-normalize true"
                },
                "composer-global-install": {
                    "package": "ergebnis/composer-normalize"
                }
            },
            "test": "composer global show ergebnis/composer-normalize",
            "tags": ["composer"]
        },
        {
            "name": "composer-unused",
            "summary": "Show unused packages by scanning your code",
            "website": "https://github.com/icanhazstring/composer-unused",
            "command": {
                "phive-install": {
                    "alias": "composer-unused",
                    "bin": "%target-dir%/composer-unused",
                    "sig": "3135AA4CB4F1AB0B"
                }
            },
            "test": "composer-unused -V",
            "tags": ["composer"]
        },
        {
            "name": "composer-require-checker",
            "summary": "Verify that no unknown symbols are used in the sources of a package.",
            "website": "https://github.com/maglnet/ComposerRequireChecker",
            "command": {
                "phive-install": {
                    "alias": "composer-require-checker",
                    "bin": "%target-dir%/composer-require-checker",
                    "sig": "033E5F8D801A2F8D"
                }
            },
            "test": "composer-require-checker -V",
            "tags": ["composer", "exclude-php:8.2", "exclude-php:8.3"]
        },
        {
            "name": "composer-require-checker-3",
            "summary": "Verify that no unknown symbols are used in the sources of a package.",
            "website": "https://github.com/maglnet/ComposerRequireChecker",
            "command": {
                "phive-install": {
                    "alias": "composer-require-checker@^3.8",
                    "bin": "%target-dir%/composer-require-checker-3",
                    "sig": "033E5F8D801A2F8D"
                }
            },
            "test": "composer-require-checker-3 -V",
            "tags": ["composer"]
        },
        {
            "name": "cyclonedx-php-composer",
            "summary": "Composer plugin to create Software-Bill-of-Materials (SBOM) in CycloneDX format",
            "website": "https://github.com/CycloneDX/cyclonedx-php-composer",
            "command": {
                "sh": {
                    "command": "composer global config --no-plugins allow-plugins.cyclonedx/cyclonedx-php-composer true"
                },
                "composer-global-install": {
                    "package": "cyclonedx/cyclonedx-php-composer"
                }
            },
            "test": "composer global show cyclonedx/cyclonedx-php-composer",
            "tags": ["composer"]
        },
        {
            "name": "composer-lock-diff",
            "summary": "Composer plugin to check what has changed after a composer update",
            "website": "https://github.com/davidrjonas/composer-lock-diff",
            "command": {
                "composer-bin-plugin": {
                    "package": "davidrjonas/composer-lock-diff",
                    "namespace": "composer-lock-diff",
                    "links": {"%target-dir%/composer-lock-diff": "composer-lock-diff"}
                }
            },
            "test": "composer-lock-diff --help",
            "tags": ["composer"]
        },
        {
            "name": "jack",
            "summary": "Helps to upgrade outdated Composer dependencies incrementally",
            "website": "https://github.com/rectorphp/jack",
            "command": {
                "composer-bin-plugin": {
                    "package": "rector/jack:0.4.0",
                    "namespace": "jack",
                    "links": {"%target-dir%/jack": "jack"}
                }
            },
            "test": "jack help",
            "tags": ["composer"]
        }
    ]
}


================================================
FILE: resources/deprecation.json
================================================
{
    "tools": [
        {
            "name": "deprecation-detector",
            "summary": "Finds usages of deprecated code",
            "website": "https://github.com/sensiolabs-de/deprecation-detector",
            "command": {
                "phive-install": {
                    "alias": "sensiolabs-de/deprecation-detector",
                    "bin": "%target-dir%/deprecation-detector"
                }
            },
            "test": "deprecation-detector list",
            "tags": ["deprecation"]
        },
        {
            "name": "phpdd",
            "summary": "Finds usage of deprecated features",
            "website": "http://wapmorgan.github.io/PhpDeprecationDetector",
            "command": {
                "phive-install": {
                    "alias": "wapmorgan/phpdeprecationdetector",
                    "bin": "%target-dir%/phpdd"
                }
            },
            "test": "phpdd -h",
            "tags": ["deprecation"]
        }
    ]
}


================================================
FILE: resources/documentation.json
================================================
{
    "tools": [
        {
            "name": "phpDocumentor",
            "summary": "Documentation generator",
            "website": "https://www.phpdoc.org/",
            "command": {
                "phive-install": {
                    "alias": "phpDocumentor",
                    "bin": "%target-dir%/phpDocumentor",
                    "sig": "6DA3ACC4991FFAE5"
                }
            },
            "test": "phpDocumentor --help",
            "tags": ["featured", "documentation"]
        },
        {
            "name": "phpcb",
            "summary": "PHP Code Browser",
            "website": "https://github.com/mayflower/PHP_CodeBrowser",
            "command": {
                "phar-download": {
                    "phar": "https://github.com/bytepark/php-phar-qatools/raw/master/phpcb.phar",
                    "bin": "%target-dir%/phpcb"
                }
            },
            "test": "phpcb -V",
            "tags": ["documentation"]
        }
    ]
}


================================================
FILE: resources/linting.json
================================================
{
    "tools": [
        {
            "name": "parallel-lint",
            "summary": "Checks PHP file syntax",
            "website": "https://github.com/php-parallel-lint/PHP-Parallel-Lint",
            "command": {
                "phive-install": {
                    "alias": "php-parallel-lint/PHP-Parallel-Lint",
                    "bin": "%target-dir%/parallel-lint"
                }
            },
            "test": "parallel-lint -h",
            "tags": ["linting"]
        },
        {
            "name": "phplint",
            "summary": "Lints php files in parallel",
            "website": "https://github.com/overtrue/phplint",
            "command": {
                "composer-bin-plugin": {
                    "package": "overtrue/phplint",
                    "namespace": "phplint",
                    "links": {"%target-dir%/phplint": "phplint"}
                }
            },
            "test": "phplint -V",
            "tags": ["linting"]
        },
        {
            "name": "twig-lint",
            "summary": "Standalone cli twig 1.X linter",
            "website": "https://github.com/asm89/twig-lint",
            "command": {
                "phar-download": {
                    "phar": "https://asm89.github.io/d/twig-lint.phar",
                    "bin": "%target-dir%/twig-lint"
                }
            },
            "test": "twig-lint --version",
            "tags": ["linting"]
        },
        {
            "name": "yaml-lint",
            "summary": "Compact command line utility for checking YAML file syntax",
            "website": "https://github.com/j13k/yaml-lint",
            "command": {
                "phive-install": {
                    "alias": "j13k/yaml-lint",
                    "bin": "%target-dir%/yaml-lint",
                    "sig": "38A182AB413064D7"
                }
            },
            "test": "yaml-lint --version",
            "tags": ["linting"]
        },
        {
            "name": "twig-linter",
            "summary": "Standalone cli twig 3.X linter",
            "website": "https://github.com/sserbin/twig-linter",
            "command": {
                "composer-bin-plugin": {
                    "package": "sserbin/twig-linter:@dev",
                    "namespace": "twig-linter",
                    "links": {"%target-dir%/twig-linter": "twig-linter"}
                }
            },
            "test": "twig-linter --help",
            "tags": ["linting"]
        },
        {
            "name": "gherkin-lint-php",
            "summary": "Gherkin linter for PHP",
            "website": "https://github.com/dantleech/gherkin-lint-php",
            "command": {
                "composer-bin-plugin": {
                    "package": "dantleech/gherkin-lint",
                    "namespace": "gherkin-lint-php",
                    "links": {"%target-dir%/gherkinlint": "gherkinlint"}
                }
            },
            "test": "gherkinlint --help",
            "tags": ["linting"]
        }
    ]
}


================================================
FILE: resources/metrics.json
================================================
{
    "tools": [
        {
            "name": "phpinsights",
            "summary": "Analyses code quality, style, architecture and complexity",
            "website": "https://phpinsights.com/",
            "command": {
                "sh": {
                    "command": "composer global bin phpinsights config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true"
                },
                "composer-bin-plugin": {
                    "package": "nunomaduro/phpinsights",
                    "namespace": "phpinsights",
                    "links": {"%target-dir%/phpinsights": "phpinsights"}
                }
            },
            "test": "phpinsights --version",
            "tags": ["metrics"]
        },
        {
            "name": "phploc",
            "summary": "A tool for quickly measuring the size of a PHP project",
            "website": "https://github.com/sebastianbergmann/phploc",
            "command": {
                "phive-install": {
                    "alias": "phploc",
                    "bin": "%target-dir%/phploc",
                    "sig": "4AA394086372C20A"
                }
            },
            "test": "phploc -v",
            "tags": ["metrics"]
        },
        {
            "name": "phpmetrics",
            "summary": "Static Analysis Tool",
            "website": "http://www.phpmetrics.org/",
            "command": {
                "phive-install": {
                    "alias": "phpmetrics/PhpMetrics",
                    "bin": "%target-dir%/phpmetrics"
                }
            },
            "test": "phpmetrics --version",
            "tags": ["featured", "metrics"]
        },
        {
            "name": "lines",
            "summary": "CLI tool for quick metrics of PHP projects",
            "website": "https://github.com/tomasVotruba/lines",
            "command": {
                "composer-bin-plugin": {
                    "package": "tomasvotruba/lines",
                    "namespace": "lines",
                    "links": {"%target-dir%/lines": "lines"}
                }
            },
            "test": "lines --version",
            "tags": ["metrics"]
        }
    ]
}


================================================
FILE: resources/phpcs.json
================================================
{
  "tools": [
    {
      "name": "phpcs",
      "summary": "Detects coding standard violations",
      "website": "https://github.com/PHPCSStandards/PHP_CodeSniffer",
      "command": {
        "composer-bin-plugin": {
          "package": "squizlabs/php_codesniffer",
          "namespace": "phpcs",
          "links": {"%target-dir%/phpcs": "phpcs"}
        }
      },
      "test": "phpcs --help",
      "tags": ["checkstyle"]
    },
    {
      "name": "phpcodesniffer-composer-install",
      "summary": "Easy installation of PHP_CodeSniffer coding standards (rulesets).",
      "website": "https://github.com/PHPCSStandards/composer-installer",
      "command": {
        "sh": {
          "command": "composer global bin phpcs config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true"
        },
        "composer-bin-plugin": {
          "package": "dealerdirect/phpcodesniffer-composer-installer",
          "namespace": "phpcs"
        }
      },
      "test": "composer global bin phpcs show dealerdirect/phpcodesniffer-composer-installer",
      "tags": ["pre-installation"]
    },
    {
      "name": "phpcs-security-audit",
      "summary": "Finds vulnerabilities and weaknesses related to security in PHP code",
      "website": "https://github.com/FloeDesignTechnologies/phpcs-security-audit",
      "command": {
        "composer-bin-plugin": {
          "package": "pheromone/phpcs-security-audit",
          "namespace": "phpcs"
        }
      },
      "test": "phpcs -i | grep Security",
      "tags": ["security"]
    }
  ]
}


================================================
FILE: resources/phpstan.json
================================================
{
    "tools": [
        {
            "name": "phpstan",
            "summary": "Static Analysis Tool",
            "website": "https://github.com/phpstan/phpstan",
            "command": {
                "composer-bin-plugin": {
                    "package": "phpstan/phpstan",
                    "namespace": "phpstan",
                    "links": {"%target-dir%/phpstan": "phpstan"}
                }
            },
            "test": "phpstan --version",
            "tags": ["featured", "phpstan"]
        },
        {
            "name": "phpstan-deprecation-rules",
            "summary": "PHPStan rules for detecting deprecated code",
            "website": "https://github.com/phpstan/phpstan-deprecation-rules",
            "command": {
                "composer-bin-plugin": {
                    "package": "phpstan/phpstan-deprecation-rules",
                    "namespace": "phpstan"
                }
            },
            "test": "composer global bin phpstan show phpstan/phpstan-deprecation-rules",
            "tags": ["phpstan"]
        },
        {
            "name": "phpstan-ergebnis-rules",
            "summary": "Additional rules for PHPstan",
            "website": "https://github.com/ergebnis/phpstan-rules",
            "command": {
                "composer-bin-plugin": {
                    "package": "ergebnis/phpstan-rules",
                    "namespace": "phpstan"
                }
            },
            "test": "composer global bin phpstan show ergebnis/phpstan-rules",
            "tags": ["phpstan"]
        },
        {
            "name": "phpstan-strict-rules",
            "summary": "Extra strict and opinionated rules for PHPStan",
            "website": "https://github.com/phpstan/phpstan-strict-rules",
            "command": {
                "composer-bin-plugin": {
                    "package": "phpstan/phpstan-strict-rules",
                    "namespace": "phpstan"
                }
            },
            "test": "composer global bin phpstan show phpstan/phpstan-strict-rules",
            "tags": ["phpstan"]
        },
        {
            "name": "phpstan-doctrine",
            "summary": "Doctrine extensions for PHPStan",
            "website": "https://github.com/phpstan/phpstan-doctrine",
            "command": {
                "composer-bin-plugin": {
                    "package": "phpstan/phpstan-doctrine",
                    "namespace": "phpstan"
                }
            },
            "test": "composer global bin phpstan show phpstan/phpstan-doctrine",
            "tags": ["phpstan"]
        },
        {
            "name": "phpstan-phpunit",
            "summary": "PHPUnit extensions and rules for PHPStan",
            "website": "https://github.com/phpstan/phpstan-phpunit",
            "command": {
                "composer-bin-plugin": {
                    "package": "phpstan/phpstan-phpunit",
                    "namespace": "phpstan"
                }
            },
            "test": "composer global bin phpstan show phpstan/phpstan-phpunit",
            "tags": ["phpstan"]
        },
        {
            "name": "phpstan-symfony",
            "summary": "Symfony extension for PHPStan",
            "website": "https://github.com/phpstan/phpstan-symfony",
            "command": {
                "composer-bin-plugin": {
                    "package": "phpstan/phpstan-symfony",
                    "namespace": "phpstan"
                }
            },
            "test": "composer global bin phpstan show phpstan/phpstan-symfony",
            "tags": ["phpstan"]
        },
        {
            "name": "phpstan-beberlei-assert",
            "summary": "PHPStan extension for beberlei/assert",
            "website": "https://github.com/phpstan/phpstan-beberlei-assert",
            "command": {
                "composer-bin-plugin": {
                    "package": "phpstan/phpstan-beberlei-assert",
                    "namespace": "phpstan"
                }
            },
            "test": "composer global bin phpstan show phpstan/phpstan-beberlei-assert",
            "tags": ["phpstan"]
        },
        {
            "name": "phpstan-webmozart-assert",
            "summary": "PHPStan extension for webmozart/assert",
            "website": "https://github.com/phpstan/phpstan-webmozart-assert",
            "command": {
                "composer-bin-plugin": {
                    "package": "phpstan/phpstan-webmozart-assert",
                    "namespace": "phpstan"
                }
            },
            "test": "composer global bin phpstan show phpstan/phpstan-webmozart-assert",
            "tags": ["phpstan"]
        },
        {
            "name": "phpat",
            "summary": "Easy to use architecture testing tool",
            "website": "https://github.com/carlosas/phpat",
            "command": {
                "composer-bin-plugin": {
                    "package": "phpat/phpat",
                    "namespace": "phpstan"
                }
            },
            "test": "composer global bin phpstan show phpat/phpat",
            "tags": ["phpstan", "architecture"]
        },
        {
            "name": "phpstan-larastan",
            "summary": "Separate installation of phpstan for larastan",
            "website": "https://github.com/phpstan/phpstan",
            "command": {
                "composer-bin-plugin": {
                    "package": "phpstan/phpstan",
                    "namespace": "larastan",
                    "links": {"%target-dir%/phpstan-larastan": "phpstan"}
                }
            },
            "test": "phpstan-larastan --version",
            "tags": ["phpstan"]
        },
        {
            "name": "larastan",
            "summary": "PHPStan extension for Laravel",
            "website": "https://github.com/nunomaduro/larastan",
            "command": {
                "composer-bin-plugin": {
                    "package": "nunomaduro/larastan",
                    "namespace": "larastan"
                }
            },
            "test": "composer global bin phpstan show ergebnis/phpstan-rules",
            "tags": ["phpstan"]
        },
        {
            "name": "phpstan-banned-code",
            "summary": "PHPStan rules for detecting calls to specific functions you don't want in your project",
            "website": "https://github.com/ekino/phpstan-banned-code",
            "command": {
                "composer-bin-plugin": {
                    "package": "ekino/phpstan-banned-code",
                    "namespace": "phpstan"
                }
            },
            "test": "composer global bin phpstan show ekino/phpstan-banned-code",
            "tags": [
                "phpstan"
            ]
        }
    ]
}


================================================
FILE: resources/pre-installation.json
================================================
{
  "tools": [
    {
      "name": "composer",
      "summary": "Dependency Manager for PHP",
      "website": "https://getcomposer.org/",
      "command": {
        "sh": {
          "command": "composer self-update"
        }
      },
      "test": "composer list",
      "tags": ["pre-installation"]
    },
    {
      "name": "phive",
      "summary": "PHAR Installation and Verification Environment",
      "website": "https://phar.io/",
      "command": {
        "file-download": {
          "url": "https://github.com/phar-io/phive/releases/download/0.16.0/phive-0.16.0.phar.asc",
          "file": "%target-dir%/phive.asc"
        },
        "phar-download": {
          "phar": "https://github.com/phar-io/phive/releases/download/0.16.0/phive-0.16.0.phar",
          "bin": "%target-dir%/phive"
        },
        "sh": {
          "command": "gpg --keyserver hkps://keys.openpgp.org --recv-keys 0x9D8A98B29B2D5D79 && gpg --verify %target-dir%/phive.asc %target-dir%/phive"
        }
      },
      "test": "phive --version",
      "tags": ["pre-installation"]
    },
    {
      "name": "composer-bin-plugin",
      "summary": "Composer plugin to install bin vendors in isolated locations",
      "website": "https://github.com/bamarni/composer-bin-plugin",
      "command": {
        "sh": {
          "command": "composer global config --json extra.bamarni-bin.bin-links false && composer config --global --json allow-plugins.bamarni/composer-bin-plugin true"
        },
        "composer-global-install": {
          "package": "bamarni/composer-bin-plugin"
        }
      },
      "test": "composer global show bamarni/composer-bin-plugin",
      "tags": ["pre-installation"]
    },
    {
      "name": "box",
      "summary": "Fast, zero config application bundler with PHARs",
      "website": "https://github.com/humbug/box",
      "command": {
        "phive-install": {
          "alias": "humbug/box",
          "bin": "%target-dir%/box",
          "sig": "2DF45277AEF09A2F"
        }
      },
      "test": "box list",
      "tags": ["pre-installation"]
    }
  ]
}


================================================
FILE: resources/psalm.json
================================================
{
    "tools": [
        {
            "name": "psalm",
            "summary": "Finds errors in PHP applications",
            "website": "https://psalm.dev/",
            "command": {
                "composer-bin-plugin": {
                    "package": "vimeo/psalm",
                    "namespace": "psalm",
                    "links": {
                        "%target-dir%/psalm": "psalm",
                        "%target-dir%/psalm-language-server": "psalm-language-server",
                        "%target-dir%/psalm-plugin": "psalm-plugin",
                        "%target-dir%/psalm-refactor": "psalm-refactor",
                        "%target-dir%/psalter": "psalter"
                    }
                }
            },
            "test": "psalm -h",
            "tags": ["featured", "psalm"]
        },
        {
            "name": "psalm-plugin-doctrine",
            "summary": "Stubs to let Psalm understand Doctrine better",
            "website": "https://github.com/weirdan/doctrine-psalm-plugin",
            "command": {
                "composer-bin-plugin": {
                    "package": "weirdan/doctrine-psalm-plugin",
                    "namespace": "psalm"
                }
            },
            "test": "cd / && psalm-plugin show | grep weirdan/doctrine-psalm-plugin",
            "tags": ["psalm"]
        },
        {
            "name": "psalm-plugin-phpunit",
            "summary": "Psalm plugin for PHPUnit",
            "website": "https://github.com/psalm/psalm-plugin-phpunit",
            "command": {
                "composer-bin-plugin": {
                    "package": "psalm/plugin-phpunit",
                    "namespace": "psalm"
                }
            },
            "test": "cd / && psalm-plugin show | grep psalm/plugin-phpunit",
            "tags": ["psalm"]
        },
        {
            "name": "psalm-plugin-symfony",
            "summary": "Psalm Plugin for Symfony",
            "website": "https://github.com/psalm/psalm-plugin-symfony",
            "command": {
                "composer-bin-plugin": {
                    "package": "psalm/plugin-symfony",
                    "namespace": "psalm"
                }
            },
            "test": "cd / && psalm-plugin show | grep psalm/plugin-symfony",
            "tags": ["psalm"]
        }
    ]
}


================================================
FILE: resources/refactoring.json
================================================
{
    "tools": [
        {
            "name": "churn",
            "summary": "Discovers good candidates for refactoring",
            "website": "https://github.com/bmitch/churn-php",
            "command": {
                "phive-install": {
                    "alias": "churn",
                    "bin": "%target-dir%/churn",
                    "sig": "96141E4421A9B0D5"
                }
            },
            "test": "churn --version",
            "tags": ["featured", "refactoring"]
        },
        {
            "name": "rector",
            "summary": "Tool for instant code upgrades and refactoring",
            "website": "https://github.com/rectorphp/rector",
            "command": {
                "composer-bin-plugin": {
                    "package": "rector/rector",
                    "namespace": "rector",
                    "links": {"%target-dir%/rector": "rector"}
                }
            },
            "test": "rector --version",
            "tags": ["refactoring"]
        }
    ]
}


================================================
FILE: resources/security.json
================================================
{
    "tools": [
        {
            "name": "psecio-parse",
            "summary": "Scans code for potential security-related issues",
            "website": "https://github.com/psecio/parse",
            "command": {
                "composer-bin-plugin": {
                    "package": "psecio/parse:dev-master",
                    "namespace": "legacy-php-parser",
                    "links": {"%target-dir%/psecio-parse": "psecio-parse"}
                }
            },
            "test": "psecio-parse --version",
            "tags": ["security"]
        },
        {
            "name": "local-php-security-checker",
            "summary": "Checks composer dependencies for known security vulnerabilities",
            "website": "https://github.com/fabpot/local-php-security-checker",
            "command": {
                "file-download": {
                    "url": "https://github.com/fabpot/local-php-security-checker/releases/download/v2.0.6/local-php-security-checker_2.0.6_linux_amd64",
                    "file": "%target-dir%/local-php-security-checker"
                },
                "sh": {
                    "command": "chmod +x %target-dir%/local-php-security-checker"
                }
            },
            "test": "local-php-security-checker --help",
            "tags": ["featured", "security"]
        }
    ]
}


================================================
FILE: resources/test.json
================================================
{
    "tools": [
        {
            "name": "behat",
            "summary": "Helps to test business expectations",
            "website": "http://behat.org/",
            "command": {
                "composer-bin-plugin": {
                    "package": "behat/behat",
                    "namespace": "behat",
                    "links": {"%target-dir%/behat": "behat"}
                }
            },
            "test": "behat --version",
            "tags": ["featured", "test"]
        },
        {
            "name": "codeception",
            "summary": "Codeception is a BDD-styled PHP testing framework",
            "website": "https://codeception.com/",
            "command": {
                "phar-download": {
                    "phar": "https://codeception.com/codecept.phar",
                    "bin": "%target-dir%/codeception"
                }
            },
            "test": "codeception --version",
            "tags": ["test", "exclude-php:8.5"]
        },
        {
            "name": "infection",
            "summary": "AST based PHP Mutation Testing Framework",
            "website": "https://infection.github.io/",
            "command": {
                "phive-install": {
                    "alias": "infection",
                    "bin": "%target-dir%/infection",
                    "sig": "C5095986493B4AA0"
                }
            },
            "test": "infection --version",
            "tags": ["featured", "test"]
        },
        {
            "name": "paratest",
            "summary": "Parallel testing for PHPUnit",
            "website": "https://github.com/paratestphp/paratest",
            "command": {
                "composer-bin-plugin": {
                    "package": "brianium/paratest",
                    "namespace": "paratest",
                    "links": {"%target-dir%/paratest": "paratest"}
                }
            },
            "test": "paratest --version",
            "tags": ["test"]
        },
        {
            "name": "phpcov",
            "summary": "a command-line frontend for the PHP_CodeCoverage library",
            "website": "https://github.com/sebastianbergmann/phpcov",
            "command": {
                "phive-install": {
                    "alias": "phpcov",
                    "bin": "%target-dir%/phpcov",
                    "sig": "4AA394086372C20A"
                }
            },
            "test": "phpcov -v",
            "tags": ["test", "exclude-php:8.2", "exclude-php:8.3"]
        },
        {
            "name": "php-fuzzer",
            "summary": "A fuzzer for PHP, which can be used to find bugs in libraries by feeding them 'random' inputs",
            "website": "https://github.com/nikic/PHP-Fuzzer",
            "command": {
                "phive-install": {
                    "alias": "nikic/php-fuzzer",
                    "bin": "%target-dir%/php-fuzzer"
                }
            },
            "test": "php-fuzzer --help | grep 'Usage:'",
            "tags": ["test"]
        },
        {
            "name": "phpspec",
            "summary": "SpecBDD Framework",
            "website": "http://www.phpspec.net/",
            "command": {
                "phive-install": {
                    "alias": "phpspec/phpspec",
                    "bin": "%target-dir%/phpspec"
                }
            },
            "test": "phpspec --version",
            "tags": ["featured", "test", "exclude-php:8.5"]
        },
        {
            "name": "phpunit",
            "summary": "The PHP testing framework",
            "website": "https://phpunit.de/",
            "command": {
                "phive-install": {
                    "alias": "phpunit",
                    "bin": "%target-dir%/phpunit",
                    "sig": "4AA394086372C20A"
                }
            },
            "test": "phpunit --version",
            "tags": ["featured", "test", "exclude-php:8.2", "exclude-php:8.3"]
        },
        {
            "name": "phpunit-12",
            "summary": "The PHP testing framework (12.x version)",
            "website": "https://phpunit.de/",
            "command": {
                "phive-install": {
                    "alias": "phpunit@^12.0",
                    "bin": "%target-dir%/phpunit-12",
                    "sig": "4AA394086372C20A"
                }
            },
            "test": "phpunit-12 --version",
            "tags": ["test", "exclude-php:8.2"]
        },
        {
            "name": "phpunit-11",
            "summary": "The PHP testing framework (11.x version)",
            "website": "https://phpunit.de/",
            "command": {
                "phive-install": {
                    "alias": "phpunit@^11.0",
                    "bin": "%target-dir%/phpunit-11",
                    "sig": "4AA394086372C20A"
                }
            },
            "test": "phpunit-11 --version",
            "tags": ["test"]
        },
        {
            "name": "phpunit-10",
            "summary": "The PHP testing framework (10.x version)",
            "website": "https://phpunit.de/",
            "command": {
                "phive-install": {
                    "alias": "phpunit@^10.0",
                    "bin": "%target-dir%/phpunit-10",
                    "sig": "4AA394086372C20A"
                }
            },
            "test": "phpunit-10 --version",
            "tags": ["test"]
        },
        {
            "name": "phpunit-9",
            "summary": "The PHP testing framework (9.x version)",
            "website": "https://phpunit.de/",
            "command": {
                "phive-install": {
                    "alias": "phpunit@^9.0",
                    "bin": "%target-dir%/phpunit-9",
                    "sig": "4AA394086372C20A"
                }
            },
            "test": "phpunit-9 --version",
            "tags": ["test"]
        },
        {
            "name": "phpunit-8",
            "summary": "The PHP testing framework (8.x version)",
            "website": "https://phpunit.de/",
            "command": {
                "phive-install": {
                    "alias": "phpunit@^8.0",
                    "bin": "%target-dir%/phpunit-8",
                    "sig": "4AA394086372C20A"
                }
            },
            "test": "phpunit-8 --version",
            "tags": ["test"]
        },
        {
            "name": "simple-phpunit",
            "summary": "Provides utilities to report legacy tests and usage of deprecated code",
            "website": "https://symfony.com/doc/current/components/phpunit_bridge.html",
            "command": {
                "composer-bin-plugin": {
                    "package": "symfony/phpunit-bridge",
                    "namespace": "symfony",
                    "links": {"%target-dir%/simple-phpunit": "simple-phpunit"}
                },
                "sh": {
                    "command": "simple-phpunit install && SYMFONY_PHPUNIT_VERSION=9 simple-phpunit install"
                }
            },
            "test": "simple-phpunit --version",
            "tags": ["test"]
        },
        {
            "name": "kahlan",
            "summary": "Kahlan is a full-featured Unit & BDD test framework a la RSpec/JSpec",
            "website": "https://kahlan.github.io/docs/",
            "command": {
                "composer-bin-plugin": {
                    "package": "kahlan/kahlan",
                    "namespace": "kahlan",
                    "links": {"%target-dir%/kahlan": "kahlan"}
                }
            },
            "test": "kahlan --version",
            "tags": ["test"]
        }
    ]
}


================================================
FILE: resources/tools.json
================================================
{
  "tools": [
    {
      "name": "diffFilter",
      "summary": "Applies QA tools to run on a single pull request",
      "website": "https://github.com/exussum12/coverageChecker",
      "command": {
        "composer-bin-plugin": {
          "package": "exussum12/coverage-checker",
          "namespace": "tools",
          "links": {"%target-dir%/diffFilter": "diffFilter"}
        }
      },
      "test": "diffFilter -v",
      "tags": []
    },
    {
      "name": "phan",
      "summary": "Static Analysis Tool",
      "website": "https://github.com/phan/phan",
      "command": {
        "phar-download": {
          "phar": "https://github.com/phan/phan/releases/latest/download/phan.phar",
          "bin": "%target-dir%/phan"
        }
      },
      "test": "phan -v",
      "tags": ["featured"]
    },
    {
      "name": "phpbench",
      "summary": "PHP Benchmarking framework",
      "website": "https://github.com/phpbench/phpbench",
      "command": {
        "phive-install": {
          "alias": "phpbench",
          "bin": "%target-dir%/phpbench",
          "sig": "6FC579F5F0FCC966"
        }
      },
      "test": "phpbench -V",
      "tags": []
    },
    {
      "name": "phpa",
      "summary": "Checks for weak assumptions",
      "website": "https://github.com/rskuipers/php-assumptions",
      "command": {
        "composer-bin-plugin": {
          "package": "rskuipers/php-assumptions",
          "namespace": "tools",
          "links": {"%target-dir%/phpa": "phpa"}
        }
      },
      "test": "phpa --version",
      "tags": ["not-maintained"]
    },
    {
      "name": "phpca",
      "summary": "Finds usage of non-built-in extensions",
      "website": "https://github.com/wapmorgan/PhpCodeAnalyzer",
      "command": {
        "composer-bin-plugin": {
          "package": "wapmorgan/php-code-analyzer",
          "namespace": "tools",
          "links": {"%target-dir%/phpca": "phpca"}
        }
      },
      "test": "phpca -h"
    },
    {
      "name": "phpcpd",
      "summary": "Copy/Paste Detector",
      "website": "https://github.com/sebastianbergmann/phpcpd",
      "command": {
        "phive-install": {
          "alias": "phpcpd",
          "bin": "%target-dir%/phpcpd",
          "sig": "4AA394086372C20A"
        }
      },
      "test": "phpcpd -v",
      "tags": ["featured"]
    },
    {
      "name": "phpmd",
      "summary": "A tool for finding problems in PHP code",
      "website": "https://phpmd.org/",
      "command": {
        "phive-install": {
          "alias": "phpmd",
          "bin": "%target-dir%/phpmd",
          "sig": "9093F8B32E4815AA"
        }
      },
      "test": "phpmd --version"
    },
    {
      "name": "phpmnd",
      "summary": "Helps to detect magic numbers",
      "website": "https://github.com/povils/phpmnd",
      "command": {
        "composer-bin-plugin": {
          "package": "povils/phpmnd",
          "namespace": "phpmnd",
          "links": {"%target-dir%/phpmnd": "phpmnd"}
        }
      },
      "test": "phpmnd -V"
    }
  ]
}



================================================
FILE: scoper.inc.php
================================================
<?php declare(strict_types=1);

return [
    // Whitelist globals so that Symfony polyfills are not scoped
    'expose-global-constants' => true,
    'expose-global-classes' => true,
    'expose-global-functions' => true,
    'exclude-files' => [
        'vendor/symfony/polyfill-php80/Resources/stubs/Stringable.php',
        'vendor/symfony/polyfill-php80/bootstrap.php',
        'vendor/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php',
        'vendor/symfony/polyfill-intl-normalizer/bootstrap.php',
    ],
    'expose-namespaces' => [
        'Symfony\Polyfill\Php80',
        'Symfony\Polyfill\Intl',
    ],
];


================================================
FILE: src/Cli/Application.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Cli;

use Psr\Container\ContainerInterface;
use Symfony\Component\Console\Application as CliApplication;
use Symfony\Component\Console\CommandLoader\CommandLoaderInterface;
use Symfony\Component\Console\CommandLoader\ContainerCommandLoader;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Zalas\Toolbox\Cli\Command\InstallCommand;
use Zalas\Toolbox\Cli\Command\ListCommand;
use Zalas\Toolbox\Cli\Command\TestCommand;

final class Application extends CliApplication
{
    private ServiceContainer $serviceContainer;

    public function __construct(string $version, ServiceContainer $serviceContainer)
    {
        parent::__construct('toolbox', $version);

        $this->serviceContainer = $serviceContainer;

        $this->setCommandLoader($this->createCommandLoader($serviceContainer));
    }

    /**
     * @throws \Throwable
     */
    public function doRun(InputInterface $input, OutputInterface $output): int
    {
        $this->serviceContainer->set(InputInterface::class, $input);
        $this->serviceContainer->set(OutputInterface::class, $output);

        return parent::doRun($input, $output);
    }

    protected function getDefaultInputDefinition(): InputDefinition
    {
        $definition = parent::getDefaultInputDefinition();
        $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()));

        return $definition;
    }

    private function toolsJsonDefault(): array
    {
        return \getenv('TOOLBOX_JSON')
            ? \array_map('trim', \explode(',', \getenv('TOOLBOX_JSON')))
            : [
                __DIR__.'/../../resources/pre-installation.json',
                __DIR__.'/../../resources/architecture.json',
                __DIR__.'/../../resources/checkstyle.json',
                __DIR__.'/../../resources/compatibility.json',
                __DIR__.'/../../resources/composer.json',
                __DIR__.'/../../resources/deprecation.json',
                __DIR__.'/../../resources/documentation.json',
                __DIR__.'/../../resources/linting.json',
                __DIR__.'/../../resources/metrics.json',
                __DIR__.'/../../resources/phpcs.json',
                __DIR__.'/../../resources/phpstan.json',
                __DIR__.'/../../resources/psalm.json',
                __DIR__.'/../../resources/refactoring.json',
                __DIR__.'/../../resources/security.json',
                __DIR__.'/../../resources/test.json',
                __DIR__.'/../../resources/tools.json',
            ];
    }

    private function createCommandLoader(ContainerInterface $container): CommandLoaderInterface
    {
        return new ContainerCommandLoader(
            $container,
            [
                InstallCommand::NAME => InstallCommand::class,
                ListCommand::NAME => ListCommand::class,
                TestCommand::NAME => TestCommand::class,
            ]
        );
    }
}


================================================
FILE: src/Cli/Command/DefaultTag.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Cli\Command;

trait DefaultTag
{
    private function defaultExcludeTag(): array
    {
        return \getenv('TOOLBOX_EXCLUDED_TAGS') ? \explode(',', \getenv('TOOLBOX_EXCLUDED_TAGS')) : [];
    }

    private function defaultTag(): array
    {
        return \getenv('TOOLBOX_TAGS') ? \explode(',', \getenv('TOOLBOX_TAGS')) : [];
    }
}


================================================
FILE: src/Cli/Command/DefaultTargetDir.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Cli\Command;

trait DefaultTargetDir
{
    private function defaultTargetDir(): string
    {
        return \getenv('TOOLBOX_TARGET_DIR') ? \getenv('TOOLBOX_TARGET_DIR') : '/usr/local/bin';
    }
}


================================================
FILE: src/Cli/Command/InstallCommand.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Cli\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Zalas\Toolbox\Runner\Runner;
use Zalas\Toolbox\Tool\Filter;
use Zalas\Toolbox\UseCase\InstallTools;

final class InstallCommand extends Command
{
    use DefaultTag;
    use DefaultTargetDir;

    public const NAME = 'install';

    private InstallTools $useCase;
    private Runner $runner;

    public function __construct(InstallTools $useCase, Runner $runner)
    {
        parent::__construct(self::NAME);

        $this->useCase = $useCase;
        $this->runner = $runner;
    }

    protected function configure(): void
    {
        $this->setDescription('Installs tools');
        $this->addOption('dry-run', null, InputOption::VALUE_NONE, 'Output the command without executing it');
        $this->addOption('target-dir', null, InputOption::VALUE_REQUIRED, 'The target installation directory', $this->defaultTargetDir());
        $this->addOption('exclude-tag', 'e', InputOption::VALUE_REQUIRED|InputOption::VALUE_IS_ARRAY, 'Tool tags to exclude', $this->defaultExcludeTag());
        $this->addOption('tag', 't', InputOption::VALUE_REQUIRED|InputOption::VALUE_IS_ARRAY, 'Tool tags to filter by', $this->defaultTag());
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        return $this->runner->run(\call_user_func($this->useCase, new Filter($input->getOption('exclude-tag'), $input->getOption('tag'))));
    }
}


================================================
FILE: src/Cli/Command/ListCommand.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Cli\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\StyleInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Zalas\Toolbox\Tool\Filter;
use Zalas\Toolbox\Tool\Tool;
use Zalas\Toolbox\UseCase\ListTools;

final class ListCommand extends Command
{
    use DefaultTag;

    public const NAME = 'list-tools';

    private ListTools $listTools;

    public function __construct(ListTools $listTools)
    {
        parent::__construct(self::NAME);

        $this->listTools = $listTools;
    }

    protected function configure(): void
    {
        $this->setDescription('Lists available tools');
        $this->addOption('exclude-tag', 'e', InputOption::VALUE_REQUIRED|InputOption::VALUE_IS_ARRAY, 'Tool tags to exclude', $this->defaultExcludeTag());
        $this->addOption('tag', 't', InputOption::VALUE_REQUIRED|InputOption::VALUE_IS_ARRAY, 'Tool tags to filter by', $this->defaultTag());
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $tools = \call_user_func($this->listTools, new Filter($input->getOption('exclude-tag'), $input->getOption('tag')));

        $style = $this->createStyle($input, $output);
        $style->title('Available tools');
        $style->table(
            ['Name', 'Summary'],
            $tools->map(function (Tool $tool) {
                return [\sprintf('<info>%s</info>', $tool->name()), $tool->summary().PHP_EOL.$tool->website().PHP_EOL];
            })->toArray()
        );

        return 0;
    }

    private function createStyle(InputInterface $input, OutputInterface $output): StyleInterface
    {
        return new SymfonyStyle($input, $output);
    }
}


================================================
FILE: src/Cli/Command/TestCommand.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Cli\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Zalas\Toolbox\Runner\Runner;
use Zalas\Toolbox\Tool\Filter;
use Zalas\Toolbox\UseCase\TestTools;

final class TestCommand extends Command
{
    use DefaultTag;
    use DefaultTargetDir;

    public const NAME = 'test';

    private TestTools $useCase;
    private Runner $runner;

    public function __construct(TestTools $useCase, Runner $runner)
    {
        parent::__construct(self::NAME);

        $this->useCase = $useCase;
        $this->runner = $runner;
    }

    protected function configure(): void
    {
        $this->setDescription('Runs basic tests to verify tools are installed');
        $this->addOption('dry-run', null, InputOption::VALUE_NONE, 'Output the command without executing it');
        $this->addOption('target-dir', null, InputOption::VALUE_REQUIRED, 'The target installation directory', $this->defaultTargetDir());
        $this->addOption('exclude-tag', 'e', InputOption::VALUE_REQUIRED|InputOption::VALUE_IS_ARRAY, 'Tool tags to exclude', $this->defaultExcludeTag());
        $this->addOption('tag', 't', InputOption::VALUE_REQUIRED|InputOption::VALUE_IS_ARRAY, 'Tool tags to filter by', $this->defaultTag());
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        return $this->runner->run(\call_user_func($this->useCase, new Filter($input->getOption('exclude-tag'), $input->getOption('tag'))));
    }
}


================================================
FILE: src/Cli/Runner/DryRunner.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Cli\Runner;

use Symfony\Component\Console\Output\OutputInterface;
use Zalas\Toolbox\Runner\Runner;
use Zalas\Toolbox\Tool\Command;

final class DryRunner implements Runner
{
    private OutputInterface $output;

    public function __construct(OutputInterface $output)
    {
        $this->output = $output;
    }

    public function run(Command $command): int
    {
        $this->output->writeln((string) $command);

        return 0;
    }
}


================================================
FILE: src/Cli/ServiceContainer/LazyRunner.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Cli\ServiceContainer;

use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Zalas\Toolbox\Runner\Runner;
use Zalas\Toolbox\Tool\Command;

final class LazyRunner implements Runner
{
    private ?Runner $runner = null;

    private RunnerFactory $factory;

    public function __construct(RunnerFactory $factory)
    {
        $this->factory = $factory;
    }

    /**
     * @throws ContainerExceptionInterface
     * @throws NotFoundExceptionInterface
     */
    public function run(Command $command): int
    {
        return $this->runner()->run($command);
    }

    /**
     * @throws ContainerExceptionInterface
     * @throws NotFoundExceptionInterface
     */
    private function runner(): Runner
    {
        if (null === $this->runner) {
            $this->runner = $this->factory->createRunner();
        }

        return $this->runner;
    }
}


================================================
FILE: src/Cli/ServiceContainer/RunnerFactory.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Cli\ServiceContainer;

use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Zalas\Toolbox\Cli\Runner\DryRunner;
use Zalas\Toolbox\Runner\ParametrisedRunner;
use Zalas\Toolbox\Runner\PassthruRunner;
use Zalas\Toolbox\Runner\Runner;

class RunnerFactory
{
    private ContainerInterface $container;

    public function __construct(ContainerInterface $container)
    {
        $this->container = $container;
    }

    /**
     * @throws ContainerExceptionInterface
     * @throws NotFoundExceptionInterface
     */
    public function createRunner(): Runner
    {
        $runner = $this->createRealRunner();

        if ($parameters = $this->parameters()) {
            return new ParametrisedRunner($runner, $parameters);
        }

        return $runner;
    }

    /**
     * @throws ContainerExceptionInterface
     * @throws NotFoundExceptionInterface
     */
    private function createRealRunner(): DryRunner|PassthruRunner
    {
        if ($this->container->get(InputInterface::class)->getOption('dry-run')) {
            return new DryRunner($this->container->get(OutputInterface::class));
        }

        return new PassthruRunner();
    }

    /**
     * @throws ContainerExceptionInterface
     * @throws NotFoundExceptionInterface
     */
    private function parameters(): array
    {
        if ($targetDir = $this->targetDir()) {
            return ['%target-dir%' => $targetDir];
        }

        return [];
    }

    /**
     * @throws ContainerExceptionInterface
     * @throws NotFoundExceptionInterface
     */
    private function targetDir(): ?string
    {
        if (!$this->container->get(InputInterface::class)->hasOption('target-dir')) {
            return null;
        }

        $targetDir = $this->container->get(InputInterface::class)->getOption('target-dir');

        if (!\is_dir($targetDir)) {
            throw new class(\sprintf('The target dir does not exist: "%s".', $targetDir)) extends \RuntimeException implements ContainerExceptionInterface {
            };
        }

        return \realpath($targetDir);
    }
}


================================================
FILE: src/Cli/ServiceContainer.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Cli;

use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;
use RuntimeException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Zalas\Toolbox\Cli\Command\InstallCommand;
use Zalas\Toolbox\Cli\Command\ListCommand;
use Zalas\Toolbox\Cli\Command\TestCommand;
use Zalas\Toolbox\Cli\ServiceContainer\LazyRunner;
use Zalas\Toolbox\Cli\ServiceContainer\RunnerFactory;
use Zalas\Toolbox\Json\JsonTools;
use Zalas\Toolbox\Runner\Runner;
use Zalas\Toolbox\Tool\Tools;
use Zalas\Toolbox\UseCase\InstallTools;
use Zalas\Toolbox\UseCase\ListTools;
use Zalas\Toolbox\UseCase\TestTools;

class ServiceContainer implements ContainerInterface
{
    private array $services = [
        InstallCommand::class => 'createInstallCommand',
        ListCommand::class => 'createListCommand',
        TestCommand::class => 'createTestCommand',
        Runner::class => 'createRunner',
        InstallTools::class => 'createInstallToolsUseCase',
        ListTools::class => 'createListToolsUseCase',
        TestTools::class => 'createTestToolsUseCase',
        Tools::class => 'createTools',
    ];

    private array $runtimeServices = [
        InputInterface::class => null,
        OutputInterface::class => null,
    ];

    public function set(string $id, /*object */$service): void
    {
        if (!\array_key_exists($id, $this->runtimeServices)) {
            throw new class(\sprintf('The "%s" runtime service is not expected.', $id)) extends RuntimeException implements ContainerExceptionInterface {
            };
        }

        $this->runtimeServices[$id] = $service;
    }

    /**
     * {@inheritdoc}
     */
    public function get(string $id)
    {
        if (isset($this->runtimeServices[$id])) {
            return $this->runtimeServices[$id];
        }

        if (isset($this->services[$id])) {
            return \call_user_func([$this, $this->services[$id]]);
        }

        throw new class(\sprintf('The "%s" service is not registered in the service container.', $id)) extends RuntimeException implements NotFoundExceptionInterface {
        };
    }

    /**
     * {@inheritdoc}
     */
    public function has(string $id): bool
    {
        return isset($this->services[$id]) || isset($this->runtimeServices[$id]);
    }

    /**
     * @throws ContainerExceptionInterface
     * @throws NotFoundExceptionInterface
     */
    private function createInstallCommand(): InstallCommand
    {
        return new InstallCommand($this->get(InstallTools::class), $this->get(Runner::class));
    }

    /**
     * @throws ContainerExceptionInterface
     * @throws NotFoundExceptionInterface
     */
    private function createListCommand(): ListCommand
    {
        return new ListCommand($this->get(ListTools::class));
    }

    /**
     * @throws ContainerExceptionInterface
     * @throws NotFoundExceptionInterface
     */
    private function createTestCommand(): TestCommand
    {
        return new TestCommand($this->get(TestTools::class), $this->get(Runner::class));
    }

    private function createRunner(): Runner
    {
        return new LazyRunner(new RunnerFactory($this));
    }

    /**
     * @throws ContainerExceptionInterface
     * @throws NotFoundExceptionInterface
     */
    private function createInstallToolsUseCase(): InstallTools
    {
        return new InstallTools($this->get(Tools::class));
    }

    /**
     * @throws ContainerExceptionInterface
     * @throws NotFoundExceptionInterface
     */
    private function createListToolsUseCase(): ListTools
    {
        return new ListTools($this->get(Tools::class));
    }

    /**
     * @throws ContainerExceptionInterface
     * @throws NotFoundExceptionInterface
     */
    private function createTestToolsUseCase(): TestTools
    {
        return new TestTools($this->get(Tools::class));
    }

    private function createTools(): Tools
    {
        return new JsonTools(function (): array {
            return $this->get(InputInterface::class)->getOption('tools');
        });
    }
}


================================================
FILE: src/Json/Factory/Assert.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Json\Factory;

final class Assert
{
    public static function requireFields(array $fields, array $data, string $type): void
    {
        $missingFields = \array_filter($fields, function (string $field) use ($data) {
            return !isset($data[$field]);
        });

        if (!empty($missingFields)) {
            throw new \InvalidArgumentException(\sprintf('Missing fields "%s" in the %s: `%s`.', \implode(', ', $missingFields), $type, \json_encode($data)));
        }
    }
}


================================================
FILE: src/Json/Factory/BoxBuildCommandFactory.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Json\Factory;

use Zalas\Toolbox\Tool\Command;
use Zalas\Toolbox\Tool\Command\BoxBuildCommand;

final class BoxBuildCommandFactory
{
    public static function import(array $definition): Command
    {
        Assert::requireFields(['repository', 'phar', 'bin'], $definition, 'BoxBuildCommand');

        return new BoxBuildCommand($definition['repository'], $definition['phar'], $definition['bin'], \sys_get_temp_dir(), $definition['version'] ?? null);
    }
}


================================================
FILE: src/Json/Factory/ComposerBinPluginCommandFactory.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Json\Factory;

use Zalas\Toolbox\Tool\Collection;
use Zalas\Toolbox\Tool\Command;
use Zalas\Toolbox\Tool\Command\ComposerBinPluginCommand;
use Zalas\Toolbox\Tool\Command\ComposerBinPluginLinkCommand;

final class ComposerBinPluginCommandFactory
{
    public static function import(array $command): Command
    {
        Assert::requireFields(['package', 'namespace'], $command, 'ComposerBinPluginCommand');

        return new ComposerBinPluginCommand($command['package'], $command['namespace'], self::importLinks($command));
    }

    private static function importLinks(array $command): Collection
    {
        $links = $command['links'] ?? [];
        $namespace = $command['namespace'];

        return Collection::create(
            \array_map(function (string $source, string $target) use ($namespace) {
                return new ComposerBinPluginLinkCommand($source, $target, $namespace);
            }, $links, \array_keys($links))
        );
    }
}


================================================
FILE: src/Json/Factory/ComposerGlobalInstallCommandFactory.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Json\Factory;

use Zalas\Toolbox\Tool\Command;
use Zalas\Toolbox\Tool\Command\ComposerGlobalInstallCommand;

final class ComposerGlobalInstallCommandFactory
{
    public static function import(array $command): Command
    {
        Assert::requireFields(['package'], $command, 'ComposerGlobalInstallCommand');

        return new ComposerGlobalInstallCommand($command['package']);
    }
}


================================================
FILE: src/Json/Factory/ComposerInstallCommandFactory.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Json\Factory;

use Zalas\Toolbox\Tool\Command;
use Zalas\Toolbox\Tool\Command\ComposerInstallCommand;

final class ComposerInstallCommandFactory
{
    public static function import(array $command): Command
    {
        Assert::requireFields(['repository', 'target-dir'], $command, 'ComposerInstallCommand');

        return new ComposerInstallCommand($command['repository'], $command['target-dir'], $command['version'] ?? null);
    }
}


================================================
FILE: src/Json/Factory/FileDownloadCommandFactory.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Json\Factory;

use Zalas\Toolbox\Tool\Command;
use Zalas\Toolbox\Tool\Command\FileDownloadCommand;

final class FileDownloadCommandFactory
{
    public static function import(array $command): Command
    {
        Assert::requireFields(['url', 'file'], $command, 'FileDownloadCommand');

        return new FileDownloadCommand($command['url'], $command['file']);
    }
}


================================================
FILE: src/Json/Factory/PharDownloadCommandFactory.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Json\Factory;

use Zalas\Toolbox\Tool\Command;
use Zalas\Toolbox\Tool\Command\PharDownloadCommand;

final class PharDownloadCommandFactory
{
    public static function import(array $command): Command
    {
        Assert::requireFields(['phar', 'bin'], $command, 'PharDownloadCommand');

        return new PharDownloadCommand($command['phar'], $command['bin']);
    }
}


================================================
FILE: src/Json/Factory/PhiveInstallCommandFactory.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Json\Factory;

use Zalas\Toolbox\Tool\Command;
use Zalas\Toolbox\Tool\Command\PhiveInstallCommand;

final class PhiveInstallCommandFactory
{
    public static function import(array $command): Command
    {
        Assert::requireFields(['alias', 'bin'], $command, 'PhiveInstallCommand');

        return new PhiveInstallCommand($command['alias'], $command['bin'], $command['sig'] ?? null);
    }
}


================================================
FILE: src/Json/Factory/ShCommandFactory.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Json\Factory;

use Zalas\Toolbox\Tool\Command;
use Zalas\Toolbox\Tool\Command\ShCommand;

final class ShCommandFactory
{
    public static function import(array $command): Command
    {
        Assert::requireFields(['command'], $command, 'ShCommand');

        return new ShCommand($command['command']);
    }
}


================================================
FILE: src/Json/Factory/ToolFactory.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Json\Factory;

use Zalas\Toolbox\Tool\Collection;
use Zalas\Toolbox\Tool\Command;
use Zalas\Toolbox\Tool\Command\MultiStepCommand;
use Zalas\Toolbox\Tool\Command\TestCommand;
use Zalas\Toolbox\Tool\Tool;

final class ToolFactory
{
    public static function import(array $tool): Tool
    {
        Assert::requireFields(['name', 'summary', 'website', 'command', 'test'], $tool, 'tool');

        return new Tool(
            $tool['name'],
            $tool['summary'],
            $tool['website'],
            $tool['tags'] ?? [],
            self::importCommand($tool),
            new TestCommand($tool['test'], $tool['name'])
        );
    }

    private static function importCommand(array $tool): Command
    {
        $commands = Collection::create([]);

        foreach ($tool['command'] as $type => $command) {
            $commands = $commands->merge(self::createCommands($type, $command));
        }

        if (0 === $commands->count()) {
            throw new \RuntimeException(\sprintf('No valid command defined for the tool: %s', \json_encode($tool)));
        }

        return 1 === $commands->count() ? $commands->toArray()[0] : new MultiStepCommand($commands);
    }

    private static function createCommands($type, $command): Collection
    {
        $factories = [
            'phar-download' => \sprintf('%s::import', PharDownloadCommandFactory::class),
            'file-download' => \sprintf('%s::import', FileDownloadCommandFactory::class),
            'box-build' => \sprintf('%s::import', BoxBuildCommandFactory::class),
            'composer-install' => \sprintf('%s::import', ComposerInstallCommandFactory::class),
            'phive-install' => \sprintf('%s::import', PhiveInstallCommandFactory::class),
            'composer-global-install' => \sprintf('%s::import', ComposerGlobalInstallCommandFactory::class),
            'composer-bin-plugin' => \sprintf('%s::import', ComposerBinPluginCommandFactory::class),
            'sh' => \sprintf('%s::import', ShCommandFactory::class),
        ];

        if (!isset($factories[$type])) {
            throw new \RuntimeException(\sprintf('Unrecognised command: "%s". Supported commands are: "%s".', $type, \implode(', ', \array_keys($factories))));
        }

        $command = !\is_numeric(\key($command)) ? [$command] : $command;

        return Collection::create(\array_map(function ($c) use ($type, $factories) {
            return $factories[$type]($c);
        }, $command));
    }
}


================================================
FILE: src/Json/JsonTools.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Json;

use InvalidArgumentException;
use RuntimeException;
use Zalas\Toolbox\Json\Factory\ToolFactory;
use Zalas\Toolbox\Tool\Collection;
use Zalas\Toolbox\Tool\Filter;
use Zalas\Toolbox\Tool\Tools;

final class JsonTools implements Tools
{
    /**
     * @var callable
     */
    private $resourceLocator;

    public function __construct(callable $resourceLocator)
    {
        $this->resourceLocator = $resourceLocator;
    }

    /**
     * @param Filter $filter
     * @return Collection
     */
    public function all(Filter $filter): Collection
    {
        return $this->loadTools()->filter($filter);
    }

    private function loadTools(): Collection
    {
        return \array_reduce($this->resources(), function (Collection $tools, string $resource): Collection {
            return $tools->merge(Collection::create(
                \array_map(\sprintf('%s::import', ToolFactory::class), $this->loadJson($resource))
            ));
        }, Collection::create([]));
    }

    private function loadJson(string $resource): array
    {
        $json = \json_decode(\file_get_contents($resource), true);

        if (!$json) {
            throw new RuntimeException(\sprintf('Failed to parse json: "%s"', $resource));
        }

        if (!isset($json['tools']) || !\is_array($json['tools'])) {
            throw new RuntimeException(\sprintf('Failed to find any tools in: "%s".', $resource));
        }

        return $json['tools'];
    }

    private function resources(): array
    {
        $resources = \call_user_func($this->resourceLocator);

        return \array_map(function (string $resource) {
            if (!\is_readable($resource)) {
                throw new InvalidArgumentException(\sprintf('Could not read the file: "%s".', $resource));
            }

            return $resource;
        }, $resources);
    }
}


================================================
FILE: src/Runner/ParametrisedRunner.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Runner;

use Zalas\Toolbox\Tool\Command;

final class ParametrisedRunner implements Runner
{
    private Runner $decoratedRunner;
    private array $parameters;

    public function __construct(Runner $decoratedRunner, array $parameters)
    {
        $this->decoratedRunner = $decoratedRunner;
        $this->parameters = $parameters;
    }

    public function run(Command $command): int
    {
        return $this->decoratedRunner->run(new class($command, $this->parameters) implements Command {
            private Command $command;
            private array $parameters;

            public function __construct(Command $command, array $parameters)
            {
                $this->command = $command;
                $this->parameters = $parameters;
            }

            public function __toString(): string
            {
                return \strtr((string) $this->command, $this->parameters);
            }
        });
    }
}


================================================
FILE: src/Runner/PassthruRunner.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Runner;

use Zalas\Toolbox\Tool\Command;

final class PassthruRunner implements Runner
{
    public function run(Command $command): int
    {
        \passthru((string) $command, $status);

        return $status;
    }
}


================================================
FILE: src/Runner/Runner.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Runner;

use Zalas\Toolbox\Tool\Command;

interface Runner
{
    public function run(Command $command): int;
}


================================================
FILE: src/Tool/Collection.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Tool;

use Countable;
use IteratorAggregate;
use Traversable;

class Collection implements IteratorAggregate, Countable
{
    private array $elements;

    private function __construct(array $elements)
    {
        $this->elements = $elements;
    }

    public static function create(array $elements): Collection
    {
        return new self($elements);
    }

    public function getIterator(): Traversable
    {
        yield from $this->elements;
    }

    public function merge(Collection $other): Collection
    {
        return self::create(\array_merge($this->elements, $other->elements));
    }

    public function filter(callable $f): Collection
    {
        return self::create(\array_values(\array_filter($this->elements, $f)));
    }

    public function map(callable $f): Collection
    {
        return self::create(\array_map($f, $this->elements));
    }

    public function reduce($initial, callable $param)
    {
        return \array_reduce($this->elements, $param, $initial);
    }

    public function sort(callable $f): Collection
    {
        $elements = $this->elements;
        \usort($elements, $f);

        return self::create($elements);
    }

    public function toArray(): array
    {
        return $this->elements;
    }

    public function count(): int
    {
        return \count($this->elements);
    }

    public function empty(): bool
    {
        return empty($this->elements);
    }
}


================================================
FILE: src/Tool/Command/BoxBuildCommand.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Tool\Command;

use Zalas\Toolbox\Tool\Command;

final class BoxBuildCommand implements Command
{
    private string $repository;
    private string $phar;
    private string $bin;
    private string $workDir;
    private ?string $version;

    public function __construct(string $repository, string $phar, string $bin, string $workDir, ?string $version = null)
    {
        $this->repository = $repository;
        $this->phar = $phar;
        $this->bin = $bin;
        $this->workDir = $workDir;
        $this->version = $version;
    }

    public function __toString(): string
    {
        return \sprintf(
            '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',
            $this->repository,
            $this->targetDir(),
            $this->targetDir(),
            $this->version ?? '$(git describe --tags $(git rev-list --tags --max-count=1) 2>/dev/null)',
            $this->phar,
            $this->bin,
            $this->bin,
            $this->targetDir()
        );
    }

    private function targetDir(): string
    {
        $targetDir = \preg_replace('#^.*/(.*?)(.git)?$#', '$1', $this->repository);

        return  \sprintf('%s/%s', $this->workDir, $targetDir !== $this->repository ? $targetDir : 'tmp');
    }
}


================================================
FILE: src/Tool/Command/ComposerBinPluginCommand.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Tool\Command;

use Zalas\Toolbox\Tool\Collection;
use Zalas\Toolbox\Tool\Command;

final class ComposerBinPluginCommand implements Command
{
    private string $package;

    private string $namespace;

    private Collection $links;

    public function __construct(string $package, string $namespace, Collection $links)
    {
        $this->package = $package;
        $this->namespace = $namespace;
        $this->links = $links;
    }

    public function __toString(): string
    {
        return \sprintf('composer global bin %s require --prefer-dist --update-no-dev -n %s%s', $this->namespace, $this->package, $this->linkCommand());
    }

    public function package(): string
    {
        return $this->package;
    }

    public function namespace(): string
    {
        return $this->namespace;
    }

    public function links(): Collection
    {
        return $this->links;
    }

    private function linkCommand(): string
    {
        return $this->links->reduce('', function (string $command, ComposerBinPluginLinkCommand $link) {
            return $command.' && '.$link;
        });
    }
}


================================================
FILE: src/Tool/Command/ComposerBinPluginLinkCommand.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Tool\Command;

use Zalas\Toolbox\Tool\Command;

final class ComposerBinPluginLinkCommand implements Command
{
    private string $source;
    private string $target;
    private string $namespace;

    public function __construct(string $source, string $target, string $namespace)
    {
        $this->source = $source;
        $this->target = $target;
        $this->namespace = $namespace;
    }

    public function __toString(): string
    {
        return \sprintf(
            'ln -sf ${COMPOSER_HOME:-"~/.composer"}/vendor-bin/%s/vendor/bin/%s %s',
            $this->namespace,
            $this->source,
            $this->target
        );
    }
}


================================================
FILE: src/Tool/Command/ComposerGlobalInstallCommand.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Tool\Command;

use Zalas\Toolbox\Tool\Command;

final class ComposerGlobalInstallCommand implements Command
{
    private string $package;

    public function __construct(string $package)
    {
        $this->package = $package;
    }

    public function __toString(): string
    {
        return \sprintf('composer global require --prefer-dist --update-no-dev -n %s', $this->package);
    }

    public function package(): string
    {
        return $this->package;
    }
}


================================================
FILE: src/Tool/Command/ComposerGlobalMultiInstallCommand.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Tool\Command;

use InvalidArgumentException;
use Zalas\Toolbox\Tool\Collection;
use Zalas\Toolbox\Tool\Command;

final class ComposerGlobalMultiInstallCommand implements Command
{
    private Collection $commands;

    public function __construct(Collection $commands)
    {
        if ($commands->empty()) {
            throw new InvalidArgumentException('Collection of composer global install commands cannot be empty.');
        }

        $this->commands = $commands->filter(function (ComposerGlobalInstallCommand $c) {
            return $c;
        });
    }

    public function __toString(): string
    {
        $packages = \implode(' ', \array_map(function (ComposerGlobalInstallCommand $command) {
            return $command->package();
        }, $this->commands->toArray()));

        return \sprintf('composer global require --prefer-dist --update-no-dev -n %s', $packages);
    }
}


================================================
FILE: src/Tool/Command/ComposerInstallCommand.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Tool\Command;

use Zalas\Toolbox\Tool\Command;

final class ComposerInstallCommand implements Command
{
    private string $repository;
    private string $targetDir;
    private ?string $version;

    public function __construct(string $repository, string $targetDir, ?string $version = null)
    {
        $this->repository = $repository;
        $this->targetDir = $targetDir;
        $this->version = $version;
    }

    public function __toString(): string
    {
        return \sprintf(
            'git clone %s %s && cd %s && git checkout %s && composer install --no-dev --prefer-dist -n',
            $this->repository,
            $this->targetDir,
            $this->targetDir,
            $this->version ?? '$(git describe --tags $(git rev-list --tags --max-count=1) 2>/dev/null)'
        );
    }
}


================================================
FILE: src/Tool/Command/FileDownloadCommand.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Tool\Command;

use Zalas\Toolbox\Tool\Command;

final class FileDownloadCommand implements Command
{
    private string $url;
    private string $file;

    public function __construct(string $url, string $file)
    {
        $this->url = $url;
        $this->file = $file;
    }

    public function __toString(): string
    {
        return \sprintf('curl -Ls -w %%{filename_effective}\'\n\' %s -o %s', $this->url, $this->file);
    }
}


================================================
FILE: src/Tool/Command/MultiStepCommand.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Tool\Command;

use InvalidArgumentException;
use Zalas\Toolbox\Tool\Collection;
use Zalas\Toolbox\Tool\Command;

final class MultiStepCommand implements Command
{
    private Collection $commands;
    private mixed $glue;

    public function __construct(Collection $commands, $glue = ' && ')
    {
        if ($commands->empty()) {
            throw new InvalidArgumentException('Collection of commands cannot be empty.');
        }

        $this->commands = $commands->filter(function (Command $c) {
            return $c;
        });
        $this->glue = $glue;
    }

    public function __toString(): string
    {
        return \implode($this->glue, $this->commands->toArray());
    }
}


================================================
FILE: src/Tool/Command/OptimisedComposerBinPluginCommand.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Tool\Command;

use InvalidArgumentException;
use Zalas\Toolbox\Tool\Collection;
use Zalas\Toolbox\Tool\Command;

final class OptimisedComposerBinPluginCommand implements Command
{
    private Collection $commands;

    public function __construct(Collection $commands)
    {
        if ($commands->empty()) {
            throw new InvalidArgumentException('Collection of composer bin plugin commands cannot be empty.');
        }

        $this->commands = $commands->filter(function (ComposerBinPluginCommand $command) {
            return $command;
        });
    }

    public function __toString(): string
    {
        return \implode(' && ', \array_merge($this->commandsToRun($this->packagesGroupedByNamespace()), $this->linksToCreate()));
    }

    private function packagesGroupedByNamespace(): array
    {
        return $this->commands->reduce([], function (array $packages, ComposerBinPluginCommand $command) {
            $packages[$command->namespace()][] = $command->package();

            return $packages;
        });
    }

    private function commandToRun(string $namespace, array $packages): string
    {
        return \sprintf('composer global bin %s require --prefer-dist --update-no-dev -n %s', $namespace, \implode(' ', $packages));
    }

    private function commandsToRun(array $packagesGrouped): array
    {
        return \array_map([$this, 'commandToRun'], \array_keys($packagesGrouped), $packagesGrouped);
    }

    private function linksToCreate(): array
    {
        return $this->commands
            ->filter(function (ComposerBinPluginCommand $command) {
                return !$command->links()->empty();
            })
            ->map(function (ComposerBinPluginCommand $command) {
                return $command->links()->reduce('', function (string $command, ComposerBinPluginLinkCommand $link) {
                    return !empty($command) ? $command.' && '.$link : $link;
                });
            })
            ->toArray();
    }
}


================================================
FILE: src/Tool/Command/PharDownloadCommand.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Tool\Command;

use Zalas\Toolbox\Tool\Command;

final class PharDownloadCommand implements Command
{
    private string $phar;
    private string $bin;

    public function __construct(string $phar, string $bin)
    {
        $this->phar = $phar;
        $this->bin = $bin;
    }

    public function __toString(): string
    {
        return \sprintf('curl -Ls -w %%{filename_effective}\'\n\' %s -o %s && chmod +x %s', $this->phar, $this->bin, $this->bin);
    }
}


================================================
FILE: src/Tool/Command/PhiveInstallCommand.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Tool\Command;

use Zalas\Toolbox\Tool\Command;

final class PhiveInstallCommand implements Command
{
    private string $alias;
    private string $bin;
    private ?string $sig;

    public function __construct(string $alias, string $bin, ?string $sig = null)
    {
        $this->alias = $alias;
        $this->bin = $bin;
        $this->sig = $sig;
    }

    public function __toString(): string
    {
        $home = \sprintf('%s/.phive', \dirname($this->bin));
        $tmp = \sprintf('%s/tmp/%s', $home, \md5($this->alias));

        return \sprintf(
            'phive --no-progress --home %s install %s %s -t %s && mv %s/* %s',
            $home,
            $this->sig ? '--trust-gpg-keys '.$this->sig : '--force-accept-unsigned',
            $this->alias,
            $tmp,
            $tmp,
            $this->bin
        );
    }
}


================================================
FILE: src/Tool/Command/ShCommand.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Tool\Command;

use Zalas\Toolbox\Tool\Command;

final class ShCommand implements Command
{
    private string $command;

    public function __construct(string $command)
    {
        $this->command = $command;
    }

    public function __toString(): string
    {
        return $this->command;
    }
}


================================================
FILE: src/Tool/Command/TestCommand.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Tool\Command;

use Zalas\Toolbox\Tool\Command;

final class TestCommand implements Command
{
    private string $command;
    private string $name;

    public function __construct(string $command, string $name)
    {
        $this->command = $command;
        $this->name = $name;
    }

    public function __toString(): string
    {
        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);
    }
}


================================================
FILE: src/Tool/Command.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Tool;

interface Command
{
    public function __toString(): string;
}


================================================
FILE: src/Tool/Filter.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Tool;

class Filter
{
    /**
     * @var string[]
     */
    private array $excludedTags;

    /**
     * @var string[]
     */
    private array $tags;

    /**
     * @param string[] $excludedTags
     * @param string[] $tags
     */
    public function __construct(array $excludedTags, array $tags)
    {
        $this->excludedTags = $excludedTags;
        $this->tags = $tags;
    }

    public function __invoke(Tool $tool): bool
    {
        return $this->excludedTags === \array_diff($this->excludedTags, $tool->tags())
            && (empty($this->tags) || \array_intersect($this->tags, $tool->tags()));
    }
}


================================================
FILE: src/Tool/Tool.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Tool;

class Tool
{
    private string $name;
    private string $summary;
    private string $website;
    private Command $command;
    private Command $testCommand;
    private array $tags;

    public function __construct(string $name, string $summary, string $website, array $tags, Command $command, Command $testCommand)
    {
        $this->name = $name;
        $this->summary = $summary;
        $this->website = $website;
        $this->tags = \array_map(function (string $tag) {
            return $tag;
        }, $tags);
        $this->command = $command;
        $this->testCommand = $testCommand;
    }

    public function name(): string
    {
        return $this->name;
    }

    public function summary(): string
    {
        return $this->summary;
    }

    public function website(): string
    {
        return $this->website;
    }

    public function command(): Command
    {
        return $this->command;
    }

    public function testCommand(): Command
    {
        return $this->testCommand;
    }

    /**
     * @return array|string[]
     */
    public function tags(): array
    {
        return $this->tags;
    }
}


================================================
FILE: src/Tool/Tools.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Tool;

use RuntimeException;

interface Tools
{
    /**
     * @param Filter $filter
     * @return Collection
     * @throws RuntimeException in case tools cannot be loaded
     */
    public function all(Filter $filter): Collection;
}


================================================
FILE: src/UseCase/InstallTools.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\UseCase;

use Closure;
use Zalas\Toolbox\Tool\Collection;
use Zalas\Toolbox\Tool\Command;
use Zalas\Toolbox\Tool\Command\BoxBuildCommand;
use Zalas\Toolbox\Tool\Command\ComposerBinPluginCommand;
use Zalas\Toolbox\Tool\Command\ComposerGlobalInstallCommand;
use Zalas\Toolbox\Tool\Command\ComposerGlobalMultiInstallCommand;
use Zalas\Toolbox\Tool\Command\ComposerInstallCommand;
use Zalas\Toolbox\Tool\Command\FileDownloadCommand;
use Zalas\Toolbox\Tool\Command\MultiStepCommand;
use Zalas\Toolbox\Tool\Command\OptimisedComposerBinPluginCommand;
use Zalas\Toolbox\Tool\Command\PharDownloadCommand;
use Zalas\Toolbox\Tool\Command\PhiveInstallCommand;
use Zalas\Toolbox\Tool\Command\ShCommand;
use Zalas\Toolbox\Tool\Filter;
use Zalas\Toolbox\Tool\Tool;
use Zalas\Toolbox\Tool\Tools;

class InstallTools
{
    public const PRE_INSTALLATION_TAG = 'pre-installation';

    private Tools $tools;

    public function __construct(Tools $tools)
    {
        $this->tools = $tools;
    }

    public function __invoke(Filter $filter): Command
    {
        $tools = $this->tools->all($filter);
        $installationCommands = $this->installationCommands($tools);
        $commandFilter = $this->commandFilter($this->toolCommands($tools));

        return new MultiStepCommand(
            $installationCommands
                ->merge($commandFilter(ShCommand::class))
                ->merge($commandFilter(FileDownloadCommand::class))
                ->merge($commandFilter(PharDownloadCommand::class))
                ->merge($commandFilter(PhiveInstallCommand::class))
                ->merge($commandFilter(MultiStepCommand::class))
                ->merge($this->groupComposerGlobalInstallCommands($commandFilter(ComposerGlobalInstallCommand::class)))
                ->merge($this->groupComposerBinPluginCommands($commandFilter(ComposerBinPluginCommand::class)))
                ->merge($commandFilter(ComposerInstallCommand::class))
                ->merge($commandFilter(BoxBuildCommand::class))
        );
    }

    private function commandFilter(Collection $commands): Closure
    {
        return function ($type) use ($commands) {
            return $commands->filter(function (Command $command) use ($type) {
                return $command instanceof $type;
            });
        };
    }

    private function installationCommands(Collection $tools): Collection
    {
        return $tools->filter(function (Tool $tool) {
            return \in_array(self::PRE_INSTALLATION_TAG, $tool->tags());
        })->map(function (Tool $tool) {
            return $tool->command();
        });
    }

    private function toolCommands(Collection $tools): Collection
    {
        return $tools->filter(function (Tool $tool) {
            return !\in_array(self::PRE_INSTALLATION_TAG, $tool->tags());
        })->map(function (Tool $tool) {
            return $tool->command();
        });
    }

    private function groupComposerGlobalInstallCommands(Collection $commands): Collection
    {
        $commands = $commands->empty() ? [] : [new ComposerGlobalMultiInstallCommand($commands)];

        return Collection::create($commands);
    }

    private function groupComposerBinPluginCommands(Collection $commands): Collection
    {
        $commands = $commands->empty() ? [] : [new OptimisedComposerBinPluginCommand($commands)];

        return Collection::create($commands);
    }
}


================================================
FILE: src/UseCase/ListTools.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\UseCase;

use Zalas\Toolbox\Tool\Collection;
use Zalas\Toolbox\Tool\Filter;
use Zalas\Toolbox\Tool\Tools;

class ListTools
{
    private Tools $tools;

    public function __construct(Tools $tools)
    {
        $this->tools = $tools;
    }

    public function __invoke(Filter $filter): Collection
    {
        return $this->tools->all($filter);
    }
}


================================================
FILE: src/UseCase/TestTools.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\UseCase;

use Zalas\Toolbox\Tool\Command;
use Zalas\Toolbox\Tool\Command\MultiStepCommand;
use Zalas\Toolbox\Tool\Filter;
use Zalas\Toolbox\Tool\Tool;
use Zalas\Toolbox\Tool\Tools;

class TestTools
{
    private Tools $tools;

    public function __construct(Tools $tools)
    {
        $this->tools = $tools;
    }

    public function __invoke(Filter $filter): Command
    {
        return new MultiStepCommand(
            $this->tools->all($filter)->map(function (Tool $tool) {
                return $tool->testCommand();
            })
        );
    }
}


================================================
FILE: tests/Cli/ApplicationTest.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Tests\Cli;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Application as CliApplication;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Output\OutputInterface;
use Zalas\PHPUnit\Globals\Attribute\Putenv;
use Zalas\Toolbox\Cli\Application;
use Zalas\Toolbox\Cli\Command\InstallCommand;
use Zalas\Toolbox\Cli\Command\ListCommand;
use Zalas\Toolbox\Cli\ServiceContainer;

class ApplicationTest extends TestCase
{
    private const VERSION = 'test';

    private Application $app;

    protected function setUp(): void
    {
        $container = $this->createStub(ServiceContainer::class);
        $this->app = new Application(self::VERSION, $container);
    }

    public function test_it_is_a_cli_application()
    {
        $this->assertInstanceOf(CliApplication::class, $this->app);
    }

    public function test_it_defines_the_app_name_and_version()
    {
        $this->assertSame('toolbox', $this->app->getName());
        $this->assertSame(self::VERSION, $this->app->getVersion());
    }

    public function test_it_defines_tools_option()
    {
        $this->assertTrue($this->app->getDefinition()->hasOption('tools'));
        $this->assertEquals(
            [
                \realpath(__DIR__.'/../../src/Cli/').'/../../resources/pre-installation.json',
                \realpath(__DIR__.'/../../src/Cli/').'/../../resources/architecture.json',
                \realpath(__DIR__.'/../../src/Cli/').'/../../resources/checkstyle.json',
                \realpath(__DIR__.'/../../src/Cli/').'/../../resources/compatibility.json',
                \realpath(__DIR__.'/../../src/Cli/').'/../../resources/composer.json',
                \realpath(__DIR__.'/../../src/Cli/').'/../../resources/deprecation.json',
                \realpath(__DIR__.'/../../src/Cli/').'/../../resources/documentation.json',
                \realpath(__DIR__.'/../../src/Cli/').'/../../resources/linting.json',
                \realpath(__DIR__.'/../../src/Cli/').'/../../resources/metrics.json',
                \realpath(__DIR__.'/../../src/Cli/').'/../../resources/phpcs.json',
                \realpath(__DIR__.'/../../src/Cli/').'/../../resources/phpstan.json',
                \realpath(__DIR__.'/../../src/Cli/').'/../../resources/psalm.json',
                \realpath(__DIR__.'/../../src/Cli/').'/../../resources/refactoring.json',
                \realpath(__DIR__.'/../../src/Cli/').'/../../resources/security.json',
                \realpath(__DIR__.'/../../src/Cli/').'/../../resources/test.json',
                \realpath(__DIR__.'/../../src/Cli/').'/../../resources/tools.json'
            ],
            $this->app->getDefinition()->getOption('tools')->getDefault()
        );
    }

    #[Putenv('TOOLBOX_JSON', 'resources/pre.json,resources/tools.json')]
    public function test_it_takes_the_tools_option_default_from_environment_if_present()
    {
        $this->assertSame(['resources/pre.json', 'resources/tools.json'], $this->app->getDefinition()->getOption('tools')->getDefault());
    }

    #[Putenv('TOOLBOX_JSON', 'resources/pre.json , resources/tools.json')]
    public function test_it_trims_the_tools_option()
    {
        $this->assertSame(['resources/pre.json', 'resources/tools.json'], $this->app->getDefinition()->getOption('tools')->getDefault());
    }

    /**
     * @group integration
     */
    public function test_it_allows_to_override_tools_location()
    {
        $app = new Application(self::VERSION, new ServiceContainer());
        $result = $app->doRun(
            new ArrayInput([
                'command' => ListCommand::NAME,
                '--tools' => [__DIR__.'/../resources/tools.json'],
                '--no-interaction' => true,
            ]),
            new NullOutput()
        );

        $this->assertSame(0, $result);
    }

    /**
     * @group integration
     */
    public function test_it_runs_the_command_in_dry_run_mode()
    {
        $output = $this->givenOutputThatExpectsMessageWritten('composer global bin phpstan require');

        $app = new Application(self::VERSION, new ServiceContainer());
        $app->doRun(
            new ArrayInput([
                'command' => InstallCommand::NAME,
                '--dry-run' => true,
                '--tools' => [__DIR__.'/../resources/tools.json'],
                '--no-interaction' => true,
            ]),
            $output
        );
    }

    public function givenOutputThatExpectsMessageWritten(string $message): OutputInterface
    {
        $output = $this->createMock(OutputInterface::class);
        $output->expects(self::once())
            ->method('writeln')
            ->with(self::stringContains($message));

        return $output;
    }
}


================================================
FILE: tests/Cli/Command/InstallCommandTest.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Tests\Cli\Command;

use PHPUnit\Framework\MockObject\Stub;
use Zalas\PHPUnit\Globals\Attribute\Putenv;
use Zalas\Toolbox\Cli\Command\InstallCommand;
use Zalas\Toolbox\Runner\Runner;
use Zalas\Toolbox\Tool\Command;
use Zalas\Toolbox\Tool\Command\ShCommand;
use Zalas\Toolbox\Tool\Filter;
use Zalas\Toolbox\UseCase\InstallTools;

class InstallCommandTest extends ToolboxCommandTestCase
{
    protected const CLI_COMMAND_NAME = InstallCommand::NAME;

    private Runner|Stub $runner;

    private InstallTools|Stub $useCase;

    protected function setUp(): void
    {
        $this->runner = $this->createStub(Runner::class);
        $this->useCase = $this->createStub(InstallTools::class);

        parent::setUp();
    }

    public function test_it_runs_the_install_tools_use_case()
    {
        $command = $this->createCommand();
        $this->useCase->method('__invoke')->willReturn($command);
        $this->runner->method('run')->with($command)->willReturn(0);

        $tester = $this->executeCliCommand();

        $this->assertSame(0, $tester->getStatusCode());
    }

    public function test_it_returns_the_status_code_of_the_run()
    {
        $this->useCase->method('__invoke')->willReturn($this->createCommand());
        $this->runner->method('run')->willReturn(1);

        $tester = $this->executeCliCommand();

        $this->assertSame(1, $tester->getStatusCode());
    }

    public function test_it_filters_by_tags()
    {
        $this->useCase
            ->method('__invoke')
            ->with(new Filter(['foo'], ['bar']))
            ->willReturn($this->createCommand());
        $this->runner->method('run')->willReturn(0);

        $tester = $this->executeCliCommand(['--exclude-tag' => ['foo'], '--tag' => ['bar']]);

        $this->assertSame(0, $tester->getStatusCode());
    }

    public function test_it_defines_dry_run_option()
    {
        $this->assertTrue($this->cliCommand()->getDefinition()->hasOption('dry-run'));
    }

    public function test_it_defines_target_dir_option()
    {
        $this->assertTrue($this->cliCommand()->getDefinition()->hasOption('target-dir'));
        $this->assertSame('/usr/local/bin', $this->cliCommand()->getDefinition()->getOption('target-dir')->getDefault());
    }

    #[Putenv('TOOLBOX_TARGET_DIR', '/tmp')]
    public function test_it_takes_the_target_dir_option_default_from_environment_if_present()
    {
        $this->assertSame('/tmp', $this->cliCommand()->getDefinition()->getOption('target-dir')->getDefault());
    }

    public function test_it_defines_exclude_tag_option()
    {
        $this->assertTrue($this->cliCommand()->getDefinition()->hasOption('exclude-tag'));
        $this->assertSame([], $this->cliCommand()->getDefinition()->getOption('exclude-tag')->getDefault());
    }

    #[Putenv('TOOLBOX_EXCLUDED_TAGS', 'foo,bar,baz')]
    public function test_it_takes_the_excluded_tag_option_default_from_environment_if_present()
    {
        $this->assertSame(['foo', 'bar', 'baz'], $this->cliCommand()->getDefinition()->getOption('exclude-tag')->getDefault());
    }

    public function test_it_defines_tag_option()
    {
        $this->assertTrue($this->cliCommand()->getDefinition()->hasOption('tag'));
        $this->assertSame([], $this->cliCommand()->getDefinition()->getOption('tag')->getDefault());
    }

    #[Putenv('TOOLBOX_TAGS', 'foo,bar,baz')]
    public function test_it_takes_the_tag_option_default_from_environment_if_present()
    {
        $this->assertSame(['foo', 'bar', 'baz'], $this->cliCommand()->getDefinition()->getOption('tag')->getDefault());
    }

    protected function getContainerTestDoubles(): array
    {
        return [
            Runner::class => $this->runner,
            InstallTools::class => $this->useCase,
        ];
    }

    private function createCommand(): Command
    {
        return new ShCommand('echo "foo"');
    }
}


================================================
FILE: tests/Cli/Command/ListCommandTest.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Tests\Cli\Command;

use PHPUnit\Framework\MockObject\Stub;
use Zalas\PHPUnit\Globals\Attribute\Putenv;
use Zalas\Toolbox\Cli\Command\ListCommand;
use Zalas\Toolbox\Tool\Collection;
use Zalas\Toolbox\Tool\Command\ShCommand;
use Zalas\Toolbox\Tool\Command\TestCommand;
use Zalas\Toolbox\Tool\Filter;
use Zalas\Toolbox\Tool\Tool;
use Zalas\Toolbox\UseCase\ListTools;

class ListCommandTest extends ToolboxCommandTestCase
{
    protected const CLI_COMMAND_NAME = ListCommand::NAME;

    private ListTools|Stub $useCase;

    protected function setUp(): void
    {
        $this->useCase = $this->createStub(ListTools::class);

        parent::setUp();
    }

    public function test_it_runs_the_list_tools_use_case()
    {
        $this->useCase->method('__invoke')->willReturn(Collection::create([
            $this->createTool('Behat', 'Tests business expectations', 'http://behat.org'),
        ]));

        $tester = $this->executeCliCommand();

        $this->assertSame(0, $tester->getStatusCode());
        $this->assertMatchesRegularExpression('#Available tools#i', $tester->getDisplay());
        $this->assertMatchesRegularExpression('#Behat.*?Tests business expectations.*?http://behat.org#smi', $tester->getDisplay());
    }

    public function test_it_filters_by_tags()
    {
        $this->useCase->method('__invoke')
            ->with(new Filter(['foo'], ['bar']))
            ->willReturn(Collection::create([
                 $this->createTool('Behat', 'Tests business expectations', 'http://behat.org'),
            ]));

        $tester = $this->executeCliCommand(['--exclude-tag' => ['foo'], '--tag' => ['bar']]);

        $this->assertSame(0, $tester->getStatusCode());
    }

    public function test_it_defines_exclude_tag_option()
    {
        $this->assertTrue($this->cliCommand()->getDefinition()->hasOption('exclude-tag'));
        $this->assertSame([], $this->cliCommand()->getDefinition()->getOption('exclude-tag')->getDefault());
    }

    #[Putenv('TOOLBOX_EXCLUDED_TAGS', 'foo,bar,baz')]
    public function test_it_takes_the_excluded_tag_option_default_from_environment_if_present()
    {
        $this->assertSame(['foo', 'bar', 'baz'], $this->cliCommand()->getDefinition()->getOption('exclude-tag')->getDefault());
    }

    public function test_it_defines_tag_option()
    {
        $this->assertTrue($this->cliCommand()->getDefinition()->hasOption('tag'));
    }

    #[Putenv('TOOLBOX_TAGS', 'foo,bar,baz')]
    public function test_it_takes_the_tag_option_default_from_environment_if_present()
    {
        $this->assertSame(['foo', 'bar', 'baz'], $this->cliCommand()->getDefinition()->getOption('tag')->getDefault());
    }

    protected function getContainerTestDoubles(): array
    {
        return [
            ListTools::class => $this->useCase,
        ];
    }

    private function createTool(string $name, string $summary, string $website): Tool
    {
        return new Tool(
            $name,
            $summary,
            $website,
            [],
            new ShCommand('any command'),
            new TestCommand('any test command', 'any')
        );
    }
}


================================================
FILE: tests/Cli/Command/TestCommandTest.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Tests\Cli\Command;

use PHPUnit\Framework\MockObject\Stub;
use Zalas\PHPUnit\Globals\Attribute\Putenv;
use Zalas\Toolbox\Cli\Command\TestCommand;
use Zalas\Toolbox\Runner\Runner;
use Zalas\Toolbox\Tool\Command;
use Zalas\Toolbox\Tool\Command\ShCommand;
use Zalas\Toolbox\Tool\Filter;
use Zalas\Toolbox\UseCase\TestTools;

class TestCommandTest extends ToolboxCommandTestCase
{
    protected const CLI_COMMAND_NAME = TestCommand::NAME;

    private Runner|Stub $runner;

    private TestTools|Stub $useCase;

    protected function setUp(): void
    {
        $this->runner = $this->createStub(Runner::class);
        $this->useCase = $this->createStub(TestTools::class);

        parent::setUp();
    }

    public function test_it_runs_the_test_tools_use_case()
    {
        $command = $this->createCommand();
        $this->useCase->method('__invoke')->willReturn($command);
        $this->runner->method('run')->with($command)->willReturn(0);

        $tester = $this->executeCliCommand();

        $this->assertSame(0, $tester->getStatusCode());
    }

    public function test_it_returns_the_status_code_of_the_run()
    {
        $this->useCase->method('__invoke')->willReturn($this->createCommand());
        $this->runner->method('run')->willReturn(1);

        $tester = $this->executeCliCommand();

        $this->assertSame(1, $tester->getStatusCode());
    }

    public function test_it_filters_by_tags()
    {
        $this->useCase->method('__invoke')->with(new Filter(['foo'], ['bar']))->willReturn($this->createCommand());
        $this->runner->method('run')->willReturn(0);

        $tester = $this->executeCliCommand(['--exclude-tag' => ['foo'], '--tag' => ['bar']]);

        $this->assertSame(0, $tester->getStatusCode());
    }

    public function test_it_defines_dry_run_option()
    {
        $this->assertTrue($this->cliCommand()->getDefinition()->hasOption('dry-run'));
    }

    public function test_it_defines_target_dir_option()
    {
        $this->assertSame('/usr/local/bin', $this->cliCommand()->getDefinition()->getOption('target-dir')->getDefault());
        $this->assertTrue($this->cliCommand()->getDefinition()->hasOption('target-dir'));
    }

    #[Putenv('TOOLBOX_TARGET_DIR', '/tmp')]
    public function test_it_takes_the_target_dir_option_default_from_environment_if_present()
    {
        $this->assertSame('/tmp', $this->cliCommand()->getDefinition()->getOption('target-dir')->getDefault());
    }

    public function test_it_defines_exclude_tag_option()
    {
        $this->assertTrue($this->cliCommand()->getDefinition()->hasOption('exclude-tag'));
        $this->assertSame([], $this->cliCommand()->getDefinition()->getOption('exclude-tag')->getDefault());
    }

    #[Putenv('TOOLBOX_EXCLUDED_TAGS', 'foo,bar,baz')]
    public function test_it_takes_the_excluded_tag_option_default_from_environment_if_present()
    {
        $this->assertSame(['foo', 'bar', 'baz'], $this->cliCommand()->getDefinition()->getOption('exclude-tag')->getDefault());
    }

    public function test_it_defines_tag_option()
    {
        $this->assertTrue($this->cliCommand()->getDefinition()->hasOption('tag'));
    }

    #[Putenv('TOOLBOX_TAGS', 'foo,bar,baz')]
    public function test_it_takes_the_tag_option_default_from_environment_if_present()
    {
        $this->assertSame(['foo', 'bar', 'baz'], $this->cliCommand()->getDefinition()->getOption('tag')->getDefault());
    }

    protected function getContainerTestDoubles(): array
    {
        return [
            Runner::class => $this->runner,
            TestTools::class => $this->useCase,
        ];
    }

    private function createCommand(): Command
    {
        return new ShCommand('true');
    }
}


================================================
FILE: tests/Cli/Command/ToolboxCommandTestCase.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Tests\Cli\Command;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Tester\CommandTester;
use Zalas\Toolbox\Cli\Application;
use Zalas\Toolbox\Cli\ServiceContainer;

abstract class ToolboxCommandTestCase extends TestCase
{
    protected const CLI_COMMAND_NAME = '';

    protected Application $app;

    protected function setUp(): void
    {
        $this->app = new Application('test', $this->createServiceContainer());
    }

    public function test_it_provides_help()
    {
        $this->assertNotEmpty($this->cliCommand()->getDescription());
    }

    protected function getContainerTestDoubles(): array
    {
        return [];
    }

    protected function executeCliCommand(array $input = []): CommandTester
    {
        $tester = new CommandTester($this->cliCommand());
        $tester->execute($input);

        return $tester;
    }

    protected function cliCommand(): Command
    {
        return $this->app->find(static::CLI_COMMAND_NAME);
    }

    private function createServiceContainer(): ServiceContainer
    {
        return new class($this->getContainerTestDoubles()) extends ServiceContainer {
            private array $services;

            public function __construct(array $services)
            {
                $this->services = $services;
            }

            public function get($id)
            {
                if (isset($this->services[$id])) {
                    return $this->services[$id];
                }

                return parent::get($id);
            }
        };
    }
}


================================================
FILE: tests/Cli/Runner/DryRunnerTest.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Tests\Cli\Runner;

use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Output\OutputInterface;
use Zalas\Toolbox\Cli\Runner\DryRunner;
use Zalas\Toolbox\Runner\Runner;
use Zalas\Toolbox\Tool\Command;

class DryRunnerTest extends TestCase
{
    private DryRunner $runner;

    private OutputInterface|MockObject $out;

    protected function setUp(): void
    {
        $this->out = $this->createMock(OutputInterface::class);
        $this->runner = new DryRunner($this->out);
    }

    public function test_it_is_a_runner()
    {
        $this->assertInstanceOf(Runner::class, $this->runner);
    }

    public function test_it_sends_the_command_to_the_output()
    {
        $this->out->expects(self::once())
            ->method('writeln')
            ->with('echo "Foo"');

        $result = $this->runner->run(new class implements Command {
            public function __toString(): string
            {
                return 'echo "Foo"';
            }
        });

        $this->assertSame(0, $result);
    }
}


================================================
FILE: tests/Cli/ServiceContainer/LazyRunnerTest.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Tests\Cli\ServiceContainer;

use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Zalas\Toolbox\Cli\ServiceContainer\LazyRunner;
use Zalas\Toolbox\Cli\ServiceContainer\RunnerFactory;
use Zalas\Toolbox\Runner\Runner;
use Zalas\Toolbox\Tool\Command;

class LazyRunnerTest extends TestCase
{
    private LazyRunner $lazyRunner;

    private RunnerFactory|MockObject $factory;

    protected function setUp(): void
    {
        $this->factory = $this->createMock(RunnerFactory::class);

        $this->lazyRunner = new LazyRunner($this->factory);
    }

    public function test_it_is_a_runner()
    {
        $this->assertInstanceOf(Runner::class, $this->lazyRunner);
    }

    public function test_it_returns_status_code_of_returned_by_the_created_runner()
    {
        $command = $this->command();

        $runner = $this->givenRunner(command: $command, result: 1);
        $this->givenFactoryCreates($runner);

        $this->assertSame(1, $this->lazyRunner->run($command));
    }

    public function test_it_only_initializes_the_runner_once()
    {
        $command = $this->command();

        $runner = $this->givenRunner($command, 0);

        $this->factory
            ->expects(self::once())
            ->method('createRunner')
            ->willReturn($runner);

        $this->lazyRunner->run($command);
        $this->lazyRunner->run($command);
    }

    public function givenRunner(Command $command, int $result): Runner
    {
        $runner = $this->createStub(Runner::class);
        $runner->method('run')->with($command)->willReturn($result);

        return $runner;
    }

    private function command(): Command
    {
        return new Command\ShCommand('any command');
    }

    private function givenFactoryCreates(Runner $runner): void
    {
        $this->factory->method('createRunner')->willReturn($runner);
    }
}


================================================
FILE: tests/Cli/ServiceContainer/RunnerFactoryTest.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Tests\Cli\ServiceContainer;

use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Zalas\Toolbox\Cli\Runner\DryRunner;
use Zalas\Toolbox\Cli\ServiceContainer\RunnerFactory;
use Zalas\Toolbox\Runner\ParametrisedRunner;
use Zalas\Toolbox\Runner\PassthruRunner;
use Zalas\Toolbox\Tool\Command;

class RunnerFactoryTest extends TestCase
{
    private RunnerFactory $runnerFactory;

    private InputInterface $input;

    private OutputInterface|MockObject $output;

    protected function setUp(): void
    {
        $this->input = $this->givenInput([]);
        $this->output = $this->createMock(OutputInterface::class);

        $container = new class([ InputInterface::class => &$this->input, OutputInterface::class => &$this->output, ]) implements ContainerInterface {

            public function __construct(private readonly array $services)
            {
            }

            public function get(string $id)
            {
                return $this->services[$id];
            }

            public function has(string $id): bool
            {
                return isset($this->services[$id]);
            }
        };

        $this->runnerFactory = new RunnerFactory($container);
    }

    public function test_it_creates_the_passthru_runner_by_default()
    {
        $runner = $this->runnerFactory->createRunner();

        $this->assertInstanceOf(PassthruRunner::class, $runner);
    }

    public function test_it_creates_the_dry_runner_if_dry_run_option_is_passed()
    {
        $this->givenInput(['--dry-run' => true]);

        $runner = $this->runnerFactory->createRunner();

        $this->assertInstanceOf(DryRunner::class, $runner);
    }

    public function test_it_creates_the_parametrised_runner_if_target_dir_option_is_present()
    {
        $this->givenInput(['--target-dir' => '/usr/local/bin']);

        $runner = $this->runnerFactory->createRunner();

        $this->assertInstanceOf(ParametrisedRunner::class, $runner);
    }

    public function test_the_parametrised_runner_includes_the_target_dir_parameter()
    {
        $this->givenInput(['--target-dir' => '/usr/local/bin', '--dry-run' => true]);

        $this->output->expects(self::once())->method('writeln')->with('ls /usr/local/bin');

        $runner = $this->runnerFactory->createRunner();

        $runner->run(new class implements Command {
            public function __toString(): string
            {
                return 'ls %target-dir%';
            }
        });
    }

    public function test_it_throws_an_exception_if_target_dir_does_not_exist()
    {
        $this->expectException(ContainerExceptionInterface::class);

        $this->givenInput(['--target-dir' => '/foo/bar/baz']);

        $this->runnerFactory->createRunner();
    }

    public function test_it_uses_the_real_path_as_target_dir()
    {
        $this->givenInput(['--target-dir' => __DIR__.'/../../../bin', '--dry-run' => true]);

        $this->output->expects(self::once())->method('writeln')->with(\sprintf('ls %s', \realpath(__DIR__.'/../../../bin')));

        $runner = $this->runnerFactory->createRunner();
        $runner->run(new class implements Command {
            public function __toString(): string
            {
                return 'ls %target-dir%';
            }
        });
    }

    private function givenInput(array $parameters): InputInterface
    {
        $this->input = new ArrayInput($parameters, new InputDefinition(\array_filter([
            new InputOption('dry-run', null, InputOption::VALUE_NONE),
            isset($parameters['--target-dir']) ? new InputOption('target-dir', null, InputOption::VALUE_REQUIRED) : null,
        ])));

        return $this->input;
    }
}


================================================
FILE: tests/Cli/ServiceContainerTest.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Tests\Cli;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Zalas\Toolbox\Cli\Command\InstallCommand;
use Zalas\Toolbox\Cli\Command\ListCommand;
use Zalas\Toolbox\Cli\Command\TestCommand;
use Zalas\Toolbox\Cli\ServiceContainer;
use Zalas\Toolbox\Cli\ServiceContainer\LazyRunner;
use Zalas\Toolbox\Runner\Runner;

class ServiceContainerTest extends TestCase
{
    private ServiceContainer $container;

    protected function setUp(): void
    {
        $this->container = new ServiceContainer();
        $this->container->set(InputInterface::class, $this->createStub(InputInterface::class));
        $this->container->set(OutputInterface::class, $this->createStub(OutputInterface::class));
    }

    public function test_it_is_a_psr_container()
    {
        $this->assertInstanceOf(ContainerInterface::class, $this->container);
    }

    public function test_it_returns_false_if_service_is_not_registered()
    {
        $this->assertFalse($this->container->has('foo'));
    }

    #[DataProvider('provideApplicationServices')]
    public function test_it_creates_application_services(string $serviceId, string $expectedType)
    {
        $this->assertTrue($this->container->has($serviceId));
        $this->assertInstanceOf($expectedType, $this->container->get($serviceId));
    }

    public static function provideApplicationServices(): \Generator
    {
        yield [InstallCommand::class, InstallCommand::class];
        yield [ListCommand::class, ListCommand::class];
        yield [TestCommand::class, TestCommand::class];
        yield [Runner::class, LazyRunner::class];
    }

    public function test_it_throws_an_exception_if_unregistered_service_is_accessed()
    {
        $this->expectException(NotFoundExceptionInterface::class);
        $this->expectExceptionMessage('The "foo" service is not registered in the service container.');

        $this->container->get('foo');
    }

    public function test_it_registers_a_runtime_service()
    {
        $service = $this->createStub(InputInterface::class);

        $this->container->set(InputInterface::class, $service);

        $this->assertTrue($this->container->has(InputInterface::class));
        $this->assertSame($service, $this->container->get(InputInterface::class));
    }

    public function test_it_returns_false_if_runtime_service_has_not_been_defined()
    {
        $this->container = new ServiceContainer();

        $this->assertFalse($this->container->has(InputInterface::class));
    }

    public function test_it_throws_an_exception_if_missing_runtime_service_is_accessed()
    {
        $this->expectException(NotFoundExceptionInterface::class);

        $this->container = new ServiceContainer();
        $this->container->get(InputInterface::class);
    }

    public function test_it_throws_an_exception_if_unknown_runtime_service_is_provided()
    {
        $this->expectException(ContainerExceptionInterface::class);
        $this->expectExceptionMessage('The "foo" runtime service is not expected.');

        $this->container->set('foo', new \stdClass());
    }
}


================================================
FILE: tests/Json/Factory/AssertTest.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Tests\Json\Factory;

use PHPUnit\Framework\TestCase;
use Zalas\Toolbox\Json\Factory\Assert;

class AssertTest extends TestCase
{
    public function test_it_throws_an_exception_if_a_field_is_missing()
    {
        $this->expectException(\InvalidArgumentException::class);
        $this->expectExceptionMessage('Missing fields "b, d" in the Test: `{"a":"A","c":"C"}`.');

        Assert::requireFields(['a', 'b', 'c', 'd'], ['a' => 'A', 'c' => 'C'], 'Test');
    }
}


================================================
FILE: tests/Json/Factory/BoxBuildCommandFactoryTest.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Tests\Json\Factory;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Zalas\Toolbox\Json\Factory\BoxBuildCommandFactory;
use Zalas\Toolbox\Tool\Command\BoxBuildCommand;

class BoxBuildCommandFactoryTest extends TestCase
{
    private const REPOSITORY = 'https://github.com/behat/behat.git';

    private const PHAR = 'behat.phar';

    private const BIN = '/usr/local/bin/behat';

    private const VERSION = 'v3.4.0';

    public function test_it_creates_a_command()
    {
        $command = BoxBuildCommandFactory::import([
            'repository' => self::REPOSITORY,
            'phar' => self::PHAR,
            'bin' => self::BIN,
            'version' => self::VERSION,
        ]);

        $this->assertInstanceOf(BoxBuildCommand::class, $command);
        $this->assertMatchesRegularExpression('#git checkout '.self::VERSION.'#', (string) $command);
    }

    public function test_the_version_is_not_required()
    {
        $command = BoxBuildCommandFactory::import([
            'repository' => self::REPOSITORY,
            'phar' => self::PHAR,
            'bin' => self::BIN,
        ]);

        $this->assertInstanceOf(BoxBuildCommand::class, $command);
    }

    #[DataProvider('provideRequiredProperties')]
    public function test_it_complains_if_any_of_required_properties_is_missing(string $property)
    {
        $this->expectException(\InvalidArgumentException::class);

        $properties = [
            'repository' => self::REPOSITORY,
            'phar' => self::PHAR,
            'bin' => self::BIN,
        ];

        unset($properties[$property]);

        BoxBuildCommandFactory::import($properties);
    }

    public static function provideRequiredProperties(): \Generator
    {
        yield ['repository'];
        yield ['phar'];
        yield ['bin'];
    }
}


================================================
FILE: tests/Json/Factory/ComposerBinPluginCommandFactoryTest.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Tests\Json\Factory;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Zalas\Toolbox\Json\Factory\ComposerBinPluginCommandFactory;
use Zalas\Toolbox\Tool\Collection;
use Zalas\Toolbox\Tool\Command\ComposerBinPluginCommand;
use Zalas\Toolbox\Tool\Command\ComposerBinPluginLinkCommand;

class ComposerBinPluginCommandFactoryTest extends TestCase
{
    private const PACKAGE = 'phpstan/phpstan';
    private const NAMESPACE = 'tools';

    public function test_it_creates_a_command()
    {
        $command = ComposerBinPluginCommandFactory::import([
            'package' => self::PACKAGE,
            'namespace' => self::NAMESPACE,
        ]);

        $this->assertInstanceOf(ComposerBinPluginCommand::class, $command);
    }

    public function test_it_creates_a_command_with_links_in_tools()
    {
        $command = ComposerBinPluginCommandFactory::import([
            'package' => self::PACKAGE,
            'namespace' => self::NAMESPACE,
            'links' => ['/tools/phpstan' => 'phpstan'],
        ]);

        $this->assertInstanceOf(ComposerBinPluginCommand::class, $command);
        $this->assertEquals(
            Collection::create([
                new ComposerBinPluginLinkCommand('phpstan', '/tools/phpstan', self::NAMESPACE)
            ]),
            $command->links()
        );
    }

    #[DataProvider('provideRequiredProperties')]
    public function test_it_complains_if_any_of_required_properties_is_missing(string $property)
    {
        $this->expectException(\InvalidArgumentException::class);

        $properties = [
            'package' => self::PACKAGE,
            'namespace' => self::NAMESPACE,
        ];

        unset($properties[$property]);

        ComposerBinPluginCommandFactory::import($properties);
    }

    public static function provideRequiredProperties(): \Generator
    {
        yield ['package'];
        yield ['namespace'];
    }
}


================================================
FILE: tests/Json/Factory/ComposerGlobalInstallCommandFactoryTest.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Tests\Json\Factory;

use PHPUnit\Framework\TestCase;
use Zalas\Toolbox\Json\Factory\ComposerGlobalInstallCommandFactory;
use Zalas\Toolbox\Tool\Command\ComposerGlobalInstallCommand;

class ComposerGlobalInstallCommandFactoryTest extends TestCase
{
    private const PACKAGE = 'phan/phan';

    public function test_creates_a_command()
    {
        $command = ComposerGlobalInstallCommandFactory::import([
            'package' => self::PACKAGE,
        ]);

        $this->assertInstanceOf(ComposerGlobalInstallCommand::class, $command);
    }

    public function test_it_complains_if_package_is_missing()
    {
        $this->expectException(\InvalidArgumentException::class);

        ComposerGlobalInstallCommandFactory::import([]);
    }
}


================================================
FILE: tests/Json/Factory/ComposerInstallCommandFactoryTest.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Tests\Json\Factory;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Zalas\Toolbox\Json\Factory\ComposerInstallCommandFactory;
use Zalas\Toolbox\Tool\Command\ComposerInstallCommand;

class ComposerInstallCommandFactoryTest extends TestCase
{
    private const REPOSITORY = 'https://github.com/behat/behat.git';
    private const LOCATION = '/tools';
    private const VERSION = 'v3.4.0';

    public function test_it_creates_a_command()
    {
        $command = ComposerInstallCommandFactory::import([
            'repository' => self::REPOSITORY,
            'target-dir' => self::LOCATION,
            'version' => self::VERSION,
        ]);

        $this->assertInstanceOf(ComposerInstallCommand::class, $command);
        $this->assertMatchesRegularExpression('#git checkout '.self::VERSION.'#', (string) $command);
    }

    #[DataProvider('provideRequiredProperties')]
    public function test_it_complains_if_a_required_property_is_missing(string $property)
    {
        $this->expectException(\InvalidArgumentException::class);

        $properties = [
            'repository' => self::REPOSITORY,
            'target-dir' => self::LOCATION,
            'version' => self::VERSION,
        ];
        unset($properties[$property]);

        ComposerInstallCommandFactory::import($properties);
    }

    public static function provideRequiredProperties(): \Generator
    {
        yield ['repository'];
        yield ['target-dir'];
    }
}


================================================
FILE: tests/Json/Factory/FileDownloadCommandFactoryTest.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Tests\Json\Factory;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Zalas\Toolbox\Json\Factory\FileDownloadCommandFactory;
use Zalas\Toolbox\Tool\Command\FileDownloadCommand;

class FileDownloadCommandFactoryTest extends TestCase
{
    private const URL = 'https://example.com/file';
    private const FILE = '/usr/local/bin/file.txt';
    
    public function test_it_creates_a_command()
    {
        $command = FileDownloadCommandFactory::import([
            'url' => self::URL,
            'file' => self::FILE,
        ]);

        $this->assertInstanceOf(FileDownloadCommand::class, $command);
    }

    #[DataProvider('provideRequiredProperties')]
    public function test_it_complains_if_any_of_required_properties_is_missing(string $property)
    {
        $this->expectException(\InvalidArgumentException::class);

        $properties = [
            'url' => self::URL,
            'file' => self::FILE,
        ];

        unset($properties[$property]);

        FileDownloadCommandFactory::import($properties);
    }

    public static function provideRequiredProperties(): \Generator
    {
        yield ['url'];
        yield ['file'];
    }
}


================================================
FILE: tests/Json/Factory/PharDownloadCommandFactoryTest.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Tests\Json\Factory;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Zalas\Toolbox\Json\Factory\PharDownloadCommandFactory;
use Zalas\Toolbox\Tool\Command\PharDownloadCommand;

class PharDownloadCommandFactoryTest extends TestCase
{
    private const PHAR = 'https://example.com/foo.phar';
    private const BIN = '/usr/local/bin/foo';

    public function test_it_creates_a_command()
    {
        $command = PharDownloadCommandFactory::import([
            'phar' => self::PHAR,
            'bin' => self::BIN,
        ]);
        
        $this->assertInstanceOf(PharDownloadCommand::class, $command);
    }

    #[DataProvider('provideRequiredProperties')]
    public function test_it_complains_if_any_of_required_properties_is_missing(string $property)
    {
        $this->expectException(\InvalidArgumentException::class);

        $properties = [
            'phar' => self::PHAR,
            'bin' => self::BIN,
        ];

        unset($properties[$property]);

        PharDownloadCommandFactory::import($properties);
    }

    public static function provideRequiredProperties(): \Generator
    {
        yield ['phar'];
        yield ['bin'];
    }
}


================================================
FILE: tests/Json/Factory/PhiveInstallCommandFactoryTest.php
================================================
<?php declare(strict_types=1);

namespace Zalas\Toolbox\Tests\Json\Factory;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Zalas\Toolbox\Json\Factory\PhiveInstallCommandFactory;
use Zalas\Toolbox\Tool\Command\PhiveInstallCommand;

class PhiveInstallCommandFactoryTest extends TestCase
{
    private const ALIAS = 'example/foo';
    private const BIN = '/usr/local/bin/foo';
    private const SIG = '0000000000000000';

    public function test_it_creates_a_command()
    {
        $comm
Download .txt
gitextract_t8u0t2r9/

├── .gitattributes
├── .github/
│   ├── FUNDING.yml
│   ├── pull_request_template.md
│   └── workflows/
│       ├── build.yml
│       ├── publish-website.yml
│       └── update-phars.yml
├── .gitignore
├── .php-cs-fixer.dist.php
├── .scrutinizer.yml
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── Makefile
├── README.md
├── bin/
│   ├── devkit.php
│   └── toolbox.php
├── box-devkit.json.dist
├── box.json.dist
├── composer.json
├── deptrac.yaml
├── infection.json.dist
├── phpunit.xml.dist
├── resources/
│   ├── architecture.json
│   ├── checkstyle.json
│   ├── compatibility.json
│   ├── composer.json
│   ├── deprecation.json
│   ├── documentation.json
│   ├── linting.json
│   ├── metrics.json
│   ├── phpcs.json
│   ├── phpstan.json
│   ├── pre-installation.json
│   ├── psalm.json
│   ├── refactoring.json
│   ├── security.json
│   ├── test.json
│   └── tools.json
├── scoper.inc.php
├── src/
│   ├── Cli/
│   │   ├── Application.php
│   │   ├── Command/
│   │   │   ├── DefaultTag.php
│   │   │   ├── DefaultTargetDir.php
│   │   │   ├── InstallCommand.php
│   │   │   ├── ListCommand.php
│   │   │   └── TestCommand.php
│   │   ├── Runner/
│   │   │   └── DryRunner.php
│   │   ├── ServiceContainer/
│   │   │   ├── LazyRunner.php
│   │   │   └── RunnerFactory.php
│   │   └── ServiceContainer.php
│   ├── Json/
│   │   ├── Factory/
│   │   │   ├── Assert.php
│   │   │   ├── BoxBuildCommandFactory.php
│   │   │   ├── ComposerBinPluginCommandFactory.php
│   │   │   ├── ComposerGlobalInstallCommandFactory.php
│   │   │   ├── ComposerInstallCommandFactory.php
│   │   │   ├── FileDownloadCommandFactory.php
│   │   │   ├── PharDownloadCommandFactory.php
│   │   │   ├── PhiveInstallCommandFactory.php
│   │   │   ├── ShCommandFactory.php
│   │   │   └── ToolFactory.php
│   │   └── JsonTools.php
│   ├── Runner/
│   │   ├── ParametrisedRunner.php
│   │   ├── PassthruRunner.php
│   │   └── Runner.php
│   ├── Tool/
│   │   ├── Collection.php
│   │   ├── Command/
│   │   │   ├── BoxBuildCommand.php
│   │   │   ├── ComposerBinPluginCommand.php
│   │   │   ├── ComposerBinPluginLinkCommand.php
│   │   │   ├── ComposerGlobalInstallCommand.php
│   │   │   ├── ComposerGlobalMultiInstallCommand.php
│   │   │   ├── ComposerInstallCommand.php
│   │   │   ├── FileDownloadCommand.php
│   │   │   ├── MultiStepCommand.php
│   │   │   ├── OptimisedComposerBinPluginCommand.php
│   │   │   ├── PharDownloadCommand.php
│   │   │   ├── PhiveInstallCommand.php
│   │   │   ├── ShCommand.php
│   │   │   └── TestCommand.php
│   │   ├── Command.php
│   │   ├── Filter.php
│   │   ├── Tool.php
│   │   └── Tools.php
│   └── UseCase/
│       ├── InstallTools.php
│       ├── ListTools.php
│       └── TestTools.php
├── tests/
│   ├── Cli/
│   │   ├── ApplicationTest.php
│   │   ├── Command/
│   │   │   ├── InstallCommandTest.php
│   │   │   ├── ListCommandTest.php
│   │   │   ├── TestCommandTest.php
│   │   │   └── ToolboxCommandTestCase.php
│   │   ├── Runner/
│   │   │   └── DryRunnerTest.php
│   │   ├── ServiceContainer/
│   │   │   ├── LazyRunnerTest.php
│   │   │   └── RunnerFactoryTest.php
│   │   └── ServiceContainerTest.php
│   ├── Json/
│   │   ├── Factory/
│   │   │   ├── AssertTest.php
│   │   │   ├── BoxBuildCommandFactoryTest.php
│   │   │   ├── ComposerBinPluginCommandFactoryTest.php
│   │   │   ├── ComposerGlobalInstallCommandFactoryTest.php
│   │   │   ├── ComposerInstallCommandFactoryTest.php
│   │   │   ├── FileDownloadCommandFactoryTest.php
│   │   │   ├── PharDownloadCommandFactoryTest.php
│   │   │   ├── PhiveInstallCommandFactoryTest.php
│   │   │   ├── ShCommandFactoryTest.php
│   │   │   └── ToolFactoryTest.php
│   │   └── JsonToolsTest.php
│   ├── Runner/
│   │   ├── ParametrisedRunnerTest.php
│   │   └── PassthruRunnerTest.php
│   ├── Tool/
│   │   ├── CollectionTest.php
│   │   ├── Command/
│   │   │   ├── BoxBuildCommandTest.php
│   │   │   ├── ComposerBinPluginCommandTest.php
│   │   │   ├── ComposerBinPluginLinkCommandTest.php
│   │   │   ├── ComposerGlobalInstallCommandTest.php
│   │   │   ├── ComposerGlobalMultiInstallCommandTest.php
│   │   │   ├── ComposerInstallCommandTest.php
│   │   │   ├── FileDownloadCommandTest.php
│   │   │   ├── MultiStepCommandTest.php
│   │   │   ├── OptimisedComposerBinPluginCommandTest.php
│   │   │   ├── PharDownloadCommandTest.php
│   │   │   ├── PhiveInstallCommandTest.php
│   │   │   ├── ShCommandTest.php
│   │   │   └── TestCommandTest.php
│   │   ├── FilterTest.php
│   │   └── ToolTest.php
│   ├── UseCase/
│   │   ├── InstallToolsTest.php
│   │   ├── ListToolsTest.php
│   │   └── TestToolsTest.php
│   └── resources/
│       ├── invalid-tools.json
│       ├── invalid.json
│       ├── no-tools.json
│       ├── pre-installation.json
│       └── tools.json
└── tools/
    └── .gitignore
Download .txt
SYMBOL INDEX (465 symbols across 87 files)

FILE: bin/devkit.php
  type Tools (line 19) | trait Tools
    method toolsJsonDefault (line 21) | private function toolsJsonDefault(): array
    method loadTools (line 45) | private function loadTools($jsonPath, ?Filter $filter = null): Collection
  method configure (line 59) | protected function configure(): void
  method execute (line 67) | protected function execute(InputInterface $input, OutputInterface $outpu...
  method configure (line 109) | protected function configure(): void
  method execute (line 116) | protected function execute(InputInterface $input, OutputInterface $outpu...
  method updatePhars (line 129) | private function updatePhars(string $jsonPath, OutputInterface $output):...
  method findLatestPharsCommand (line 148) | private function findLatestPharsCommand(string $jsonPath): Command
  method findLatestPhars (line 162) | private function findLatestPhars(string $jsonPath): array
  method updatePharsCommand (line 171) | private function updatePharsCommand(string $jsonPath, array $phars): Com...
  method configure (line 196) | protected function configure(): void
  method execute (line 203) | protected function execute(InputInterface $input, OutputInterface $outpu...
  method toolToHtml (line 212) | private function toolToHtml(): \Closure
  method renderPage (line 245) | private function renderPage(Collection $toolsHtml): string

FILE: src/Cli/Application.php
  class Application (line 17) | final class Application extends CliApplication
    method __construct (line 21) | public function __construct(string $version, ServiceContainer $service...
    method doRun (line 33) | public function doRun(InputInterface $input, OutputInterface $output):...
    method getDefaultInputDefinition (line 41) | protected function getDefaultInputDefinition(): InputDefinition
    method toolsJsonDefault (line 49) | private function toolsJsonDefault(): array
    method createCommandLoader (line 73) | private function createCommandLoader(ContainerInterface $container): C...

FILE: src/Cli/Command/DefaultTag.php
  type DefaultTag (line 5) | trait DefaultTag
    method defaultExcludeTag (line 7) | private function defaultExcludeTag(): array
    method defaultTag (line 12) | private function defaultTag(): array

FILE: src/Cli/Command/DefaultTargetDir.php
  type DefaultTargetDir (line 5) | trait DefaultTargetDir
    method defaultTargetDir (line 7) | private function defaultTargetDir(): string

FILE: src/Cli/Command/InstallCommand.php
  class InstallCommand (line 13) | final class InstallCommand extends Command
    method __construct (line 23) | public function __construct(InstallTools $useCase, Runner $runner)
    method configure (line 31) | protected function configure(): void
    method execute (line 40) | protected function execute(InputInterface $input, OutputInterface $out...

FILE: src/Cli/Command/ListCommand.php
  class ListCommand (line 15) | final class ListCommand extends Command
    method __construct (line 23) | public function __construct(ListTools $listTools)
    method configure (line 30) | protected function configure(): void
    method execute (line 37) | protected function execute(InputInterface $input, OutputInterface $out...
    method createStyle (line 53) | private function createStyle(InputInterface $input, OutputInterface $o...

FILE: src/Cli/Command/TestCommand.php
  class TestCommand (line 13) | final class TestCommand extends Command
    method __construct (line 23) | public function __construct(TestTools $useCase, Runner $runner)
    method configure (line 31) | protected function configure(): void
    method execute (line 40) | protected function execute(InputInterface $input, OutputInterface $out...

FILE: src/Cli/Runner/DryRunner.php
  class DryRunner (line 9) | final class DryRunner implements Runner
    method __construct (line 13) | public function __construct(OutputInterface $output)
    method run (line 18) | public function run(Command $command): int

FILE: src/Cli/ServiceContainer.php
  class ServiceContainer (line 23) | class ServiceContainer implements ContainerInterface
    method set (line 41) | public function set(string $id, /*object */$service): void
    method get (line 54) | public function get(string $id)
    method has (line 71) | public function has(string $id): bool
    method createInstallCommand (line 80) | private function createInstallCommand(): InstallCommand
    method createListCommand (line 89) | private function createListCommand(): ListCommand
    method createTestCommand (line 98) | private function createTestCommand(): TestCommand
    method createRunner (line 103) | private function createRunner(): Runner
    method createInstallToolsUseCase (line 112) | private function createInstallToolsUseCase(): InstallTools
    method createListToolsUseCase (line 121) | private function createListToolsUseCase(): ListTools
    method createTestToolsUseCase (line 130) | private function createTestToolsUseCase(): TestTools
    method createTools (line 135) | private function createTools(): Tools

FILE: src/Cli/ServiceContainer/LazyRunner.php
  class LazyRunner (line 10) | final class LazyRunner implements Runner
    method __construct (line 16) | public function __construct(RunnerFactory $factory)
    method run (line 25) | public function run(Command $command): int
    method runner (line 34) | private function runner(): Runner

FILE: src/Cli/ServiceContainer/RunnerFactory.php
  class RunnerFactory (line 15) | class RunnerFactory
    method __construct (line 19) | public function __construct(ContainerInterface $container)
    method createRunner (line 28) | public function createRunner(): Runner
    method createRealRunner (line 43) | private function createRealRunner(): DryRunner|PassthruRunner
    method parameters (line 56) | private function parameters(): array
    method targetDir (line 69) | private function targetDir(): ?string

FILE: src/Json/Factory/Assert.php
  class Assert (line 5) | final class Assert
    method requireFields (line 7) | public static function requireFields(array $fields, array $data, strin...

FILE: src/Json/Factory/BoxBuildCommandFactory.php
  class BoxBuildCommandFactory (line 8) | final class BoxBuildCommandFactory
    method import (line 10) | public static function import(array $definition): Command

FILE: src/Json/Factory/ComposerBinPluginCommandFactory.php
  class ComposerBinPluginCommandFactory (line 10) | final class ComposerBinPluginCommandFactory
    method import (line 12) | public static function import(array $command): Command
    method importLinks (line 19) | private static function importLinks(array $command): Collection

FILE: src/Json/Factory/ComposerGlobalInstallCommandFactory.php
  class ComposerGlobalInstallCommandFactory (line 8) | final class ComposerGlobalInstallCommandFactory
    method import (line 10) | public static function import(array $command): Command

FILE: src/Json/Factory/ComposerInstallCommandFactory.php
  class ComposerInstallCommandFactory (line 8) | final class ComposerInstallCommandFactory
    method import (line 10) | public static function import(array $command): Command

FILE: src/Json/Factory/FileDownloadCommandFactory.php
  class FileDownloadCommandFactory (line 8) | final class FileDownloadCommandFactory
    method import (line 10) | public static function import(array $command): Command

FILE: src/Json/Factory/PharDownloadCommandFactory.php
  class PharDownloadCommandFactory (line 8) | final class PharDownloadCommandFactory
    method import (line 10) | public static function import(array $command): Command

FILE: src/Json/Factory/PhiveInstallCommandFactory.php
  class PhiveInstallCommandFactory (line 8) | final class PhiveInstallCommandFactory
    method import (line 10) | public static function import(array $command): Command

FILE: src/Json/Factory/ShCommandFactory.php
  class ShCommandFactory (line 8) | final class ShCommandFactory
    method import (line 10) | public static function import(array $command): Command

FILE: src/Json/Factory/ToolFactory.php
  class ToolFactory (line 11) | final class ToolFactory
    method import (line 13) | public static function import(array $tool): Tool
    method importCommand (line 27) | private static function importCommand(array $tool): Command
    method createCommands (line 42) | private static function createCommands($type, $command): Collection

FILE: src/Json/JsonTools.php
  class JsonTools (line 12) | final class JsonTools implements Tools
    method __construct (line 19) | public function __construct(callable $resourceLocator)
    method all (line 28) | public function all(Filter $filter): Collection
    method loadTools (line 33) | private function loadTools(): Collection
    method loadJson (line 42) | private function loadJson(string $resource): array
    method resources (line 57) | private function resources(): array

FILE: src/Runner/ParametrisedRunner.php
  class ParametrisedRunner (line 7) | final class ParametrisedRunner implements Runner
    method __construct (line 12) | public function __construct(Runner $decoratedRunner, array $parameters)
    method run (line 18) | public function run(Command $command): int

FILE: src/Runner/PassthruRunner.php
  class PassthruRunner (line 7) | final class PassthruRunner implements Runner
    method run (line 9) | public function run(Command $command): int

FILE: src/Runner/Runner.php
  type Runner (line 7) | interface Runner
    method run (line 9) | public function run(Command $command): int;

FILE: src/Tool/Collection.php
  class Collection (line 9) | class Collection implements IteratorAggregate, Countable
    method __construct (line 13) | private function __construct(array $elements)
    method create (line 18) | public static function create(array $elements): Collection
    method getIterator (line 23) | public function getIterator(): Traversable
    method merge (line 28) | public function merge(Collection $other): Collection
    method filter (line 33) | public function filter(callable $f): Collection
    method map (line 38) | public function map(callable $f): Collection
    method reduce (line 43) | public function reduce($initial, callable $param)
    method sort (line 48) | public function sort(callable $f): Collection
    method toArray (line 56) | public function toArray(): array
    method count (line 61) | public function count(): int
    method empty (line 66) | public function empty(): bool

FILE: src/Tool/Command.php
  type Command (line 5) | interface Command
    method __toString (line 7) | public function __toString(): string;

FILE: src/Tool/Command/BoxBuildCommand.php
  class BoxBuildCommand (line 7) | final class BoxBuildCommand implements Command
    method __construct (line 15) | public function __construct(string $repository, string $phar, string $...
    method __toString (line 24) | public function __toString(): string
    method targetDir (line 39) | private function targetDir(): string

FILE: src/Tool/Command/ComposerBinPluginCommand.php
  class ComposerBinPluginCommand (line 8) | final class ComposerBinPluginCommand implements Command
    method __construct (line 16) | public function __construct(string $package, string $namespace, Collec...
    method __toString (line 23) | public function __toString(): string
    method package (line 28) | public function package(): string
    method namespace (line 33) | public function namespace(): string
    method links (line 38) | public function links(): Collection
    method linkCommand (line 43) | private function linkCommand(): string

FILE: src/Tool/Command/ComposerBinPluginLinkCommand.php
  class ComposerBinPluginLinkCommand (line 7) | final class ComposerBinPluginLinkCommand implements Command
    method __construct (line 13) | public function __construct(string $source, string $target, string $na...
    method __toString (line 20) | public function __toString(): string

FILE: src/Tool/Command/ComposerGlobalInstallCommand.php
  class ComposerGlobalInstallCommand (line 7) | final class ComposerGlobalInstallCommand implements Command
    method __construct (line 11) | public function __construct(string $package)
    method __toString (line 16) | public function __toString(): string
    method package (line 21) | public function package(): string

FILE: src/Tool/Command/ComposerGlobalMultiInstallCommand.php
  class ComposerGlobalMultiInstallCommand (line 9) | final class ComposerGlobalMultiInstallCommand implements Command
    method __construct (line 13) | public function __construct(Collection $commands)
    method __toString (line 24) | public function __toString(): string

FILE: src/Tool/Command/ComposerInstallCommand.php
  class ComposerInstallCommand (line 7) | final class ComposerInstallCommand implements Command
    method __construct (line 13) | public function __construct(string $repository, string $targetDir, ?st...
    method __toString (line 20) | public function __toString(): string

FILE: src/Tool/Command/FileDownloadCommand.php
  class FileDownloadCommand (line 7) | final class FileDownloadCommand implements Command
    method __construct (line 12) | public function __construct(string $url, string $file)
    method __toString (line 18) | public function __toString(): string

FILE: src/Tool/Command/MultiStepCommand.php
  class MultiStepCommand (line 9) | final class MultiStepCommand implements Command
    method __construct (line 14) | public function __construct(Collection $commands, $glue = ' && ')
    method __toString (line 26) | public function __toString(): string

FILE: src/Tool/Command/OptimisedComposerBinPluginCommand.php
  class OptimisedComposerBinPluginCommand (line 9) | final class OptimisedComposerBinPluginCommand implements Command
    method __construct (line 13) | public function __construct(Collection $commands)
    method __toString (line 24) | public function __toString(): string
    method packagesGroupedByNamespace (line 29) | private function packagesGroupedByNamespace(): array
    method commandToRun (line 38) | private function commandToRun(string $namespace, array $packages): string
    method commandsToRun (line 43) | private function commandsToRun(array $packagesGrouped): array
    method linksToCreate (line 48) | private function linksToCreate(): array

FILE: src/Tool/Command/PharDownloadCommand.php
  class PharDownloadCommand (line 7) | final class PharDownloadCommand implements Command
    method __construct (line 12) | public function __construct(string $phar, string $bin)
    method __toString (line 18) | public function __toString(): string

FILE: src/Tool/Command/PhiveInstallCommand.php
  class PhiveInstallCommand (line 7) | final class PhiveInstallCommand implements Command
    method __construct (line 13) | public function __construct(string $alias, string $bin, ?string $sig =...
    method __toString (line 20) | public function __toString(): string

FILE: src/Tool/Command/ShCommand.php
  class ShCommand (line 7) | final class ShCommand implements Command
    method __construct (line 11) | public function __construct(string $command)
    method __toString (line 16) | public function __toString(): string

FILE: src/Tool/Command/TestCommand.php
  class TestCommand (line 7) | final class TestCommand implements Command
    method __construct (line 12) | public function __construct(string $command, string $name)
    method __toString (line 18) | public function __toString(): string

FILE: src/Tool/Filter.php
  class Filter (line 5) | class Filter
    method __construct (line 21) | public function __construct(array $excludedTags, array $tags)
    method __invoke (line 27) | public function __invoke(Tool $tool): bool

FILE: src/Tool/Tool.php
  class Tool (line 5) | class Tool
    method __construct (line 14) | public function __construct(string $name, string $summary, string $web...
    method name (line 26) | public function name(): string
    method summary (line 31) | public function summary(): string
    method website (line 36) | public function website(): string
    method command (line 41) | public function command(): Command
    method testCommand (line 46) | public function testCommand(): Command
    method tags (line 54) | public function tags(): array

FILE: src/Tool/Tools.php
  type Tools (line 7) | interface Tools
    method all (line 14) | public function all(Filter $filter): Collection;

FILE: src/UseCase/InstallTools.php
  class InstallTools (line 23) | class InstallTools
    method __construct (line 29) | public function __construct(Tools $tools)
    method __invoke (line 34) | public function __invoke(Filter $filter): Command
    method commandFilter (line 54) | private function commandFilter(Collection $commands): Closure
    method installationCommands (line 63) | private function installationCommands(Collection $tools): Collection
    method toolCommands (line 72) | private function toolCommands(Collection $tools): Collection
    method groupComposerGlobalInstallCommands (line 81) | private function groupComposerGlobalInstallCommands(Collection $comman...
    method groupComposerBinPluginCommands (line 88) | private function groupComposerBinPluginCommands(Collection $commands):...

FILE: src/UseCase/ListTools.php
  class ListTools (line 9) | class ListTools
    method __construct (line 13) | public function __construct(Tools $tools)
    method __invoke (line 18) | public function __invoke(Filter $filter): Collection

FILE: src/UseCase/TestTools.php
  class TestTools (line 11) | class TestTools
    method __construct (line 15) | public function __construct(Tools $tools)
    method __invoke (line 20) | public function __invoke(Filter $filter): Command

FILE: tests/Cli/ApplicationTest.php
  class ApplicationTest (line 16) | class ApplicationTest extends TestCase
    method setUp (line 22) | protected function setUp(): void
    method test_it_is_a_cli_application (line 28) | public function test_it_is_a_cli_application()
    method test_it_defines_the_app_name_and_version (line 33) | public function test_it_defines_the_app_name_and_version()
    method test_it_defines_tools_option (line 39) | public function test_it_defines_tools_option()
    method test_it_takes_the_tools_option_default_from_environment_if_present (line 65) | #[Putenv('TOOLBOX_JSON', 'resources/pre.json,resources/tools.json')]
    method test_it_trims_the_tools_option (line 71) | #[Putenv('TOOLBOX_JSON', 'resources/pre.json , resources/tools.json')]
    method test_it_allows_to_override_tools_location (line 80) | public function test_it_allows_to_override_tools_location()
    method test_it_runs_the_command_in_dry_run_mode (line 98) | public function test_it_runs_the_command_in_dry_run_mode()
    method givenOutputThatExpectsMessageWritten (line 114) | public function givenOutputThatExpectsMessageWritten(string $message):...

FILE: tests/Cli/Command/InstallCommandTest.php
  class InstallCommandTest (line 14) | class InstallCommandTest extends ToolboxCommandTestCase
    method setUp (line 22) | protected function setUp(): void
    method test_it_runs_the_install_tools_use_case (line 30) | public function test_it_runs_the_install_tools_use_case()
    method test_it_returns_the_status_code_of_the_run (line 41) | public function test_it_returns_the_status_code_of_the_run()
    method test_it_filters_by_tags (line 51) | public function test_it_filters_by_tags()
    method test_it_defines_dry_run_option (line 64) | public function test_it_defines_dry_run_option()
    method test_it_defines_target_dir_option (line 69) | public function test_it_defines_target_dir_option()
    method test_it_takes_the_target_dir_option_default_from_environment_if_present (line 75) | #[Putenv('TOOLBOX_TARGET_DIR', '/tmp')]
    method test_it_defines_exclude_tag_option (line 81) | public function test_it_defines_exclude_tag_option()
    method test_it_takes_the_excluded_tag_option_default_from_environment_if_present (line 87) | #[Putenv('TOOLBOX_EXCLUDED_TAGS', 'foo,bar,baz')]
    method test_it_defines_tag_option (line 93) | public function test_it_defines_tag_option()
    method test_it_takes_the_tag_option_default_from_environment_if_present (line 99) | #[Putenv('TOOLBOX_TAGS', 'foo,bar,baz')]
    method getContainerTestDoubles (line 105) | protected function getContainerTestDoubles(): array
    method createCommand (line 113) | private function createCommand(): Command

FILE: tests/Cli/Command/ListCommandTest.php
  class ListCommandTest (line 15) | class ListCommandTest extends ToolboxCommandTestCase
    method setUp (line 21) | protected function setUp(): void
    method test_it_runs_the_list_tools_use_case (line 28) | public function test_it_runs_the_list_tools_use_case()
    method test_it_filters_by_tags (line 41) | public function test_it_filters_by_tags()
    method test_it_defines_exclude_tag_option (line 54) | public function test_it_defines_exclude_tag_option()
    method test_it_takes_the_excluded_tag_option_default_from_environment_if_present (line 60) | #[Putenv('TOOLBOX_EXCLUDED_TAGS', 'foo,bar,baz')]
    method test_it_defines_tag_option (line 66) | public function test_it_defines_tag_option()
    method test_it_takes_the_tag_option_default_from_environment_if_present (line 71) | #[Putenv('TOOLBOX_TAGS', 'foo,bar,baz')]
    method getContainerTestDoubles (line 77) | protected function getContainerTestDoubles(): array
    method createTool (line 84) | private function createTool(string $name, string $summary, string $web...

FILE: tests/Cli/Command/TestCommandTest.php
  class TestCommandTest (line 14) | class TestCommandTest extends ToolboxCommandTestCase
    method setUp (line 22) | protected function setUp(): void
    method test_it_runs_the_test_tools_use_case (line 30) | public function test_it_runs_the_test_tools_use_case()
    method test_it_returns_the_status_code_of_the_run (line 41) | public function test_it_returns_the_status_code_of_the_run()
    method test_it_filters_by_tags (line 51) | public function test_it_filters_by_tags()
    method test_it_defines_dry_run_option (line 61) | public function test_it_defines_dry_run_option()
    method test_it_defines_target_dir_option (line 66) | public function test_it_defines_target_dir_option()
    method test_it_takes_the_target_dir_option_default_from_environment_if_present (line 72) | #[Putenv('TOOLBOX_TARGET_DIR', '/tmp')]
    method test_it_defines_exclude_tag_option (line 78) | public function test_it_defines_exclude_tag_option()
    method test_it_takes_the_excluded_tag_option_default_from_environment_if_present (line 84) | #[Putenv('TOOLBOX_EXCLUDED_TAGS', 'foo,bar,baz')]
    method test_it_defines_tag_option (line 90) | public function test_it_defines_tag_option()
    method test_it_takes_the_tag_option_default_from_environment_if_present (line 95) | #[Putenv('TOOLBOX_TAGS', 'foo,bar,baz')]
    method getContainerTestDoubles (line 101) | protected function getContainerTestDoubles(): array
    method createCommand (line 109) | private function createCommand(): Command

FILE: tests/Cli/Command/ToolboxCommandTestCase.php
  class ToolboxCommandTestCase (line 11) | abstract class ToolboxCommandTestCase extends TestCase
    method setUp (line 17) | protected function setUp(): void
    method test_it_provides_help (line 22) | public function test_it_provides_help()
    method getContainerTestDoubles (line 27) | protected function getContainerTestDoubles(): array
    method executeCliCommand (line 32) | protected function executeCliCommand(array $input = []): CommandTester
    method cliCommand (line 40) | protected function cliCommand(): Command
    method createServiceContainer (line 45) | private function createServiceContainer(): ServiceContainer

FILE: tests/Cli/Runner/DryRunnerTest.php
  class DryRunnerTest (line 12) | class DryRunnerTest extends TestCase
    method setUp (line 18) | protected function setUp(): void
    method test_it_is_a_runner (line 24) | public function test_it_is_a_runner()
    method test_it_sends_the_command_to_the_output (line 29) | public function test_it_sends_the_command_to_the_output()

FILE: tests/Cli/ServiceContainer/LazyRunnerTest.php
  class LazyRunnerTest (line 12) | class LazyRunnerTest extends TestCase
    method setUp (line 18) | protected function setUp(): void
    method test_it_is_a_runner (line 25) | public function test_it_is_a_runner()
    method test_it_returns_status_code_of_returned_by_the_created_runner (line 30) | public function test_it_returns_status_code_of_returned_by_the_created...
    method test_it_only_initializes_the_runner_once (line 40) | public function test_it_only_initializes_the_runner_once()
    method givenRunner (line 55) | public function givenRunner(Command $command, int $result): Runner
    method command (line 63) | private function command(): Command
    method givenFactoryCreates (line 68) | private function givenFactoryCreates(Runner $runner): void

FILE: tests/Cli/ServiceContainer/RunnerFactoryTest.php
  class RunnerFactoryTest (line 20) | class RunnerFactoryTest extends TestCase
    method setUp (line 28) | protected function setUp(): void
    method test_it_creates_the_passthru_runner_by_default (line 53) | public function test_it_creates_the_passthru_runner_by_default()
    method test_it_creates_the_dry_runner_if_dry_run_option_is_passed (line 60) | public function test_it_creates_the_dry_runner_if_dry_run_option_is_pa...
    method test_it_creates_the_parametrised_runner_if_target_dir_option_is_present (line 69) | public function test_it_creates_the_parametrised_runner_if_target_dir_...
    method test_the_parametrised_runner_includes_the_target_dir_parameter (line 78) | public function test_the_parametrised_runner_includes_the_target_dir_p...
    method test_it_throws_an_exception_if_target_dir_does_not_exist (line 94) | public function test_it_throws_an_exception_if_target_dir_does_not_exi...
    method test_it_uses_the_real_path_as_target_dir (line 103) | public function test_it_uses_the_real_path_as_target_dir()
    method givenInput (line 118) | private function givenInput(array $parameters): InputInterface

FILE: tests/Cli/ServiceContainerTest.php
  class ServiceContainerTest (line 19) | class ServiceContainerTest extends TestCase
    method setUp (line 23) | protected function setUp(): void
    method test_it_is_a_psr_container (line 30) | public function test_it_is_a_psr_container()
    method test_it_returns_false_if_service_is_not_registered (line 35) | public function test_it_returns_false_if_service_is_not_registered()
    method test_it_creates_application_services (line 40) | #[DataProvider('provideApplicationServices')]
    method provideApplicationServices (line 47) | public static function provideApplicationServices(): \Generator
    method test_it_throws_an_exception_if_unregistered_service_is_accessed (line 55) | public function test_it_throws_an_exception_if_unregistered_service_is...
    method test_it_registers_a_runtime_service (line 63) | public function test_it_registers_a_runtime_service()
    method test_it_returns_false_if_runtime_service_has_not_been_defined (line 73) | public function test_it_returns_false_if_runtime_service_has_not_been_...
    method test_it_throws_an_exception_if_missing_runtime_service_is_accessed (line 80) | public function test_it_throws_an_exception_if_missing_runtime_service...
    method test_it_throws_an_exception_if_unknown_runtime_service_is_provided (line 88) | public function test_it_throws_an_exception_if_unknown_runtime_service...

FILE: tests/Json/Factory/AssertTest.php
  class AssertTest (line 8) | class AssertTest extends TestCase
    method test_it_throws_an_exception_if_a_field_is_missing (line 10) | public function test_it_throws_an_exception_if_a_field_is_missing()

FILE: tests/Json/Factory/BoxBuildCommandFactoryTest.php
  class BoxBuildCommandFactoryTest (line 10) | class BoxBuildCommandFactoryTest extends TestCase
    method test_it_creates_a_command (line 20) | public function test_it_creates_a_command()
    method test_the_version_is_not_required (line 33) | public function test_the_version_is_not_required()
    method test_it_complains_if_any_of_required_properties_is_missing (line 44) | #[DataProvider('provideRequiredProperties')]
    method provideRequiredProperties (line 60) | public static function provideRequiredProperties(): \Generator

FILE: tests/Json/Factory/ComposerBinPluginCommandFactoryTest.php
  class ComposerBinPluginCommandFactoryTest (line 12) | class ComposerBinPluginCommandFactoryTest extends TestCase
    method test_it_creates_a_command (line 15) | private const NAMESPACE = 'tools';
    method test_it_creates_a_command_with_links_in_tools (line 27) | public function test_it_creates_a_command_with_links_in_tools()
    method test_it_complains_if_any_of_required_properties_is_missing (line 44) | #[DataProvider('provideRequiredProperties')]
    method provideRequiredProperties (line 59) | public static function provideRequiredProperties(): \Generator

FILE: tests/Json/Factory/ComposerGlobalInstallCommandFactoryTest.php
  class ComposerGlobalInstallCommandFactoryTest (line 9) | class ComposerGlobalInstallCommandFactoryTest extends TestCase
    method test_creates_a_command (line 13) | public function test_creates_a_command()
    method test_it_complains_if_package_is_missing (line 22) | public function test_it_complains_if_package_is_missing()

FILE: tests/Json/Factory/ComposerInstallCommandFactoryTest.php
  class ComposerInstallCommandFactoryTest (line 10) | class ComposerInstallCommandFactoryTest extends TestCase
    method test_it_creates_a_command (line 16) | public function test_it_creates_a_command()
    method test_it_complains_if_a_required_property_is_missing (line 28) | #[DataProvider('provideRequiredProperties')]
    method provideRequiredProperties (line 43) | public static function provideRequiredProperties(): \Generator

FILE: tests/Json/Factory/FileDownloadCommandFactoryTest.php
  class FileDownloadCommandFactoryTest (line 10) | class FileDownloadCommandFactoryTest extends TestCase
    method test_it_creates_a_command (line 15) | public function test_it_creates_a_command()
    method test_it_complains_if_any_of_required_properties_is_missing (line 25) | #[DataProvider('provideRequiredProperties')]
    method provideRequiredProperties (line 40) | public static function provideRequiredProperties(): \Generator

FILE: tests/Json/Factory/PharDownloadCommandFactoryTest.php
  class PharDownloadCommandFactoryTest (line 10) | class PharDownloadCommandFactoryTest extends TestCase
    method test_it_creates_a_command (line 15) | public function test_it_creates_a_command()
    method test_it_complains_if_any_of_required_properties_is_missing (line 25) | #[DataProvider('provideRequiredProperties')]
    method provideRequiredProperties (line 40) | public static function provideRequiredProperties(): \Generator

FILE: tests/Json/Factory/PhiveInstallCommandFactoryTest.php
  class PhiveInstallCommandFactoryTest (line 10) | class PhiveInstallCommandFactoryTest extends TestCase
    method test_it_creates_a_command (line 16) | public function test_it_creates_a_command()
    method test_it_complains_if_any_of_required_properties_is_missing (line 28) | #[DataProvider('provideRequiredProperties')]
    method test_it_accepts_unsigned_phars (line 44) | public function test_it_accepts_unsigned_phars()
    method provideRequiredProperties (line 55) | public static function provideRequiredProperties(): \Generator

FILE: tests/Json/Factory/ShCommandFactoryTest.php
  class ShCommandFactoryTest (line 9) | class ShCommandFactoryTest extends TestCase
    method test_creates_a_command (line 11) | public function test_creates_a_command()
    method test_it_complains_if_command_is_missing (line 20) | public function test_it_complains_if_command_is_missing()

FILE: tests/Json/Factory/ToolFactoryTest.php
  class ToolFactoryTest (line 20) | class ToolFactoryTest extends TestCase
    method test_it_imports_tool_definition_from_an_array (line 22) | public function test_it_imports_tool_definition_from_an_array()
    method test_it_imports_the_composer_bin_plugin_command (line 46) | public function test_it_imports_the_composer_bin_plugin_command()
    method test_it_imports_the_phar_download_command (line 60) | public function test_it_imports_the_phar_download_command()
    method test_it_imports_the_phive_install_command (line 74) | public function test_it_imports_the_phive_install_command()
    method test_it_imports_the_file_download_command (line 88) | public function test_it_imports_the_file_download_command()
    method test_it_imports_the_box_build_command (line 102) | public function test_it_imports_the_box_build_command()
    method test_it_imports_the_composer_install_command (line 118) | public function test_it_imports_the_composer_install_command()
    method test_it_imports_the_composer_global_install_command (line 133) | public function test_it_imports_the_composer_global_install_command()
    method test_it_imports_the_sh_command (line 147) | public function test_it_imports_the_sh_command()
    method test_it_imports_multiple_commands (line 160) | public function test_it_imports_multiple_commands()
    method test_it_complains_if_it_cannot_recognise_the_command (line 184) | public function test_it_complains_if_it_cannot_recognise_the_command()
    method test_it_complains_if_the_command_is_empty (line 192) | public function test_it_complains_if_the_command_is_empty()
    method test_it_complains_if_any_of_required_properties_is_missing (line 199) | #[DataProvider('provideRequiredProperties')]
    method provideRequiredProperties (line 211) | public static function provideRequiredProperties(): \Generator
    method definition (line 220) | private function definition(array $overrides = []): array

FILE: tests/Json/JsonToolsTest.php
  class JsonToolsTest (line 12) | class JsonToolsTest extends TestCase
    method test_it_is_a_tools_repository (line 14) | public function test_it_is_a_tools_repository()
    method test_it_throws_an_exception_if_resource_is_missing (line 19) | public function test_it_throws_an_exception_if_resource_is_missing()
    method test_it_throws_an_exception_if_resource_contains_invalid_json (line 28) | public function test_it_throws_an_exception_if_resource_contains_inval...
    method test_it_throws_an_exception_if_tools_are_not_present_in_the_resource (line 37) | public function test_it_throws_an_exception_if_tools_are_not_present_i...
    method test_it_throws_an_exception_if_tools_is_not_a_collection (line 46) | public function test_it_throws_an_exception_if_tools_is_not_a_collecti...
    method test_it_loads_tools_from_multiple_resources (line 55) | public function test_it_loads_tools_from_multiple_resources()
    method test_it_filters_out_tools (line 75) | public function test_it_filters_out_tools()
    method locator (line 85) | private function locator(array $resources): callable
    method filter (line 92) | private function filter(array $excludedTags = []): Filter

FILE: tests/Runner/ParametrisedRunnerTest.php
  class ParametrisedRunnerTest (line 12) | class ParametrisedRunnerTest extends TestCase
    method setUp (line 18) | protected function setUp(): void
    method test_it_is_a_runner (line 24) | public function test_it_is_a_runner()
    method test_it_replaces_parameter_holders_in_the_command_before_running_it (line 29) | public function test_it_replaces_parameter_holders_in_the_command_befo...
    method command (line 48) | private function command(string $commandString): Command

FILE: tests/Runner/PassthruRunnerTest.php
  class PassthruRunnerTest (line 10) | class PassthruRunnerTest extends TestCase
    method test_it_is_a_runner (line 12) | public function test_it_is_a_runner()
    method test_it_returns_the_exit_code_of_the_run_command (line 17) | public function test_it_returns_the_exit_code_of_the_run_command()
    method test_it_outputs_commands_output (line 24) | public function test_it_outputs_commands_output()

FILE: tests/Tool/CollectionTest.php
  class CollectionTest (line 8) | class CollectionTest extends TestCase
    method test_it_iterates_over_its_elements (line 10) | public function test_it_iterates_over_its_elements()
    method test_it_is_cast_to_an_array (line 19) | public function test_it_is_cast_to_an_array()
    method test_it_merges_two_collections (line 28) | public function test_it_merges_two_collections()
    method test_it_filters_elements_in_the_collection (line 40) | public function test_it_filters_elements_in_the_collection()
    method test_it_maps_elements_in_the_collection (line 51) | public function test_it_maps_elements_in_the_collection()
    method test_it_folds_the_collection_left (line 62) | public function test_it_folds_the_collection_left()
    method test_it_counts_its_elements (line 73) | public function test_it_counts_its_elements()
    method test_it_checks_if_collection_is_empty (line 79) | public function test_it_checks_if_collection_is_empty()
    method test_it_sorts_the_collection (line 85) | public function test_it_sorts_the_collection()
    method assertIterates (line 96) | private function assertIterates(array $elements, Collection $c, string...

FILE: tests/Tool/Command/BoxBuildCommandTest.php
  class BoxBuildCommandTest (line 9) | class BoxBuildCommandTest extends TestCase
    method test_it_is_a_command (line 21) | public function test_it_is_a_command()
    method test_it_generates_the_installation_command (line 34) | public function test_it_generates_the_installation_command()
    method test_it_tries_to_guess_version_number_if_not_given_one (line 51) | public function test_it_tries_to_guess_version_number_if_not_given_one()
    method test_it_uses_a_generic_directory_if_name_cannot_be_guessed_from_the_repository (line 63) | public function test_it_uses_a_generic_directory_if_name_cannot_be_gue...

FILE: tests/Tool/Command/ComposerBinPluginCommandTest.php
  class ComposerBinPluginCommandTest (line 11) | class ComposerBinPluginCommandTest extends TestCase
    method setUp (line 18) | protected function setUp(): void
    method test_it_is_a_command (line 27) | public function test_it_is_a_command()
    method test_it_generates_the_installation_command (line 32) | public function test_it_generates_the_installation_command()
    method test_it_exposes_the_package_and_namespace (line 37) | public function test_it_exposes_the_package_and_namespace()
    method test_it_optionally_creates_a_symlink (line 43) | public function test_it_optionally_creates_a_symlink()
    method test_it_does_not_create_a_symlink_if_links_option_was_not_given (line 55) | public function test_it_does_not_create_a_symlink_if_links_option_was_...

FILE: tests/Tool/Command/ComposerBinPluginLinkCommandTest.php
  class ComposerBinPluginLinkCommandTest (line 9) | final class ComposerBinPluginLinkCommandTest extends TestCase
    method setUp (line 17) | protected function setUp(): void
    method test_it_is_a_command (line 22) | public function test_it_is_a_command()
    method test_it_generates_a_symlink_command (line 27) | public function test_it_generates_a_symlink_command()

FILE: tests/Tool/Command/ComposerGlobalInstallCommandTest.php
  class ComposerGlobalInstallCommandTest (line 9) | class ComposerGlobalInstallCommandTest extends TestCase
    method setUp (line 15) | protected function setUp(): void
    method test_it_is_a_command (line 20) | public function test_it_is_a_command()
    method test_it_exposes_the_package_name (line 25) | public function test_it_exposes_the_package_name()
    method test_it_generates_the_installation_command (line 30) | public function test_it_generates_the_installation_command()

FILE: tests/Tool/Command/ComposerGlobalMultiInstallCommandTest.php
  class ComposerGlobalMultiInstallCommandTest (line 12) | class ComposerGlobalMultiInstallCommandTest extends TestCase
    method test_it_is_a_command (line 14) | public function test_it_is_a_command()
    method test_it_generates_a_single_installation_command (line 24) | public function test_it_generates_a_single_installation_command()
    method test_it_throws_an_exception_if_there_is_no_commands (line 34) | public function test_it_throws_an_exception_if_there_is_no_commands()

FILE: tests/Tool/Command/ComposerInstallCommandTest.php
  class ComposerInstallCommandTest (line 9) | class ComposerInstallCommandTest extends TestCase
    method test_it_is_a_command (line 15) | public function test_it_is_a_command()
    method test_it_generates_the_installation_command (line 22) | public function test_it_generates_the_installation_command()
    method test_it_tries_to_guess_version_number_if_not_given_one (line 31) | public function test_it_tries_to_guess_version_number_if_not_given_one()

FILE: tests/Tool/Command/FileDownloadCommandTest.php
  class FileDownloadCommandTest (line 9) | class FileDownloadCommandTest extends TestCase
    method setUp (line 16) | protected function setUp(): void
    method test_it_is_a_command (line 21) | public function test_it_is_a_command()
    method test_it_generates_the_installation_command (line 26) | public function test_it_generates_the_installation_command()

FILE: tests/Tool/Command/MultiStepCommandTest.php
  class MultiStepCommandTest (line 12) | class MultiStepCommandTest extends TestCase
    method test_it_is_a_command (line 14) | public function test_it_is_a_command()
    method test_it_glues_all_its_subcommands (line 21) | public function test_it_glues_all_its_subcommands()
    method test_it_glues_all_its_subcommands_with_a_custom_glue (line 31) | public function test_it_glues_all_its_subcommands_with_a_custom_glue()
    method test_it_throws_an_exception_if_there_is_no_steps (line 41) | public function test_it_throws_an_exception_if_there_is_no_steps()
    method command (line 48) | private function command(string $commandString): Command

FILE: tests/Tool/Command/OptimisedComposerBinPluginCommandTest.php
  class OptimisedComposerBinPluginCommandTest (line 13) | class OptimisedComposerBinPluginCommandTest extends TestCase
    method test_it_is_a_command (line 15) | public function test_it_is_a_command()
    method test_it_groups_composer_bin_command_by_namespace (line 20) | public function test_it_groups_composer_bin_command_by_namespace()
    method test_it_throws_an_exception_if_there_is_no_commands (line 33) | public function test_it_throws_an_exception_if_there_is_no_commands()
    method test_it_creates_links_to_composer_bin_commands (line 40) | public function test_it_creates_links_to_composer_bin_commands()
    method test_it_does_not_create_links_if_commands_have_no_links_defined (line 77) | public function test_it_does_not_create_links_if_commands_have_no_link...

FILE: tests/Tool/Command/PharDownloadCommandTest.php
  class PharDownloadCommandTest (line 9) | class PharDownloadCommandTest extends TestCase
    method setUp (line 16) | protected function setUp(): void
    method test_it_is_a_command (line 21) | public function test_it_is_a_command()
    method test_it_generates_the_installation_command (line 26) | public function test_it_generates_the_installation_command()

FILE: tests/Tool/Command/PhiveInstallCommandTest.php
  class PhiveInstallCommandTest (line 9) | class PhiveInstallCommandTest extends TestCase
    method setUp (line 17) | protected function setUp(): void
    method test_it_is_a_command (line 22) | public function test_it_is_a_command()
    method test_it_generates_the_installation_command (line 27) | public function test_it_generates_the_installation_command()
    method test_it_accepts_unsigned_phar_command (line 32) | public function test_it_accepts_unsigned_phar_command()

FILE: tests/Tool/Command/ShCommandTest.php
  class ShCommandTest (line 9) | class ShCommandTest extends TestCase
    method test_it_is_a_command (line 11) | public function test_it_is_a_command()
    method test_it_returns_the_command (line 16) | public function test_it_returns_the_command()

FILE: tests/Tool/Command/TestCommandTest.php
  class TestCommandTest (line 9) | class TestCommandTest extends TestCase
    method test_it_is_a_command (line 11) | public function test_it_is_a_command()
    method test_it_generates_the_command (line 16) | public function test_it_generates_the_command()

FILE: tests/Tool/FilterTest.php
  class FilterTest (line 11) | class FilterTest extends TestCase
    method test_it_returns_true_if_no_excluded_tags_were_defined (line 13) | public function test_it_returns_true_if_no_excluded_tags_were_defined()
    method test_it_returns_true_if_no_excluded_tags_match (line 20) | public function test_it_returns_true_if_no_excluded_tags_match()
    method test_it_returns_true_if_tool_has_no_tags (line 27) | public function test_it_returns_true_if_tool_has_no_tags()
    method test_it_returns_true_if_neither_tool_nor_excluded_tags_were_defined (line 34) | public function test_it_returns_true_if_neither_tool_nor_excluded_tags...
    method test_it_returns_false_if_one_excluded_tag_matches (line 41) | public function test_it_returns_false_if_one_excluded_tag_matches()
    method test_it_returns_false_if_multiple_excluded_tags_match (line 48) | public function test_it_returns_false_if_multiple_excluded_tags_match()
    method test_it_returns_false_if_all_excluded_tags_match (line 55) | public function test_it_returns_false_if_all_excluded_tags_match()
    method test_it_returns_true_if_a_tag_matches (line 62) | public function test_it_returns_true_if_a_tag_matches()
    method test_it_returns_true_if_all_tags_match_exactly (line 69) | public function test_it_returns_true_if_all_tags_match_exactly()
    method test_it_returns_true_if_all_tags_match (line 76) | public function test_it_returns_true_if_all_tags_match()
    method test_it_returns_false_if_the_tool_has_no_tags_to_match (line 83) | public function test_it_returns_false_if_the_tool_has_no_tags_to_match()
    method test_it_returns_false_if_a_tag_is_both_included_and_excluded (line 90) | public function test_it_returns_false_if_a_tag_is_both_included_and_ex...
    method tool (line 97) | private function tool(array $tags): Tool

FILE: tests/Tool/ToolTest.php
  class ToolTest (line 11) | class ToolTest extends TestCase
    method test_it_exposes_its_properties (line 13) | public function test_it_exposes_its_properties()
    method test_tags_can_only_be_strings (line 28) | public function test_tags_can_only_be_strings()
    method anyCommand (line 40) | public function anyCommand(): object

FILE: tests/UseCase/InstallToolsTest.php
  class InstallToolsTest (line 24) | class InstallToolsTest extends TestCase
    method setUp (line 30) | protected function setUp(): void
    method test_it_returns_a_multi_step_command (line 36) | public function test_it_returns_a_multi_step_command()
    method test_it_groups_composer_global_install_commands (line 47) | public function test_it_groups_composer_global_install_commands()
    method test_it_does_not_include_empty_commands (line 59) | public function test_it_does_not_include_empty_commands()
    method test_it_groups_composer_bin_plugin_commands (line 71) | public function test_it_groups_composer_bin_plugin_commands()
    method test_it_includes_installation_tagged_commands_before_other_ones (line 83) | public function test_it_includes_installation_tagged_commands_before_o...
    method test_it_includes_shell_commands (line 96) | public function test_it_includes_shell_commands()
    method test_it_includes_multi_step_commands (line 107) | public function test_it_includes_multi_step_commands()
    method test_it_includes_composer_install_commands (line 121) | public function test_it_includes_composer_install_commands()
    method test_it_includes_box_build_commands (line 132) | public function test_it_includes_box_build_commands()
    method test_it_includes_phar_download_commands (line 143) | public function test_it_includes_phar_download_commands()
    method test_it_includes_phive_install_commands (line 154) | public function test_it_includes_phive_install_commands()
    method test_it_includes_file_download_commands (line 164) | public function test_it_includes_file_download_commands()
    method filter (line 175) | private function filter(): Filter
    method tool (line 180) | private function tool(Command $command, array $tags = []): Tool
    method givenToolsFor (line 192) | private function givenToolsFor(Filter $filter, Collection $tools): void
    method givenTools (line 199) | private function givenTools(Collection $tools): void

FILE: tests/UseCase/ListToolsTest.php
  class ListToolsTest (line 14) | class ListToolsTest extends TestCase
    method test_it_returns_loaded_tools (line 16) | public function test_it_returns_loaded_tools()
    method filter (line 28) | private function filter(): Filter
    method anyTool (line 33) | private function anyTool(): Tool
    method givenToolsFor (line 45) | private function givenToolsFor(Filter $filter, Collection $tools): Tools

FILE: tests/UseCase/TestToolsTest.php
  class TestToolsTest (line 14) | class TestToolsTest extends TestCase
    method test_it_returns_test_aggregated_test_command (line 16) | public function test_it_returns_test_aggregated_test_command()
    method tool (line 30) | private function tool(Command $testCommand): Tool
    method command (line 42) | private function command(string $command): Command
    method tools (line 47) | private function tools(array $testCommands, Filter $filter): Tools
    method filter (line 57) | private function filter(): Filter
Condensed preview — 131 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (263K chars).
[
  {
    "path": ".gitattributes",
    "chars": 442,
    "preview": "/.gitattributes export-ignore\n/.github export-ignore\n/.gitignore export-ignore\n/.php_cs export-ignore\n/.travis.yml expor"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 17,
    "preview": "github: [jakzal]\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "chars": 293,
    "preview": "<!--\nPlease read the CONTRIBUTING.md to learn about contributing to this project.\n\nPlease also note that this project is"
  },
  {
    "path": ".github/workflows/build.yml",
    "chars": 3314,
    "preview": "name: Build\n\non:\n    push:\n        branches: [master]\n    pull_request:\n    release:\n        types: [created]\n    schedu"
  },
  {
    "path": ".github/workflows/publish-website.yml",
    "chars": 873,
    "preview": "name: Publish the website\n\non:\n    push:\n        branches: [master]\n    release:\n        types: [created]\n\njobs:\n    pub"
  },
  {
    "path": ".github/workflows/update-phars.yml",
    "chars": 1067,
    "preview": "name: Update PHARs\n\non:\n    schedule:\n        -   cron: '30 3 * * *'\njobs:\n    update-phars:\n        runs-on: ubuntu-lat"
  },
  {
    "path": ".gitignore",
    "chars": 111,
    "preview": "/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",
    "chars": 1102,
    "preview": "<?php\n\n$finder = PhpCsFixer\\Finder::create()\n    ->in(['src', 'tests'])\n;\n\nreturn (new PhpCsFixer\\Config())\n    ->setRul"
  },
  {
    "path": ".scrutinizer.yml",
    "chars": 945,
    "preview": "inherit: true\n\nbuild:\n  environment:\n    php:\n      version: 8.2\n    variables:\n      XDEBUG_MODE: coverage\n  tests:\n   "
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "chars": 3220,
    "preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, w"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 4795,
    "preview": "# Contributing\n\nWhen contributing to this repository send a new pull request.\nIf your change is big or complex, or you s"
  },
  {
    "path": "LICENSE",
    "chars": 1072,
    "preview": "Copyright (c) 2017 Jakub Zalas <jakub@zalas.pl>\n\nPermission is hereby granted, free of charge, to any person\nobtaining a"
  },
  {
    "path": "Makefile",
    "chars": 3912,
    "preview": "default: build\n\nPHP_VERSION:=$(shell php -r 'echo PHP_MAJOR_VERSION.\".\".PHP_MINOR_VERSION;')\nTOOLBOX_VERSION?=dev\n\nbuild"
  },
  {
    "path": "README.md",
    "chars": 18486,
    "preview": "# Toolbox\n\n[![Build Status](https://github.com/jakzal/toolbox/workflows/Build/badge.svg)](https://github.com/jakzal/tool"
  },
  {
    "path": "bin/devkit.php",
    "chars": 11790,
    "preview": "#!/usr/bin/env php\n<?php declare(strict_types=1);\n\nrequire __DIR__ . '/../vendor/autoload.php';\n\nuse Symfony\\Component\\C"
  },
  {
    "path": "bin/toolbox.php",
    "chars": 165,
    "preview": "#!/usr/bin/env php\n<?php\n\nrequire __DIR__ . '/../vendor/autoload.php';\n\n(new Zalas\\Toolbox\\Cli\\Application('dev', new Za"
  },
  {
    "path": "box-devkit.json.dist",
    "chars": 396,
    "preview": "{\n    \"base-path\": \"build/devkit-phar\",\n    \"output\": \"../devkit.phar\",\n    \"compression\": \"GZ\",\n    \"directories\": [\".\""
  },
  {
    "path": "box.json.dist",
    "chars": 391,
    "preview": "{\n    \"base-path\": \"build/phar\",\n    \"output\": \"../toolbox.phar\",\n    \"compression\": \"GZ\",\n    \"directories\": [\".\"],\n   "
  },
  {
    "path": "composer.json",
    "chars": 1006,
    "preview": "{\n    \"name\": \"zalas/toolbox\",\n    \"description\": \"Helps to discover and install tools\",\n    \"type\": \"project\",\n    \"req"
  },
  {
    "path": "deptrac.yaml",
    "chars": 1546,
    "preview": "parameters:\n  paths:\n    - ./src\n  exclude_files: []\n  layers:\n    - name: Cli\n      collectors:\n        - type: classLi"
  },
  {
    "path": "infection.json.dist",
    "chars": 494,
    "preview": "{\n    \"timeout\": 2,\n    \"source\": {\n        \"directories\": [\n            \"src\"\n        ]\n    },\n    \"logs\": {\n        \"t"
  },
  {
    "path": "phpunit.xml.dist",
    "chars": 1014,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:noNamespaceSch"
  },
  {
    "path": "resources/architecture.json",
    "chars": 1939,
    "preview": "{\n    \"tools\": [\n        {\n            \"name\": \"dephpend\",\n            \"summary\": \"Detect flaws in your architecture\",\n "
  },
  {
    "path": "resources/checkstyle.json",
    "chars": 3137,
    "preview": "{\n    \"tools\": [\n        {\n            \"name\": \"ecs\",\n            \"summary\": \"Sets up and runs coding standard checks\",\n"
  },
  {
    "path": "resources/compatibility.json",
    "chars": 1496,
    "preview": "{\n    \"tools\": [\n        {\n            \"name\": \"php-semver-checker\",\n            \"summary\": \"Suggests a next version acc"
  },
  {
    "path": "resources/composer.json",
    "chars": 4236,
    "preview": "{\n    \"tools\": [\n        {\n            \"name\": \"composer-normalize\",\n            \"summary\": \"Composer plugin to normaliz"
  },
  {
    "path": "resources/deprecation.json",
    "chars": 996,
    "preview": "{\n    \"tools\": [\n        {\n            \"name\": \"deprecation-detector\",\n            \"summary\": \"Finds usages of deprecate"
  },
  {
    "path": "resources/documentation.json",
    "chars": 991,
    "preview": "{\n    \"tools\": [\n        {\n            \"name\": \"phpDocumentor\",\n            \"summary\": \"Documentation generator\",\n      "
  },
  {
    "path": "resources/linting.json",
    "chars": 3040,
    "preview": "{\n    \"tools\": [\n        {\n            \"name\": \"parallel-lint\",\n            \"summary\": \"Checks PHP file syntax\",\n       "
  },
  {
    "path": "resources/metrics.json",
    "chars": 2197,
    "preview": "{\n    \"tools\": [\n        {\n            \"name\": \"phpinsights\",\n            \"summary\": \"Analyses code quality, style, arch"
  },
  {
    "path": "resources/phpcs.json",
    "chars": 1566,
    "preview": "{\n  \"tools\": [\n    {\n      \"name\": \"phpcs\",\n      \"summary\": \"Detects coding standard violations\",\n      \"website\": \"htt"
  },
  {
    "path": "resources/phpstan.json",
    "chars": 6797,
    "preview": "{\n    \"tools\": [\n        {\n            \"name\": \"phpstan\",\n            \"summary\": \"Static Analysis Tool\",\n            \"we"
  },
  {
    "path": "resources/pre-installation.json",
    "chars": 2089,
    "preview": "{\n  \"tools\": [\n    {\n      \"name\": \"composer\",\n      \"summary\": \"Dependency Manager for PHP\",\n      \"website\": \"https://"
  },
  {
    "path": "resources/psalm.json",
    "chars": 2347,
    "preview": "{\n    \"tools\": [\n        {\n            \"name\": \"psalm\",\n            \"summary\": \"Finds errors in PHP applications\",\n     "
  },
  {
    "path": "resources/refactoring.json",
    "chars": 1032,
    "preview": "{\n    \"tools\": [\n        {\n            \"name\": \"churn\",\n            \"summary\": \"Discovers good candidates for refactorin"
  },
  {
    "path": "resources/security.json",
    "chars": 1362,
    "preview": "{\n    \"tools\": [\n        {\n            \"name\": \"psecio-parse\",\n            \"summary\": \"Scans code for potential security"
  },
  {
    "path": "resources/test.json",
    "chars": 7676,
    "preview": "{\n    \"tools\": [\n        {\n            \"name\": \"behat\",\n            \"summary\": \"Helps to test business expectations\",\n  "
  },
  {
    "path": "resources/tools.json",
    "chars": 3053,
    "preview": "{\n  \"tools\": [\n    {\n      \"name\": \"diffFilter\",\n      \"summary\": \"Applies QA tools to run on a single pull request\",\n  "
  },
  {
    "path": "scoper.inc.php",
    "chars": 634,
    "preview": "<?php declare(strict_types=1);\n\nreturn [\n    // Whitelist globals so that Symfony polyfills are not scoped\n    'expose-g"
  },
  {
    "path": "src/Cli/Application.php",
    "chars": 3287,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Cli;\n\nuse Psr\\Container\\ContainerInterface;\nuse Symfony\\Componen"
  },
  {
    "path": "src/Cli/Command/DefaultTag.php",
    "chars": 395,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Cli\\Command;\n\ntrait DefaultTag\n{\n    private function defaultExc"
  },
  {
    "path": "src/Cli/Command/DefaultTargetDir.php",
    "chars": 254,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Cli\\Command;\n\ntrait DefaultTargetDir\n{\n    private function defa"
  },
  {
    "path": "src/Cli/Command/InstallCommand.php",
    "chars": 1651,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Cli\\Command;\n\nuse Symfony\\Component\\Console\\Command\\Command;\nuse"
  },
  {
    "path": "src/Cli/Command/ListCommand.php",
    "chars": 1923,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Cli\\Command;\n\nuse Symfony\\Component\\Console\\Command\\Command;\nuse"
  },
  {
    "path": "src/Cli/Command/TestCommand.php",
    "chars": 1668,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Cli\\Command;\n\nuse Symfony\\Component\\Console\\Command\\Command;\nuse"
  },
  {
    "path": "src/Cli/Runner/DryRunner.php",
    "chars": 503,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Cli\\Runner;\n\nuse Symfony\\Component\\Console\\Output\\OutputInterfac"
  },
  {
    "path": "src/Cli/ServiceContainer/LazyRunner.php",
    "chars": 959,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Cli\\ServiceContainer;\n\nuse Psr\\Container\\ContainerExceptionInter"
  },
  {
    "path": "src/Cli/ServiceContainer/RunnerFactory.php",
    "chars": 2312,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Cli\\ServiceContainer;\n\nuse Psr\\Container\\ContainerExceptionInter"
  },
  {
    "path": "src/Cli/ServiceContainer.php",
    "chars": 4186,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Cli;\n\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Con"
  },
  {
    "path": "src/Json/Factory/Assert.php",
    "chars": 544,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Json\\Factory;\n\nfinal class Assert\n{\n    public static function r"
  },
  {
    "path": "src/Json/Factory/BoxBuildCommandFactory.php",
    "chars": 517,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Json\\Factory;\n\nuse Zalas\\Toolbox\\Tool\\Command;\nuse Zalas\\Toolbox"
  },
  {
    "path": "src/Json/Factory/ComposerBinPluginCommandFactory.php",
    "chars": 1019,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Json\\Factory;\n\nuse Zalas\\Toolbox\\Tool\\Collection;\nuse Zalas\\Tool"
  },
  {
    "path": "src/Json/Factory/ComposerGlobalInstallCommandFactory.php",
    "chars": 445,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Json\\Factory;\n\nuse Zalas\\Toolbox\\Tool\\Command;\nuse Zalas\\Toolbox"
  },
  {
    "path": "src/Json/Factory/ComposerInstallCommandFactory.php",
    "chars": 494,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Json\\Factory;\n\nuse Zalas\\Toolbox\\Tool\\Command;\nuse Zalas\\Toolbox"
  },
  {
    "path": "src/Json/Factory/FileDownloadCommandFactory.php",
    "chars": 427,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Json\\Factory;\n\nuse Zalas\\Toolbox\\Tool\\Command;\nuse Zalas\\Toolbox"
  },
  {
    "path": "src/Json/Factory/PharDownloadCommandFactory.php",
    "chars": 427,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Json\\Factory;\n\nuse Zalas\\Toolbox\\Tool\\Command;\nuse Zalas\\Toolbox"
  },
  {
    "path": "src/Json/Factory/PhiveInstallCommandFactory.php",
    "chars": 454,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Json\\Factory;\n\nuse Zalas\\Toolbox\\Tool\\Command;\nuse Zalas\\Toolbox"
  },
  {
    "path": "src/Json/Factory/ShCommandFactory.php",
    "chars": 369,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Json\\Factory;\n\nuse Zalas\\Toolbox\\Tool\\Command;\nuse Zalas\\Toolbox"
  },
  {
    "path": "src/Json/Factory/ToolFactory.php",
    "chars": 2530,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Json\\Factory;\n\nuse Zalas\\Toolbox\\Tool\\Collection;\nuse Zalas\\Tool"
  },
  {
    "path": "src/Json/JsonTools.php",
    "chars": 1910,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Json;\n\nuse InvalidArgumentException;\nuse RuntimeException;\nuse Z"
  },
  {
    "path": "src/Runner/ParametrisedRunner.php",
    "chars": 1003,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Runner;\n\nuse Zalas\\Toolbox\\Tool\\Command;\n\nfinal class Parametris"
  },
  {
    "path": "src/Runner/PassthruRunner.php",
    "chars": 278,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Runner;\n\nuse Zalas\\Toolbox\\Tool\\Command;\n\nfinal class PassthruRu"
  },
  {
    "path": "src/Runner/Runner.php",
    "chars": 167,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Runner;\n\nuse Zalas\\Toolbox\\Tool\\Command;\n\ninterface Runner\n{\n   "
  },
  {
    "path": "src/Tool/Collection.php",
    "chars": 1492,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tool;\n\nuse Countable;\nuse IteratorAggregate;\nuse Traversable;\n\nc"
  },
  {
    "path": "src/Tool/Command/BoxBuildCommand.php",
    "chars": 1402,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tool\\Command;\n\nuse Zalas\\Toolbox\\Tool\\Command;\n\nfinal class BoxB"
  },
  {
    "path": "src/Tool/Command/ComposerBinPluginCommand.php",
    "chars": 1169,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tool\\Command;\n\nuse Zalas\\Toolbox\\Tool\\Collection;\nuse Zalas\\Tool"
  },
  {
    "path": "src/Tool/Command/ComposerBinPluginLinkCommand.php",
    "chars": 714,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tool\\Command;\n\nuse Zalas\\Toolbox\\Tool\\Command;\n\nfinal class Comp"
  },
  {
    "path": "src/Tool/Command/ComposerGlobalInstallCommand.php",
    "chars": 534,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tool\\Command;\n\nuse Zalas\\Toolbox\\Tool\\Command;\n\nfinal class Comp"
  },
  {
    "path": "src/Tool/Command/ComposerGlobalMultiInstallCommand.php",
    "chars": 954,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tool\\Command;\n\nuse InvalidArgumentException;\nuse Zalas\\Toolbox\\T"
  },
  {
    "path": "src/Tool/Command/ComposerInstallCommand.php",
    "chars": 869,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tool\\Command;\n\nuse Zalas\\Toolbox\\Tool\\Command;\n\nfinal class Comp"
  },
  {
    "path": "src/Tool/Command/FileDownloadCommand.php",
    "chars": 495,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tool\\Command;\n\nuse Zalas\\Toolbox\\Tool\\Command;\n\nfinal class File"
  },
  {
    "path": "src/Tool/Command/MultiStepCommand.php",
    "chars": 751,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tool\\Command;\n\nuse InvalidArgumentException;\nuse Zalas\\Toolbox\\T"
  },
  {
    "path": "src/Tool/Command/OptimisedComposerBinPluginCommand.php",
    "chars": 2048,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tool\\Command;\n\nuse InvalidArgumentException;\nuse Zalas\\Toolbox\\T"
  },
  {
    "path": "src/Tool/Command/PharDownloadCommand.php",
    "chars": 522,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tool\\Command;\n\nuse Zalas\\Toolbox\\Tool\\Command;\n\nfinal class Phar"
  },
  {
    "path": "src/Tool/Command/PhiveInstallCommand.php",
    "chars": 901,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tool\\Command;\n\nuse Zalas\\Toolbox\\Tool\\Command;\n\nfinal class Phiv"
  },
  {
    "path": "src/Tool/Command/ShCommand.php",
    "chars": 360,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tool\\Command;\n\nuse Zalas\\Toolbox\\Tool\\Command;\n\nfinal class ShCo"
  },
  {
    "path": "src/Tool/Command/TestCommand.php",
    "chars": 571,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tool\\Command;\n\nuse Zalas\\Toolbox\\Tool\\Command;\n\nfinal class Test"
  },
  {
    "path": "src/Tool/Command.php",
    "chars": 127,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tool;\n\ninterface Command\n{\n    public function __toString(): str"
  },
  {
    "path": "src/Tool/Filter.php",
    "chars": 680,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tool;\n\nclass Filter\n{\n    /**\n     * @var string[]\n     */\n    p"
  },
  {
    "path": "src/Tool/Tool.php",
    "chars": 1211,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tool;\n\nclass Tool\n{\n    private string $name;\n    private string"
  },
  {
    "path": "src/Tool/Tools.php",
    "chars": 293,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tool;\n\nuse RuntimeException;\n\ninterface Tools\n{\n    /**\n     * @"
  },
  {
    "path": "src/UseCase/InstallTools.php",
    "chars": 3445,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\UseCase;\n\nuse Closure;\nuse Zalas\\Toolbox\\Tool\\Collection;\nuse Za"
  },
  {
    "path": "src/UseCase/ListTools.php",
    "chars": 412,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\UseCase;\n\nuse Zalas\\Toolbox\\Tool\\Collection;\nuse Zalas\\Toolbox\\T"
  },
  {
    "path": "src/UseCase/TestTools.php",
    "chars": 617,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\UseCase;\n\nuse Zalas\\Toolbox\\Tool\\Command;\nuse Zalas\\Toolbox\\Tool"
  },
  {
    "path": "tests/Cli/ApplicationTest.php",
    "chars": 4834,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Cli;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Symfony\\Componen"
  },
  {
    "path": "tests/Cli/Command/InstallCommandTest.php",
    "chars": 3934,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Cli\\Command;\n\nuse PHPUnit\\Framework\\MockObject\\Stub;\nuse Z"
  },
  {
    "path": "tests/Cli/Command/ListCommandTest.php",
    "chars": 3182,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Cli\\Command;\n\nuse PHPUnit\\Framework\\MockObject\\Stub;\nuse Z"
  },
  {
    "path": "tests/Cli/Command/TestCommandTest.php",
    "chars": 3764,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Cli\\Command;\n\nuse PHPUnit\\Framework\\MockObject\\Stub;\nuse Z"
  },
  {
    "path": "tests/Cli/Command/ToolboxCommandTestCase.php",
    "chars": 1652,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Cli\\Command;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Symfony\\"
  },
  {
    "path": "tests/Cli/Runner/DryRunnerTest.php",
    "chars": 1131,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Cli\\Runner;\n\nuse PHPUnit\\Framework\\MockObject\\MockObject;\n"
  },
  {
    "path": "tests/Cli/ServiceContainer/LazyRunnerTest.php",
    "chars": 1934,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Cli\\ServiceContainer;\n\nuse PHPUnit\\Framework\\MockObject\\Mo"
  },
  {
    "path": "tests/Cli/ServiceContainer/RunnerFactoryTest.php",
    "chars": 4122,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Cli\\ServiceContainer;\n\nuse PHPUnit\\Framework\\MockObject\\Mo"
  },
  {
    "path": "tests/Cli/ServiceContainerTest.php",
    "chars": 3392,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Cli;\n\nuse PHPUnit\\Framework\\Attributes\\DataProvider;\nuse P"
  },
  {
    "path": "tests/Json/Factory/AssertTest.php",
    "chars": 523,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Json\\Factory;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\T"
  },
  {
    "path": "tests/Json/Factory/BoxBuildCommandFactoryTest.php",
    "chars": 1894,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Json\\Factory;\n\nuse PHPUnit\\Framework\\Attributes\\DataProvid"
  },
  {
    "path": "tests/Json/Factory/ComposerBinPluginCommandFactoryTest.php",
    "chars": 1987,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Json\\Factory;\n\nuse PHPUnit\\Framework\\Attributes\\DataProvid"
  },
  {
    "path": "tests/Json/Factory/ComposerGlobalInstallCommandFactoryTest.php",
    "chars": 802,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Json\\Factory;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\T"
  },
  {
    "path": "tests/Json/Factory/ComposerInstallCommandFactoryTest.php",
    "chars": 1544,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Json\\Factory;\n\nuse PHPUnit\\Framework\\Attributes\\DataProvid"
  },
  {
    "path": "tests/Json/Factory/FileDownloadCommandFactoryTest.php",
    "chars": 1253,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Json\\Factory;\n\nuse PHPUnit\\Framework\\Attributes\\DataProvid"
  },
  {
    "path": "tests/Json/Factory/PharDownloadCommandFactoryTest.php",
    "chars": 1256,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Json\\Factory;\n\nuse PHPUnit\\Framework\\Attributes\\DataProvid"
  },
  {
    "path": "tests/Json/Factory/PhiveInstallCommandFactoryTest.php",
    "chars": 1790,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Json\\Factory;\n\nuse PHPUnit\\Framework\\Attributes\\DataProvid"
  },
  {
    "path": "tests/Json/Factory/ShCommandFactoryTest.php",
    "chars": 644,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Json\\Factory;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\T"
  },
  {
    "path": "tests/Json/Factory/ToolFactoryTest.php",
    "chars": 7713,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Json\\Factory;\n\nuse PHPUnit\\Framework\\Attributes\\DataProvid"
  },
  {
    "path": "tests/Json/JsonToolsTest.php",
    "chars": 3336,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Json;\n\nuse InvalidArgumentException;\nuse PHPUnit\\Framework"
  },
  {
    "path": "tests/Runner/ParametrisedRunnerTest.php",
    "chars": 1496,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Runner;\n\nuse PHPUnit\\Framework\\MockObject\\Stub;\nuse PHPUni"
  },
  {
    "path": "tests/Runner/PassthruRunnerTest.php",
    "chars": 889,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Runner;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\Toolbox"
  },
  {
    "path": "tests/Tool/CollectionTest.php",
    "chars": 3014,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Tool;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\Toolbox\\T"
  },
  {
    "path": "tests/Tool/Command/BoxBuildCommandTest.php",
    "chars": 2236,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Tool\\Command;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\T"
  },
  {
    "path": "tests/Tool/Command/ComposerBinPluginCommandTest.php",
    "chars": 2042,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Tool\\Command;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\T"
  },
  {
    "path": "tests/Tool/Command/ComposerBinPluginLinkCommandTest.php",
    "chars": 951,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Tool\\Command;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\T"
  },
  {
    "path": "tests/Tool/Command/ComposerGlobalInstallCommandTest.php",
    "chars": 931,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Tool\\Command;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\T"
  },
  {
    "path": "tests/Tool/Command/ComposerGlobalMultiInstallCommandTest.php",
    "chars": 1377,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Tool\\Command;\n\nuse InvalidArgumentException;\nuse PHPUnit\\F"
  },
  {
    "path": "tests/Tool/Command/ComposerInstallCommandTest.php",
    "chars": 1396,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Tool\\Command;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\T"
  },
  {
    "path": "tests/Tool/Command/FileDownloadCommandTest.php",
    "chars": 842,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Tool\\Command;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\T"
  },
  {
    "path": "tests/Tool/Command/MultiStepCommandTest.php",
    "chars": 1545,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Tool\\Command;\n\nuse InvalidArgumentException;\nuse PHPUnit\\F"
  },
  {
    "path": "tests/Tool/Command/OptimisedComposerBinPluginCommandTest.php",
    "chars": 4123,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Tool\\Command;\n\nuse InvalidArgumentException;\nuse PHPUnit\\F"
  },
  {
    "path": "tests/Tool/Command/PharDownloadCommandTest.php",
    "chars": 841,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Tool\\Command;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\T"
  },
  {
    "path": "tests/Tool/Command/PhiveInstallCommandTest.php",
    "chars": 1310,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Tool\\Command;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\T"
  },
  {
    "path": "tests/Tool/Command/ShCommandTest.php",
    "chars": 490,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Tool\\Command;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\T"
  },
  {
    "path": "tests/Tool/Command/TestCommandTest.php",
    "chars": 664,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Tool\\Command;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\T"
  },
  {
    "path": "tests/Tool/FilterTest.php",
    "chars": 3178,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Tool;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\Toolbox\\T"
  },
  {
    "path": "tests/Tool/ToolTest.php",
    "chars": 1400,
    "preview": "<?php\ndeclare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\Tool;\n\nuse PHPUnit\\Framework\\TestCase;\nuse TypeError;\nuse "
  },
  {
    "path": "tests/UseCase/InstallToolsTest.php",
    "chars": 7481,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\UseCase;\n\nuse PHPUnit\\Framework\\MockObject\\Stub;\nuse PHPUn"
  },
  {
    "path": "tests/UseCase/ListToolsTest.php",
    "chars": 1356,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\UseCase;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\Toolbo"
  },
  {
    "path": "tests/UseCase/TestToolsTest.php",
    "chars": 1590,
    "preview": "<?php declare(strict_types=1);\n\nnamespace Zalas\\Toolbox\\Tests\\UseCase;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Zalas\\Toolbo"
  },
  {
    "path": "tests/resources/invalid-tools.json",
    "chars": 16,
    "preview": "{\"tools\": \"bar\"}"
  },
  {
    "path": "tests/resources/invalid.json",
    "chars": 10,
    "preview": "{\"tools\":}"
  },
  {
    "path": "tests/resources/no-tools.json",
    "chars": 14,
    "preview": "{\"foo\": \"bar\"}"
  },
  {
    "path": "tests/resources/pre-installation.json",
    "chars": 1058,
    "preview": "{\n  \"tools\": [\n    {\n      \"name\": \"composer\",\n      \"summary\": \"Dependency Manager for PHP\",\n      \"website\": \"https://"
  },
  {
    "path": "tests/resources/tools.json",
    "chars": 2149,
    "preview": "{\n  \"tools\": [\n    {\n      \"name\": \"analyze\",\n      \"summary\": \"Visualizes metrics and source code\",\n      \"website\": \"h"
  },
  {
    "path": "tools/.gitignore",
    "chars": 13,
    "preview": "*\n!.gitignore"
  }
]

About this extraction

This page contains the full source code of the jakzal/toolbox GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 131 files (235.8 KB), approximately 62.6k tokens, and a symbol index with 465 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!